commit adf0d174978b7900c866d229cbb403b20135de80 Author: wehub-resource-sync Date: Mon Jul 13 13:17:32 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.agents/skills/frontend-unit-testing/SKILL.md b/.agents/skills/frontend-unit-testing/SKILL.md new file mode 100644 index 0000000..8bd056c --- /dev/null +++ b/.agents/skills/frontend-unit-testing/SKILL.md @@ -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//.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: `, `Events`, `Events: `, `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 diff --git a/.agents/skills/gradio-themes/SKILL.md b/.agents/skills/gradio-themes/SKILL.md new file mode 100644 index 0000000..8222083 --- /dev/null +++ b/.agents/skills/gradio-themes/SKILL.md @@ -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 `` shadow DOM. The page's `` 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 `` (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 `` 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 `` from `+layout.svelte`). Do *not* try to style `body` from `custom_css`. + +```python +# CORRECT — paints actual , 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 | \ No newline at end of file diff --git a/.agents/skills/gradio/SKILL.md b/.agents/skills/gradio/SKILL.md new file mode 100644 index 0000000..7a6a154 --- /dev/null +++ b/.agents/skills/gradio/SKILL.md @@ -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`. diff --git a/.agents/skills/gradio/references/api-signatures.md b/.agents/skills/gradio/references/api-signatures.md new file mode 100644 index 0000000..354587c --- /dev/null +++ b/.agents/skills/gradio/references/api-signatures.md @@ -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. diff --git a/.agents/skills/gradio/references/event-listeners.md b/.agents/skills/gradio/references/event-listeners.md new file mode 100644 index 0000000..5042479 --- /dev/null +++ b/.agents/skills/gradio/references/event-listeners.md @@ -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 diff --git a/.agents/skills/gradio/references/examples.md b/.agents/skills/gradio/references/examples.md new file mode 100644 index 0000000..b48c4cd --- /dev/null +++ b/.agents/skills/gradio/references/examples.md @@ -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() +``` diff --git a/.agents/skills/hf-gradio/SKILL.md b/.agents/skills/hf-gradio/SKILL.md new file mode 100644 index 0000000..bf5d776 --- /dev/null +++ b/.agents/skills/hf-gradio/SKILL.md @@ -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 +``` + +```bash +hf-gradio info +``` + +```bash +hf gradio info +``` + +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 +``` + +```bash +hf-gradio predict +``` + +```bash +hf gradio predict +``` + +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. \ No newline at end of file diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -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) diff --git a/.changeset/changeset.cjs b/.changeset/changeset.cjs new file mode 100644 index 0000000..de9958b --- /dev/null +++ b/.changeset/changeset.cjs @@ -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} 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} 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; diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..d3179ef --- /dev/null +++ b/.changeset/config.json @@ -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"] +} diff --git a/.changeset/fix_changelogs.cjs b/.changeset/fix_changelogs.cjs new file mode 100644 index 0000000..5d7138d --- /dev/null +++ b/.changeset/fix_changelogs.cjs @@ -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(); diff --git a/.changeset/great-ears-grab.md b/.changeset/great-ears-grab.md new file mode 100644 index 0000000..81f46b4 --- /dev/null +++ b/.changeset/great-ears-grab.md @@ -0,0 +1,5 @@ +--- +"gradio": minor +--- + +feat:Fix spurious separators from empty tokens in `HighlightedText` with `combine_adjacent=True` diff --git a/.changeset/itchy-steaks-walk.md b/.changeset/itchy-steaks-walk.md new file mode 100644 index 0000000..b7cd27c --- /dev/null +++ b/.changeset/itchy-steaks-walk.md @@ -0,0 +1,6 @@ +--- +"@gradio/code": patch +"gradio": patch +--- + +fix:Fix `gr.Code` not rendering when it has no initial value diff --git a/.changeset/pink-news-relax.md b/.changeset/pink-news-relax.md new file mode 100644 index 0000000..7f3b1b5 --- /dev/null +++ b/.changeset/pink-news-relax.md @@ -0,0 +1,5 @@ +--- +"gradio": patch +--- + +fix:Fix `gr.State` passing its callable default to event handlers instead of the called value diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.config/.prettierignore b/.config/.prettierignore new file mode 100644 index 0000000..d51dc49 --- /dev/null +++ b/.config/.prettierignore @@ -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/**/* \ No newline at end of file diff --git a/.config/.prettierrc.json b/.config/.prettierrc.json new file mode 100644 index 0000000..4e22682 --- /dev/null +++ b/.config/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "useTabs": true, + "singleQuote": false, + "trailingComma": "none", + "printWidth": 80, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/.config/demos.json b/.config/demos.json new file mode 100644 index 0000000..8774a19 --- /dev/null +++ b/.config/demos.json @@ -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" +] diff --git a/.config/eslint.config.js b/.config/eslint.config.js new file mode 100644 index 0000000..a4b4869 --- /dev/null +++ b/.config/eslint.config.js @@ -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" + } + } +]; diff --git a/.config/playwright.config.js b/.config/playwright.config.js new file mode 100644 index 0000000..354e5dd --- /dev/null +++ b/.config/playwright.config.js @@ -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; diff --git a/.config/postcss.config.cjs b/.config/postcss.config.cjs new file mode 100644 index 0000000..81b1976 --- /dev/null +++ b/.config/postcss.config.cjs @@ -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] +}; diff --git a/.config/pycompile-lite-wheel.py b/.config/pycompile-lite-wheel.py new file mode 100644 index 0000000..0d45b4c --- /dev/null +++ b/.config/pycompile-lite-wheel.py @@ -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) diff --git a/.config/setup_vite_tests.ts b/.config/setup_vite_tests.ts new file mode 100644 index 0000000..b012a77 --- /dev/null +++ b/.config/setup_vite_tests.ts @@ -0,0 +1,7 @@ +import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers"; +import "@testing-library/jest-dom/vitest"; + +declare module "vitest" { + interface Assertion + extends jest.Matchers, TestingLibraryMatchers {} +} diff --git a/.config/svelte.config.js b/.config/svelte.config.js new file mode 100644 index 0000000..bf37931 --- /dev/null +++ b/.config/svelte.config.js @@ -0,0 +1,10 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; + +export default { + preprocess: vitePreprocess(), + compilerOptions: { + experimental: { + async: true + } + } +}; diff --git a/.config/tailwind.config.cjs b/.config/tailwind.config.cjs new file mode 100644 index 0000000..fe92c25 --- /dev/null +++ b/.config/tailwind.config.cjs @@ -0,0 +1,12 @@ +module.exports = { + content: [ + "./src/**/*.{html,js,svelte,ts}", + "**/@gradio/**/*.{html,js,svelte,ts}" + ], + + theme: { + extend: {} + }, + + plugins: [require("@tailwindcss/forms")] +}; diff --git a/.config/vitest.config.ts b/.config/vitest.config.ts new file mode 100644 index 0000000..be7741d --- /dev/null +++ b/.config/vitest.config.ts @@ -0,0 +1,3 @@ +import config from "../js/spa/vite.config"; + +export default config; diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..1b49a6b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..17e4ab1 --- /dev/null +++ b/.dockerignore @@ -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 \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f4e045f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ + +root = true + +[{js/**,client/js/**}] +end_of_line = lf +insert_final_newline = true +indent_style = tab +tab_width = 2 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..5acfdd1 --- /dev/null +++ b/.git-blame-ignore-revs @@ -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 diff --git a/.github/ISSUE_TEMPLATE/bug_report_template.yml b/.github/ISSUE_TEMPLATE/bug_report_template.yml new file mode 100644 index 0000000..dc795e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_template.yml @@ -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. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..f7f2938 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..c51010a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -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. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..9be9432 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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` + diff --git a/.github/actions/changes/action.yml b/.github/actions/changes/action.yml new file mode 100644 index 0000000..44353f2 --- /dev/null +++ b/.github/actions/changes/action.yml @@ -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 diff --git a/.github/actions/install-all-deps/action.yml b/.github/actions/install-all-deps/action.yml new file mode 100644 index 0000000..29d67ac --- /dev/null +++ b/.github/actions/install-all-deps/action.yml @@ -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 diff --git a/.github/actions/install-frontend-deps/action.yml b/.github/actions/install-frontend-deps/action.yml new file mode 100644 index 0000000..1544273 --- /dev/null +++ b/.github/actions/install-frontend-deps/action.yml @@ -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 diff --git a/.github/configs/semgrep_rules.yaml b/.github/configs/semgrep_rules.yaml new file mode 100644 index 0000000..2800c6b --- /dev/null +++ b/.github/configs/semgrep_rules.yaml @@ -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 diff --git a/.github/filters.json b/.github/filters.json new file mode 100644 index 0000000..75cdcad --- /dev/null +++ b/.github/filters.json @@ -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" + ] +} diff --git a/.github/stale b/.github/stale new file mode 100644 index 0000000..9d23fb5 --- /dev/null +++ b/.github/stale @@ -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 diff --git a/.github/workflows/comment-queue.yml b/.github/workflows/comment-queue.yml new file mode 100644 index 0000000..a55c416 --- /dev/null +++ b/.github/workflows/comment-queue.yml @@ -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 }} diff --git a/.github/workflows/delete-stale-spaces.yml b/.github/workflows/delete-stale-spaces.yml new file mode 100644 index 0000000..e68fffa --- /dev/null +++ b/.github/workflows/delete-stale-spaces.yml @@ -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 }} diff --git a/.github/workflows/frontend_profiling.yml b/.github/workflows/frontend_profiling.yml new file mode 100644 index 0000000..de9a1cc --- /dev/null +++ b/.github/workflows/frontend_profiling.yml @@ -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 .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 = ''; + 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, + }); + } diff --git a/.github/workflows/generate-changeset.yml b/.github/workflows/generate-changeset.yml new file mode 100644 index 0000000..1753db5 --- /dev/null +++ b/.github/workflows/generate-changeset.yml @@ -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}}/ diff --git a/.github/workflows/npm-previews.yml b/.github/workflows/npm-previews.yml new file mode 100644 index 0000000..9c469bf --- /dev/null +++ b/.github/workflows/npm-previews.yml @@ -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 diff --git a/.github/workflows/previews-build.yml b/.github/workflows/previews-build.yml new file mode 100644 index 0000000..967969b --- /dev/null +++ b/.github/workflows/previews-build.yml @@ -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 diff --git a/.github/workflows/previews-deploy.yml b/.github/workflows/previews-deploy.yml new file mode 100644 index 0000000..1fe334b --- /dev/null +++ b/.github/workflows/previews-deploy.yml @@ -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}}/ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7a7095b --- /dev/null +++ b/.github/workflows/publish.yml @@ -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 diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 0000000..6115544 --- /dev/null +++ b/.github/workflows/semgrep.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 diff --git a/.github/workflows/storybook-build.yml b/.github/workflows/storybook-build.yml new file mode 100644 index 0000000..5c78fbd --- /dev/null +++ b/.github/workflows/storybook-build.yml @@ -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 diff --git a/.github/workflows/storybook-deploy.yml b/.github/workflows/storybook-deploy.yml new file mode 100644 index 0000000..94191a9 --- /dev/null +++ b/.github/workflows/storybook-deploy.yml @@ -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}}/ diff --git a/.github/workflows/sync-frontend.yml b/.github/workflows/sync-frontend.yml new file mode 100644 index 0000000..7f79b78 --- /dev/null +++ b/.github/workflows/sync-frontend.yml @@ -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 }} diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml new file mode 100644 index 0000000..a91642c --- /dev/null +++ b/.github/workflows/sync-skills.yml @@ -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 diff --git a/.github/workflows/test-functional.yml b/.github/workflows/test-functional.yml new file mode 100644 index 0000000..4d0cd1b --- /dev/null +++ b/.github/workflows/test-functional.yml @@ -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 diff --git a/.github/workflows/test-hygiene.yml b/.github/workflows/test-hygiene.yml new file mode 100644 index 0000000..faa75cb --- /dev/null +++ b/.github/workflows/test-hygiene.yml @@ -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/* diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml new file mode 100644 index 0000000..c01bf1a --- /dev/null +++ b/.github/workflows/test-python.yml @@ -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" diff --git a/.github/workflows/tests-js.yml b/.github/workflows/tests-js.yml new file mode 100644 index 0000000..8ce57bd --- /dev/null +++ b/.github/workflows/tests-js.yml @@ -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 diff --git a/.github/workflows/trigger-changeset.yml b/.github/workflows/trigger-changeset.yml new file mode 100644 index 0000000..ffad71e --- /dev/null +++ b/.github/workflows/trigger-changeset.yml @@ -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" diff --git a/.github/workflows/trigger-semgrep.yml b/.github/workflows/trigger-semgrep.yml new file mode 100644 index 0000000..41fdff9 --- /dev/null +++ b/.github/workflows/trigger-semgrep.yml @@ -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 }} diff --git a/.github/workflows/update-checks.yml b/.github/workflows/update-checks.yml new file mode 100644 index 0000000..6ea19c4 --- /dev/null +++ b/.github/workflows/update-checks.yml @@ -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 }} diff --git a/.github/workflows/website-build.yml b/.github/workflows/website-build.yml new file mode 100644 index 0000000..128f907 --- /dev/null +++ b/.github/workflows/website-build.yml @@ -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 diff --git a/.github/workflows/website-deploy.yml b/.github/workflows/website-deploy.yml new file mode 100644 index 0000000..8b185f9 --- /dev/null +++ b/.github/workflows/website-deploy.yml @@ -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}}/ diff --git a/.github/workflows/website-docs-build.yml b/.github/workflows/website-docs-build.yml new file mode 100644 index 0000000..bc23133 --- /dev/null +++ b/.github/workflows/website-docs-build.yml @@ -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." diff --git a/.github/workflows/website-docs-deploy.yml b/.github/workflows/website-docs-deploy.yml new file mode 100644 index 0000000..b2b4354 --- /dev/null +++ b/.github/workflows/website-docs-deploy.yml @@ -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}}/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59b408a --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.storybook/GradioContext.svelte b/.storybook/GradioContext.svelte new file mode 100644 index 0000000..28e37a7 --- /dev/null +++ b/.storybook/GradioContext.svelte @@ -0,0 +1,37 @@ + + +{#if i18nReady} + {@render children()} +{/if} diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..6457690 --- /dev/null +++ b/.storybook/main.ts @@ -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`). + 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; diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..6696b52 --- /dev/null +++ b/.storybook/preview.ts @@ -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; diff --git a/.storybook/test_files/audio_sample.mp3 b/.storybook/test_files/audio_sample.mp3 new file mode 100644 index 0000000..05f5675 Binary files /dev/null and b/.storybook/test_files/audio_sample.mp3 differ diff --git a/.storybook/test_files/bus.png b/.storybook/test_files/bus.png new file mode 100644 index 0000000..855b404 Binary files /dev/null and b/.storybook/test_files/bus.png differ diff --git a/.storybook/test_files/cheetah.jpg b/.storybook/test_files/cheetah.jpg new file mode 100644 index 0000000..d1fde62 Binary files /dev/null and b/.storybook/test_files/cheetah.jpg differ diff --git a/.storybook/test_files/lion.jpg b/.storybook/test_files/lion.jpg new file mode 100644 index 0000000..e9bf9f5 Binary files /dev/null and b/.storybook/test_files/lion.jpg differ diff --git a/.storybook/test_files/video_sample.mp4 b/.storybook/test_files/video_sample.mp4 new file mode 100644 index 0000000..7b2d7c7 Binary files /dev/null and b/.storybook/test_files/video_sample.mp4 differ diff --git a/.storybook/vitest.setup.ts b/.storybook/vitest.setup.ts new file mode 100644 index 0000000..e1bc3bb --- /dev/null +++ b/.storybook/vitest.setup.ts @@ -0,0 +1,4 @@ +import { setProjectAnnotations } from "@storybook/svelte"; +import * as previewAnnotations from "./preview"; + +setProjectAnnotations([previewAnnotations]); diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f94e184 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "phoenisx.cssvar", + "esbenp.prettier-vscode", + "svelte.svelte-vscode", + "charliermarsh.ruff" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2bbf04b --- /dev/null +++ b/.vscode/settings.json @@ -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" + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ab97a5d --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..273d61f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8128 @@ +# gradio + +## 6.20.0 + +### Features + +- [#13566](https://github.com/gradio-app/gradio/pull/13566) [`79b9171`](https://github.com/gradio-app/gradio/commit/79b91718b5c972c174ed042d8b04443bc2274076) - Gallery Download All. Thanks @dawoodkhan82! +- [#13544](https://github.com/gradio-app/gradio/pull/13544) [`784eb53`](https://github.com/gradio-app/gradio/commit/784eb536ab6596b0fa967419aaa93de39fa9418d) - workflow: show downstream output on subgraph run. Thanks @hannahblair! +- [#13582](https://github.com/gradio-app/gradio/pull/13582) [`59968e8`](https://github.com/gradio-app/gradio/commit/59968e8fdc86230eebb329ce0871dfaa9fa9c746) - workflow: update input change handling. Thanks @hannahblair! +- [#13543](https://github.com/gradio-app/gradio/pull/13543) [`0533483`](https://github.com/gradio-app/gradio/commit/0533483bccdee38f334a598f18297e8c02966343) - Migrate Image components to Svelte 5. Thanks @dawoodkhan82! +- [#13557](https://github.com/gradio-app/gradio/pull/13557) [`55241c8`](https://github.com/gradio-app/gradio/commit/55241c890fc5f0bbfddae4ce2ec14525d594e05e) - workflow: auto add node on port click. Thanks @hannahblair! +- [#13549](https://github.com/gradio-app/gradio/pull/13549) [`ec64242`](https://github.com/gradio-app/gradio/commit/ec642427856c9b7a3e90c35987d87bb98a6e9fbc) - workflow: validate model before invoking inference client. Thanks @hannahblair! +- [#13555](https://github.com/gradio-app/gradio/pull/13555) [`f7b872e`](https://github.com/gradio-app/gradio/commit/f7b872e52136d0f9308f0c7e0ebd49371148088a) - Replace deprecated Starlette 422 status constant in queue validator error path. Thanks @copilot-swe-agent! +- [#13574](https://github.com/gradio-app/gradio/pull/13574) [`75c5d1e`](https://github.com/gradio-app/gradio/commit/75c5d1eeec6f9cc7e84128812bc0c03b9682d7da) - workflow: inject token into fn functions. Thanks @hannahblair! + +### Fixes + +- [#13592](https://github.com/gradio-app/gradio/pull/13592) [`876d333`](https://github.com/gradio-app/gradio/commit/876d3334a9229e403193f7fd176af8c0720817cf) - Fix `JSONDecodeError` when loading cached examples with negative number outputs. Thanks @hysts! +- [#13596](https://github.com/gradio-app/gradio/pull/13596) [`1c5c538`](https://github.com/gradio-app/gradio/commit/1c5c53842df9c2750552d85c19a92e7e732cff3f) - Security: serve `/gradio_api/file=` via an SSRF-safe streaming proxy instead of an open redirect. Thanks @abidlabs! +- [#13595](https://github.com/gradio-app/gradio/pull/13595) [`4e72cd1`](https://github.com/gradio-app/gradio/commit/4e72cd1d6fd5cdb7d1267df4cfd1eec9a4b4214d) - workflow: forward `_token` to bound fn when no request session. Thanks @hannahblair! +- [#13540](https://github.com/gradio-app/gradio/pull/13540) [`98de45e`](https://github.com/gradio-app/gradio/commit/98de45e708305563f79614221aede2bf5d863922) - Fix gr.Plot collapsing to zero height for autosize Plotly figures. Thanks @hysts! +- [#13563](https://github.com/gradio-app/gradio/pull/13563) [`46d391b`](https://github.com/gradio-app/gradio/commit/46d391bd4385b6cfa31f10b35955c95c86c4cb85) - Fix embedded Gradio apps growing infinitely tall on HF Spaces when using `vh`/`%` heights or `fill_height`. Thanks @abidlabs! +- [#13580](https://github.com/gradio-app/gradio/pull/13580) [`63609f1`](https://github.com/gradio-app/gradio/commit/63609f10bb54523e5a996e584d1e9f635c02e80f) - Enforce `max_file_size` for multipart uploads to the `/component_server` route. Thanks @abidlabs! +- [#13571](https://github.com/gradio-app/gradio/pull/13571) [`c576675`](https://github.com/gradio-app/gradio/commit/c576675c1d0d9a917706bb830f7a738d9f96672f) - Fix `gr.ImageEditor` high CPU usage when idle by sleeping the render loop when nothing is changing. Thanks @abidlabs! +- [`dcc654d`](https://github.com/gradio-app/gradio/commit/dcc654d8467c9202a78c50b31e7d123e9804ebf4) - Fix ImageEditor transform tools and hidden cleanup. Thanks @dawoodkhan82! +- [#13588](https://github.com/gradio-app/gradio/pull/13588) [`2e80558`](https://github.com/gradio-app/gradio/commit/2e805588c8dc84a4a584898fd43b267c72209f78) - Fire `state.change()` for streaming (`.stream()`) events. Thanks @hysts! +- [#13597](https://github.com/gradio-app/gradio/pull/13597) [`7a595cb`](https://github.com/gradio-app/gradio/commit/7a595cb72d8a82151ee2231e86d20c91d0bb9062) - Fix ImageEditor brush texture resets. Thanks @dawoodkhan82! +- [#13581](https://github.com/gradio-app/gradio/pull/13581) [`461d82d`](https://github.com/gradio-app/gradio/commit/461d82df2689ec0c53e84b8722eaeae58fd2a6ae) - Use same-origin credentials in the JS client so cross-origin embeds work again. Thanks @hysts! +- [#13560](https://github.com/gradio-app/gradio/pull/13560) [`745f20c`](https://github.com/gradio-app/gradio/commit/745f20c9ec7d1e542c4b35f121b6f74e8e7c100c) - Fix `gr.Tabs.select()` event not firing when switching tabs. Thanks @abidlabs! +- [#13529](https://github.com/gradio-app/gradio/pull/13529) [`6438369`](https://github.com/gradio-app/gradio/commit/64383695167206d7775a364938a1005c662ea9c7) - Wait for in-flight head scripts before running js_on_load in gr.HTML. Thanks @hysts! +- [#13553](https://github.com/gradio-app/gradio/pull/13553) [`9a72d0c`](https://github.com/gradio-app/gradio/commit/9a72d0c76e62af365236a36c0137f7e0497d98cb) - Fix Column staying hidden after a multi-yield visibility update. Thanks @hysts! +- [#13561](https://github.com/gradio-app/gradio/pull/13561) [`882df35`](https://github.com/gradio-app/gradio/commit/882df35e9eaa7b2fb8767e23f03c66c46ab66d78) - Fix `TypeError: this.app.$destroy is not a function` when embedding a Gradio app with the `` web component. Thanks @abidlabs! + +## 6.19.0 + +### Features + +- [#13526](https://github.com/gradio-app/gradio/pull/13526) [`53cb4ca`](https://github.com/gradio-app/gradio/commit/53cb4cae1ec3521e9170d12867253516413ba37a) - Run `pnpm lint` and `pnpm ts:check` on CI. Thanks @abidlabs! +- [#13534](https://github.com/gradio-app/gradio/pull/13534) [`a11c728`](https://github.com/gradio-app/gradio/commit/a11c7282461ccef90f24a031d77a597ac9ae3d0e) - Re-translate i18n choices display names when the language is switched at runtime. Thanks @hysts! +- [#13524](https://github.com/gradio-app/gradio/pull/13524) [`d4d340d`](https://github.com/gradio-app/gradio/commit/d4d340d1bbc1dfdec72fa46431b43f6a808f084f) - Run `gr.Workflow` subgraphs via the Gradio API — each subgraph is exposed as a named endpoint (returning all of its outputs) reusing `/info`, `/call`, and `/api`, with a "View API" panel in the canvas. Thanks @abidlabs! + +### Fixes + +- [#13542](https://github.com/gradio-app/gradio/pull/13542) [`9d83502`](https://github.com/gradio-app/gradio/commit/9d83502215b56ed7747490afd0d5145b087a23e7) - Make dropdowns accessible to screen readers (combobox ARIA pattern). Thanks @hysts! +- [#13532](https://github.com/gradio-app/gradio/pull/13532) [`0a933b4`](https://github.com/gradio-app/gradio/commit/0a933b428f5b2158cb8c764f38abf0da2312d58a) - Fix `gr.SelectData` coordinates when the image in `gr.Image` does not fill its container. Thanks @hysts! +- [#13511](https://github.com/gradio-app/gradio/pull/13511) [`8eafb31`](https://github.com/gradio-app/gradio/commit/8eafb31ee17c134e65d1a006753b9a6ee4631f07) - refactor space and model discovery modal. Thanks @hannahblair! +- [#13533](https://github.com/gradio-app/gradio/pull/13533) [`e5ec1ca`](https://github.com/gradio-app/gradio/commit/e5ec1ca66c45e7e3a057b75ac2b8b86660b2827b) - Fix fullscreen button not working in ImageSlider, interactive Image, native plots, and AnnotatedImage. Thanks @hysts! +- [#13541](https://github.com/gradio-app/gradio/pull/13541) [`1d19f2b`](https://github.com/gradio-app/gradio/commit/1d19f2b21f6b12285089abc73468ac67b5b93e27) - Fix long unbroken text overflowing in the Markdown component. Thanks @hysts! + +## 6.18.0 + +### Features + +- [#13510](https://github.com/gradio-app/gradio/pull/13510) [`75c684e`](https://github.com/gradio-app/gradio/commit/75c684efb87624bee2fb63b08122564e6538509e) - Migrate Plot to Svelte 5. Thanks @dawoodkhan82! +- [#13501](https://github.com/gradio-app/gradio/pull/13501) [`e547392`](https://github.com/gradio-app/gradio/commit/e547392d794bc8ac0bcf60f6846229e95350f2c4) - workflow: update pipeline UX around pro accounts. Thanks @hannahblair! +- [#13517](https://github.com/gradio-app/gradio/pull/13517) [`100eaf2`](https://github.com/gradio-app/gradio/commit/100eaf2705861ccb71ea53748e2c6965a9c68bd0) - workflow: implement drag selection. Thanks @hannahblair! +- [#13509](https://github.com/gradio-app/gradio/pull/13509) [`dcd072c`](https://github.com/gradio-app/gradio/commit/dcd072cd57f09ffd2dc5f97ae6afc505894824a6) - Migrate Chatbot, Tabs, TabItem to Svelte 5. Thanks @dawoodkhan82! +- [#13502](https://github.com/gradio-app/gradio/pull/13502) [`429faeb`](https://github.com/gradio-app/gradio/commit/429faeb643fb1afc1722c0f63fafa11603f2c87f) - Ensure every component dispatches a `change` event when its value changes. Thanks @abidlabs! +- [#13516](https://github.com/gradio-app/gradio/pull/13516) [`8ec4cbc`](https://github.com/gradio-app/gradio/commit/8ec4cbc8de3bbc1d8ecb660ad8dd22ad35500e1c) - Translate i18n `choices` display names in choice-based components. Thanks @hysts! +- [#13513](https://github.com/gradio-app/gradio/pull/13513) [`df59064`](https://github.com/gradio-app/gradio/commit/df590646af4c2e518817201a2f1d3e334566669c) - Treat expired OAuth sessions as logged out users. Thanks @abidlabs! +- [#13507](https://github.com/gradio-app/gradio/pull/13507) [`a9550d8`](https://github.com/gradio-app/gradio/commit/a9550d863e2487eb0012d872e589d04dc60fb3c9) - Warn when a ` +``` + + Thanks [@hannahblair](https://github.com/hannahblair)! + +### Features + +- [#5268](https://github.com/gradio-app/gradio/pull/5268) [`f49028cf`](https://github.com/gradio-app/gradio/commit/f49028cfe3e21097001ddbda71c560b3d8b42e1c) - Move markdown & latex processing to the frontend for the gr.Markdown and gr.DataFrame components. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5215](https://github.com/gradio-app/gradio/pull/5215) [`fbdad78a`](https://github.com/gradio-app/gradio/commit/fbdad78af4c47454cbb570f88cc14bf4479bbceb) - Lazy load interactive or static variants of a component individually, rather than loading both variants regardless. This change will improve performance for many applications. Thanks [@pngwn](https://github.com/pngwn)! +- [#5216](https://github.com/gradio-app/gradio/pull/5216) [`4b58ea6d`](https://github.com/gradio-app/gradio/commit/4b58ea6d98e7a43b3f30d8a4cb6f379bc2eca6a8) - Update i18n tokens and locale files. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5283](https://github.com/gradio-app/gradio/pull/5283) [`a7460557`](https://github.com/gradio-app/gradio/commit/a74605572dd0d6bb41df6b38b120d656370dd67d) - Add height parameter and scrolling to `gr.Dataframe`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5232](https://github.com/gradio-app/gradio/pull/5232) [`c57d4c23`](https://github.com/gradio-app/gradio/commit/c57d4c232a97e03b4671f9e9edc3af456438fe89) - `gr.Radio` and `gr.CheckboxGroup` can now accept different names and values. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5219](https://github.com/gradio-app/gradio/pull/5219) [`e8fd4e4e`](https://github.com/gradio-app/gradio/commit/e8fd4e4ec68a6c974bc8c84b61f4a0ec50a85bc6) - Add `api_name` parameter to `gr.Interface`. Additionally, completely hide api page if show_api=False. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5280](https://github.com/gradio-app/gradio/pull/5280) [`a2f42e28`](https://github.com/gradio-app/gradio/commit/a2f42e28bd793bce4bed6d54164bb2a327a46fd5) - Allow updating the label of `gr.UpdateButton`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5112](https://github.com/gradio-app/gradio/pull/5112) [`1cefee7f`](https://github.com/gradio-app/gradio/commit/1cefee7fc05175aca23ba04b3a3fda7b97f49bf0) - chore(deps): update dependency marked to v7. Thanks [@renovate](https://github.com/apps/renovate)! +- [#5260](https://github.com/gradio-app/gradio/pull/5260) [`a773eaf7`](https://github.com/gradio-app/gradio/commit/a773eaf7504abb53b99885b3454dc1e027adbb42) - Stop passing inputs and preprocessing on iterators. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#4943](https://github.com/gradio-app/gradio/pull/4943) [`947d615d`](https://github.com/gradio-app/gradio/commit/947d615db6f76519d0e8bc0d1a0d7edf89df267b) - Sign in with Hugging Face (OAuth support). Thanks [@Wauplin](https://github.com/Wauplin)! +- [#5298](https://github.com/gradio-app/gradio/pull/5298) [`cf167cd1`](https://github.com/gradio-app/gradio/commit/cf167cd1dd4acd9aee225ff1cb6fac0e849806ba) - Create event listener table for components on docs. Thanks [@aliabd](https://github.com/aliabd)! +- [#5173](https://github.com/gradio-app/gradio/pull/5173) [`730f0c1d`](https://github.com/gradio-app/gradio/commit/730f0c1d54792eb11359e40c9f2326e8a6e39203) - Ensure gradio client works as expected for functions that return nothing. Thanks [@raymondtri](https://github.com/raymondtri)! +- [#5188](https://github.com/gradio-app/gradio/pull/5188) [`b22e1888`](https://github.com/gradio-app/gradio/commit/b22e1888fcf0843520525c1e4b7e1fe73fdeb948) - Fix the images in the theme builder to use permanent URI. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5221](https://github.com/gradio-app/gradio/pull/5221) [`f344592a`](https://github.com/gradio-app/gradio/commit/f344592aeb1658013235ded154107f72d86f24e7) - Allows setting a height to `gr.File` and improves the UI of the component. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5265](https://github.com/gradio-app/gradio/pull/5265) [`06982212`](https://github.com/gradio-app/gradio/commit/06982212dfbd613853133d5d0eebd75577967027) - Removes scrollbar from File preview when not needed. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5305](https://github.com/gradio-app/gradio/pull/5305) [`15075241`](https://github.com/gradio-app/gradio/commit/15075241fa7ad3f7fd9ae2a91e54faf8f19a46f9) - Rotate axes labels on LinePlot, BarPlot, and ScatterPlot. Thanks [@Faiga91](https://github.com/Faiga91)! +- [#5258](https://github.com/gradio-app/gradio/pull/5258) [`92282cea`](https://github.com/gradio-app/gradio/commit/92282cea6afdf7e9930ece1046d8a63be34b3cea) - Chatbot Avatar Images. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! +- [#5244](https://github.com/gradio-app/gradio/pull/5244) [`b3e50db9`](https://github.com/gradio-app/gradio/commit/b3e50db92f452f376aa2cc081326d40bb69d6dd7) - Remove aiohttp dependency. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5264](https://github.com/gradio-app/gradio/pull/5264) [`46a2b600`](https://github.com/gradio-app/gradio/commit/46a2b600a7ff030a9ea1560b882b3bf3ad266bbc) - ensure translations for audio work correctly. Thanks [@hannahblair](https://github.com/hannahblair)! + +### Fixes + +- [#5256](https://github.com/gradio-app/gradio/pull/5256) [`933db53e`](https://github.com/gradio-app/gradio/commit/933db53e93a1229fdf149556d61da5c4c7e1a331) - Better handling of empty dataframe in `gr.DataFrame`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5242](https://github.com/gradio-app/gradio/pull/5242) [`2b397791`](https://github.com/gradio-app/gradio/commit/2b397791fe2059e4beb72937ff0436f2d4d28b4b) - Fix message text overflow onto copy button in `gr.Chatbot`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5253](https://github.com/gradio-app/gradio/pull/5253) [`ddac7e4d`](https://github.com/gradio-app/gradio/commit/ddac7e4d0f55c3bdc6c3e9a9e24588b2563e4049) - Ensure File component uploads files to the server. Thanks [@pngwn](https://github.com/pngwn)! +- [#5179](https://github.com/gradio-app/gradio/pull/5179) [`6fb92b48`](https://github.com/gradio-app/gradio/commit/6fb92b48a916104db573602011a448b904d42e5e) - Fixes audio streaming issues. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#5295](https://github.com/gradio-app/gradio/pull/5295) [`7b8fa8aa`](https://github.com/gradio-app/gradio/commit/7b8fa8aa58f95f5046b9add64b40368bd3f1b700) - Allow caching examples with streamed output. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#5285](https://github.com/gradio-app/gradio/pull/5285) [`cdfd4217`](https://github.com/gradio-app/gradio/commit/cdfd42174a9c777eaee9c1209bf8e90d8c7791f2) - Tweaks to `icon` parameter in `gr.Button()`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5122](https://github.com/gradio-app/gradio/pull/5122) [`3b805346`](https://github.com/gradio-app/gradio/commit/3b8053469aca6c7a86a6731e641e4400fc34d7d3) - Allows code block in chatbot to scroll horizontally. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! +- [#5312](https://github.com/gradio-app/gradio/pull/5312) [`f769cb67`](https://github.com/gradio-app/gradio/commit/f769cb67149d8e209091508f06d87014acaed965) - only start listening for events after the components are mounted. Thanks [@pngwn](https://github.com/pngwn)! +- [#5254](https://github.com/gradio-app/gradio/pull/5254) [`c39f06e1`](https://github.com/gradio-app/gradio/commit/c39f06e16b9feea97984e4822df35a99c807461c) - Fix `.update()` for `gr.Radio()` and `gr.CheckboxGroup()`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5231](https://github.com/gradio-app/gradio/pull/5231) [`87f1c2b4`](https://github.com/gradio-app/gradio/commit/87f1c2b4ac7c685c43477215fa5b96b6cbeffa05) - Allow `gr.Interface.from_pipeline()` and `gr.load()` to work within `gr.Blocks()`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5238](https://github.com/gradio-app/gradio/pull/5238) [`de23e9f7`](https://github.com/gradio-app/gradio/commit/de23e9f7d67e685e791faf48a21f34121f6d094a) - Improve audio streaming. Thanks [@aliabid94](https://github.com/aliabid94)!/n - Proper audio streaming with WAV files. We now do the proper processing to stream out wav files as a single stream of audio without any cracks in the seams./n - Audio streaming with bytes. Stream any audio type by yielding out bytes, and it should work flawlessly. +- [#5313](https://github.com/gradio-app/gradio/pull/5313) [`54bcb724`](https://github.com/gradio-app/gradio/commit/54bcb72417b2781ad9d7500ea0f89aa9d80f7d8f) - Restores missing part of bottom border on file component. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5235](https://github.com/gradio-app/gradio/pull/5235) [`1ecf88ac`](https://github.com/gradio-app/gradio/commit/1ecf88ac5f20bc5a1c91792d1a68559575e6afd7) - fix #5229. Thanks [@breengles](https://github.com/breengles)! +- [#5276](https://github.com/gradio-app/gradio/pull/5276) [`502f1015`](https://github.com/gradio-app/gradio/commit/502f1015bf23b365bc32446dd2e549b0c5d0dc72) - Ensure `Blocks` translation copy renders correctly. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5296](https://github.com/gradio-app/gradio/pull/5296) [`a0f22626`](https://github.com/gradio-app/gradio/commit/a0f22626f2aff297754414bbc83d5c4cfe086ea0) - `make_waveform()` twitter video resolution fix. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! + +## 3.40.0 + +### Highlights + +#### Client.predict will now return the final output for streaming endpoints ([#5057](https://github.com/gradio-app/gradio/pull/5057) [`35856f8b`](https://github.com/gradio-app/gradio/commit/35856f8b54548cae7bd3b8d6a4de69e1748283b2)) + +### This is a breaking change (for gradio_client only)! + +Previously, `Client.predict` would only return the first output of an endpoint that streamed results. This was causing confusion for developers that wanted to call these streaming demos via the client. + +We realize that developers using the client don't know the internals of whether a demo streams or not, so we're changing the behavior of predict to match developer expectations. + +Using `Client.predict` will now return the final output of a streaming endpoint. This will make it even easier to use gradio apps via the client. + + Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +#### Gradio now supports streaming audio outputs + +Allows users to use generators to stream audio out, yielding consecutive chunks of audio. Requires `streaming=True` to be set on the output audio. + +```python +import gradio as gr +from pydub import AudioSegment + +def stream_audio(audio_file): + audio = AudioSegment.from_mp3(audio_file) + i = 0 + chunk_size = 3000 + + while chunk_size*i < len(audio): + chunk = audio[chunk_size*i:chunk_size*(i+1)] + i += 1 + if chunk: + file = f"/tmp/{i}.mp3" + chunk.export(file, format="mp3") + yield file + +demo = gr.Interface( + fn=stream_audio, + inputs=gr.Audio(type="filepath", label="Audio file to stream"), + outputs=gr.Audio(autoplay=True, streaming=True), +) + +demo.queue().launch() +``` + +From the backend, streamed outputs are served from the `/stream/` endpoint instead of the `/file/` endpoint. Currently just used to serve audio streaming output. The output JSON will have `is_stream`: `true`, instead of `is_file`: `true` in the file data object. Thanks [@aliabid94](https://github.com/aliabid94)! + +### Features + +- [#5081](https://github.com/gradio-app/gradio/pull/5081) [`d7f83823`](https://github.com/gradio-app/gradio/commit/d7f83823fbd7604456b0127d689a63eed759807d) - solve how can I config root_path dynamically? #4968. Thanks [@eastonsuo](https://github.com/eastonsuo)! +- [#5025](https://github.com/gradio-app/gradio/pull/5025) [`6693660a`](https://github.com/gradio-app/gradio/commit/6693660a790996f8f481feaf22a8c49130d52d89) - Add download button to selected images in `Gallery`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5133](https://github.com/gradio-app/gradio/pull/5133) [`61129052`](https://github.com/gradio-app/gradio/commit/61129052ed1391a75c825c891d57fa0ad6c09fc8) - Update dependency esbuild to ^0.19.0. Thanks [@renovate](https://github.com/apps/renovate)! +- [#5125](https://github.com/gradio-app/gradio/pull/5125) [`80be7a1c`](https://github.com/gradio-app/gradio/commit/80be7a1ca44c0adef1668367b2cf36b65e52e576) - chatbot conversation nodes can contain a copy button. Thanks [@fazpu](https://github.com/fazpu)! +- [#5048](https://github.com/gradio-app/gradio/pull/5048) [`0b74a159`](https://github.com/gradio-app/gradio/commit/0b74a1595b30df744e32a2c358c07acb7fd1cfe5) - Use `importlib` in favor of deprecated `pkg_resources`. Thanks [@jayceslesar](https://github.com/jayceslesar)! +- [#5045](https://github.com/gradio-app/gradio/pull/5045) [`3b9494f5`](https://github.com/gradio-app/gradio/commit/3b9494f5c57e6b52e6a040ce8d6b5141f780e84d) - Lite: Fix the analytics module to use asyncio to work in the Wasm env. Thanks [@whitphx](https://github.com/whitphx)! +- [#5046](https://github.com/gradio-app/gradio/pull/5046) [`5244c587`](https://github.com/gradio-app/gradio/commit/5244c5873c355cf3e2f0acb7d67fda3177ef8b0b) - Allow new lines in `HighlightedText` with `/n` and preserve whitespace. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5076](https://github.com/gradio-app/gradio/pull/5076) [`2745075a`](https://github.com/gradio-app/gradio/commit/2745075a26f80e0e16863d483401ff1b6c5ada7a) - Add deploy_discord to docs. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5116](https://github.com/gradio-app/gradio/pull/5116) [`0dc49b4c`](https://github.com/gradio-app/gradio/commit/0dc49b4c517706f572240f285313a881089ced79) - Add support for async functions and async generators to `gr.ChatInterface`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5047](https://github.com/gradio-app/gradio/pull/5047) [`883ac364`](https://github.com/gradio-app/gradio/commit/883ac364f69d92128774ac446ce49bdf8415fd7b) - Add `step` param to `Number`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5137](https://github.com/gradio-app/gradio/pull/5137) [`22aa5eba`](https://github.com/gradio-app/gradio/commit/22aa5eba3fee3f14473e4b0fac29cf72fe31ef04) - Use font size `--text-md` for `` in Chatbot messages. Thanks [@jaywonchung](https://github.com/jaywonchung)! +- [#5005](https://github.com/gradio-app/gradio/pull/5005) [`f5539c76`](https://github.com/gradio-app/gradio/commit/f5539c7618e31451420bd3228754774da14dc65f) - Enhancement: Add focus event to textbox and number component. Thanks [@JodyZ0203](https://github.com/JodyZ0203)! +- [#5104](https://github.com/gradio-app/gradio/pull/5104) [`34f6b22e`](https://github.com/gradio-app/gradio/commit/34f6b22efbfedfa569d452f3f99ed2e6593e3c21) - Strip leading and trailing spaces from username in login route. Thanks [@sweep-ai](https://github.com/apps/sweep-ai)! +- [#5149](https://github.com/gradio-app/gradio/pull/5149) [`144df459`](https://github.com/gradio-app/gradio/commit/144df459a3b7895e524defcfc4c03fbb8b083aca) - Add `show_edit_button` param to `gr.Audio`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5136](https://github.com/gradio-app/gradio/pull/5136) [`eaa1ce14`](https://github.com/gradio-app/gradio/commit/eaa1ce14ac41de1c23321e93f11f1b03a2f3c7f4) - Enhancing Tamil Translation: Language Refinement 🌟. Thanks [@sanjaiyan-dev](https://github.com/sanjaiyan-dev)! +- [#5035](https://github.com/gradio-app/gradio/pull/5035) [`8b4eb8ca`](https://github.com/gradio-app/gradio/commit/8b4eb8cac9ea07bde31b44e2006ca2b7b5f4de36) - JS Client: Fixes cannot read properties of null (reading 'is_file'). Thanks [@raymondtri](https://github.com/raymondtri)! +- [#5023](https://github.com/gradio-app/gradio/pull/5023) [`e6317d77`](https://github.com/gradio-app/gradio/commit/e6317d77f87d3dad638acca3dbc4a9228570e63c) - Update dependency extendable-media-recorder to v8. Thanks [@renovate](https://github.com/apps/renovate)! +- [#5085](https://github.com/gradio-app/gradio/pull/5085) [`13e47835`](https://github.com/gradio-app/gradio/commit/13e478353532c4af18cfa50772f8b6fb3c6c9818) - chore(deps): update dependency extendable-media-recorder to v8. Thanks [@renovate](https://github.com/apps/renovate)! +- [#5080](https://github.com/gradio-app/gradio/pull/5080) [`37caa2e0`](https://github.com/gradio-app/gradio/commit/37caa2e0fe95d6cab8beb174580fb557904f137f) - Add icon and link params to `gr.Button`. Thanks [@hannahblair](https://github.com/hannahblair)! + +### Fixes + +- [#5062](https://github.com/gradio-app/gradio/pull/5062) [`7d897165`](https://github.com/gradio-app/gradio/commit/7d89716519d0751072792c9bbda668ffeb597296) - `gr.Dropdown` now has correct behavior in static mode as well as when an option is selected. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5077](https://github.com/gradio-app/gradio/pull/5077) [`667875b2`](https://github.com/gradio-app/gradio/commit/667875b2441753e74d25bd9d3c8adedd8ede11cd) - Live audio streaming output +- [#5118](https://github.com/gradio-app/gradio/pull/5118) [`1b017e68`](https://github.com/gradio-app/gradio/commit/1b017e68f6a9623cc2ec085bd20e056229552028) - Add `interactive` args to `gr.ColorPicker`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5114](https://github.com/gradio-app/gradio/pull/5114) [`56d2609d`](https://github.com/gradio-app/gradio/commit/56d2609de93387a75dc82b1c06c1240c5b28c0b8) - Reset textbox value to empty string when value is None. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5075](https://github.com/gradio-app/gradio/pull/5075) [`67265a58`](https://github.com/gradio-app/gradio/commit/67265a58027ef1f9e4c0eb849a532f72eaebde48) - Allow supporting >1000 files in `gr.File()` and `gr.UploadButton()`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5135](https://github.com/gradio-app/gradio/pull/5135) [`80727bbe`](https://github.com/gradio-app/gradio/commit/80727bbe2c6d631022054edf01515017691b3bdd) - Fix dataset features and dataset preview for HuggingFaceDatasetSaver. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5039](https://github.com/gradio-app/gradio/pull/5039) [`620e4645`](https://github.com/gradio-app/gradio/commit/620e46452729d6d4877b3fab84a65daf2f2b7bc6) - `gr.Dropdown()` now supports values with arbitrary characters and doesn't clear value when re-focused. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5061](https://github.com/gradio-app/gradio/pull/5061) [`136adc9c`](https://github.com/gradio-app/gradio/commit/136adc9ccb23e5cb4d02d2e88f23f0b850041f98) - Ensure `gradio_client` is backwards compatible with `gradio==3.24.1`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5129](https://github.com/gradio-app/gradio/pull/5129) [`97d804c7`](https://github.com/gradio-app/gradio/commit/97d804c748be9acfe27b8369dd2d64d61f43c2e7) - [Spaces] ZeroGPU Queue fix. Thanks [@cbensimon](https://github.com/cbensimon)! +- [#5140](https://github.com/gradio-app/gradio/pull/5140) [`cd1353fa`](https://github.com/gradio-app/gradio/commit/cd1353fa3eb1b015f5860ca5d5a8e8d1aa4a831c) - Fixes the display of minutes in the video player. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5111](https://github.com/gradio-app/gradio/pull/5111) [`b84a35b7`](https://github.com/gradio-app/gradio/commit/b84a35b7b91eca947f787648ceb361b1d023427b) - Add icon and link to DuplicateButton. Thanks [@aliabd](https://github.com/aliabd)! +- [#5030](https://github.com/gradio-app/gradio/pull/5030) [`f6c491b0`](https://github.com/gradio-app/gradio/commit/f6c491b079d335af633dd854c68eb26f9e61c552) - highlightedtext throws an error basing on model. Thanks [@rajeunoia](https://github.com/rajeunoia)! + +## 3.39.0 + +### Highlights + +#### Create Discord Bots from Gradio Apps 🤖 ([#4960](https://github.com/gradio-app/gradio/pull/4960) [`46e4ef67`](https://github.com/gradio-app/gradio/commit/46e4ef67d287dd68a91473b73172b29cbad064bc)) + +We're excited to announce that Gradio can now automatically create a discord bot from any `gr.ChatInterface` app. + +It's as easy as importing `gradio_client`, connecting to the app, and calling `deploy_discord`! + +_🦙 Turning Llama 2 70b into a discord bot 🦙_ + +```python +import gradio_client as grc +grc.Client("ysharma/Explore_llamav2_with_TGI").deploy_discord(to_id="llama2-70b-discord-bot") +``` + + + +#### Getting started with template spaces + +To help get you started, we have created an organization on Hugging Face called [gradio-discord-bots](https://huggingface.co/gradio-discord-bots) with template spaces you can use to turn state of the art LLMs powered by Gradio to discord bots. + +Currently we have template spaces for: + +- [Llama-2-70b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/Llama-2-70b-chat-hf) powered by a FREE Hugging Face Inference Endpoint! +- [Llama-2-13b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/Llama-2-13b-chat-hf) powered by Hugging Face Inference Endpoints. +- [Llama-2-13b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/llama-2-13b-chat-transformers) powered by Hugging Face transformers. +- [falcon-7b-instruct](https://huggingface.co/spaces/gradio-discord-bots/falcon-7b-instruct) powered by Hugging Face Inference Endpoints. +- [gpt-3.5-turbo](https://huggingface.co/spaces/gradio-discord-bots/gpt-35-turbo), powered by openai. Requires an OpenAI key. + +But once again, you can deploy ANY `gr.ChatInterface` app exposed on the internet! So don't hesitate to try it on your own Chatbots. + +❗️ Additional Note ❗️: Technically, any gradio app that exposes an api route that takes in a single string and outputs a single string can be deployed to discord. But `gr.ChatInterface` apps naturally lend themselves to discord's chat functionality so we suggest you start with those. + +Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Features + +- [#4995](https://github.com/gradio-app/gradio/pull/4995) [`3f8c210b`](https://github.com/gradio-app/gradio/commit/3f8c210b01ef1ceaaf8ee73be4bf246b5b745bbf) - Implement left and right click in `Gallery` component and show implicit images in `Gallery` grid. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#4993](https://github.com/gradio-app/gradio/pull/4993) [`dc07a9f9`](https://github.com/gradio-app/gradio/commit/dc07a9f947de44b419d8384987a02dcf94977851) - Bringing back the "Add download button for audio" PR by [@leuryr](https://github.com/leuryr). Thanks [@abidlabs](https://github.com/abidlabs)! +- [#4979](https://github.com/gradio-app/gradio/pull/4979) [`44ac8ad0`](https://github.com/gradio-app/gradio/commit/44ac8ad08d82ea12c503dde5c78f999eb0452de2) - Allow setting sketch color default. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#4985](https://github.com/gradio-app/gradio/pull/4985) [`b74f8453`](https://github.com/gradio-app/gradio/commit/b74f8453034328f0e42da8e41785f5eb039b45d7) - Adds `additional_inputs` to `gr.ChatInterface`. Thanks [@abidlabs](https://github.com/abidlabs)! + +### Fixes + +- [#4997](https://github.com/gradio-app/gradio/pull/4997) [`41c83070`](https://github.com/gradio-app/gradio/commit/41c83070b01632084e7d29123048a96c1e261407) - Add CSS resets and specifiers to play nice with HF blog. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 3.38 + +### New Features: + +- Provide a parameter `animate` (`False` by default) in `gr.make_waveform()` which animates the overlayed waveform by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4918](https://github.com/gradio-app/gradio/pull/4918) +- Add `show_download_button` param to allow the download button in static Image components to be hidden by [@hannahblair](https://github.com/hannahblair) in [PR 4959](https://github.com/gradio-app/gradio/pull/4959) +- Added autofocus argument to Textbox by [@aliabid94](https://github.com/aliabid94) in [PR 4978](https://github.com/gradio-app/gradio/pull/4978) +- The `gr.ChatInterface` UI now converts the "Submit" button to a "Stop" button in ChatInterface while streaming, which can be used to pause generation. By [@abidlabs](https://github.com/abidlabs) in [PR 4971](https://github.com/gradio-app/gradio/pull/4971). +- Add a `border_color_accent_subdued` theme variable to add a subdued border color to accented items. This is used by chatbot user messages. Set the value of this variable in `Default` theme to `*primary_200`. By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4989](https://github.com/gradio-app/gradio/pull/4989) +- Add default sketch color argument `brush_color`. Also, masks drawn on images are now slightly translucent (and mask color can also be set via brush_color). By [@aliabid94](https://github.com/aliabid94) in [PR 4979](https://github.com/gradio-app/gradio/pull/4979) + +### Bug Fixes: + +- Fixes `cancels` for generators so that if a generator is canceled before it is complete, subsequent runs of the event do not continue from the previous iteration, but rather start from the beginning. By [@abidlabs](https://github.com/abidlabs) in [PR 4969](https://github.com/gradio-app/gradio/pull/4969). +- Use `gr.State` in `gr.ChatInterface` to reduce latency by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4976](https://github.com/gradio-app/gradio/pull/4976) +- Fix bug with `gr.Interface` where component labels inferred from handler parameters were including special args like `gr.Request` or `gr.EventData`. By [@cbensimon](https://github.com/cbensimon) in [PR 4956](https://github.com/gradio-app/gradio/pull/4956) + +### Breaking Changes: + +No changes to highlight. + +### Other Changes: + +- Apply pyright to the `components` directory by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4948](https://github.com/gradio-app/gradio/pull/4948) +- Improved look of ChatInterface by [@aliabid94](https://github.com/aliabid94) in [PR 4978](https://github.com/gradio-app/gradio/pull/4978) + +## 3.37 + +### New Features: + +Introducing a new `gr.ChatInterface` abstraction, which allows Gradio users to build fully functioning Chat interfaces very easily. The only required parameter is a chat function `fn`, which accepts a (string) user input `message` and a (list of lists) chat `history` and returns a (string) response. Here's a toy example: + +```py +import gradio as gr + +def echo(message, history): + return message + +demo = gr.ChatInterface(fn=echo, examples=["hello", "hola", "merhaba"], title="Echo Bot") +demo.launch() +``` + +Which produces: + +image + +And a corresponding easy-to-use API at `/chat`: + +image + +The `gr.ChatInterface` abstraction works nicely with various LLM libraries, such as `langchain`. See the [dedicated guide](https://gradio.app/guides/creating-a-chatbot-fast) for more examples using `gr.ChatInterface`. Collective team effort in [PR 4869](https://github.com/gradio-app/gradio/pull/4869) + +- Chatbot messages now show hyperlinks to download files uploaded to `gr.Chatbot()` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4848](https://github.com/gradio-app/gradio/pull/4848) +- Cached examples now work with generators and async generators by [@abidlabs](https://github.com/abidlabs) in [PR 4927](https://github.com/gradio-app/gradio/pull/4927) +- Add RTL support to `gr.Markdown`, `gr.Chatbot`, `gr.Textbox` (via the `rtl` boolean parameter) and text-alignment to `gr.Textbox`(via the string `text_align` parameter) by [@abidlabs](https://github.com/abidlabs) in [PR 4933](https://github.com/gradio-app/gradio/pull/4933) + +Examples of usage: + +```py +with gr.Blocks() as demo: + gr.Textbox(interactive=True, text_align="right") +demo.launch() +``` + +```py +with gr.Blocks() as demo: + gr.Markdown("سلام", rtl=True) +demo.launch() +``` + +- The `get_api_info` method of `Blocks` now supports layout output components [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4871](https://github.com/gradio-app/gradio/pull/4871) + +- Added the support for the new command `gradio environment`to make it easier for people to file bug reports if we shipped an easy command to list the OS, gradio version, and versions of gradio/gradio-client dependencies. bu [@varshneydevansh](https://github.com/varshneydevansh) in [PR 4915](https://github.com/gradio-app/gradio/pull/4915). + +### Bug Fixes: + +- The `.change()` event is fixed in `Video` and `Image` so that it only fires once by [@abidlabs](https://github.com/abidlabs) in [PR 4793](https://github.com/gradio-app/gradio/pull/4793) +- The `.change()` event is fixed in `Audio` so that fires when the component value is programmatically updated by [@abidlabs](https://github.com/abidlabs) in [PR 4793](https://github.com/gradio-app/gradio/pull/4793) + +* Add missing `display: flex` property to `Row` so that flex styling is applied to children by [@hannahblair] in [PR 4896](https://github.com/gradio-app/gradio/pull/4896) +* Fixed bug where `gr.Video` could not preprocess urls by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4904](https://github.com/gradio-app/gradio/pull/4904) +* Fixed copy button rendering in API page on Safari by [@aliabid94](https://github.com/aliabid94) in [PR 4924](https://github.com/gradio-app/gradio/pull/4924) +* Fixed `gr.Group` and `container=False`. `container` parameter only available for `Textbox`, `Number`, and `Dropdown`, the only elements where it makes sense. By [@aliabid94](https://github.com/aliabid94) in [PR 4916](https://github.com/gradio-app/gradio/pull/4916) +* Fixed broken image link in auto-generated `app.py` from `ThemeClass.push_to_hub` by [@deepkyu](https://github.com/deepkyu) in [PR 4944](https://github.com/gradio-app/gradio/pull/4944) + +### Other Changes: + +- Warning on mobile that if a user leaves the tab, websocket connection may break. On broken connection, tries to rejoin queue and displays error conveying connection broke. By [@aliabid94](https://github.com/aliabid94) in [PR 4742](https://github.com/gradio-app/gradio/pull/4742) +- Remove blocking network calls made before the local URL gets printed - these slow down the display of the local URL, especially when no internet is available. [@aliabid94](https://github.com/aliabid94) in [PR 4905](https://github.com/gradio-app/gradio/pull/4905). +- Pinned dependencies to major versions to reduce the likelihood of a broken `gradio` due to changes in downstream dependencies by [@abidlabs](https://github.com/abidlabs) in [PR 4885](https://github.com/gradio-app/gradio/pull/4885) +- Queue `max_size` defaults to parent Blocks `max_thread` when running on Spaces with ZeroGPU hardware. By [@cbensimon](https://github.com/cbensimon) in [PR 4937](https://github.com/gradio-app/gradio/pull/4937) + +### Breaking Changes: + +Motivated by the release of `pydantic==2.0`, which included breaking changes that broke a large number of Gradio apps, we've pinned many gradio dependencies. Note that pinned dependencies can cause downstream conflicts, so this may be a breaking change. That being said, we've kept the pins pretty loose, and we're expecting change to be better for the long-term stability of Gradio apps. + +## 3.36.1 + +### New Features: + +- Hotfix to support pydantic v1 and v2 by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4835](https://github.com/gradio-app/gradio/pull/4835) + +### Bug Fixes: + +- Fix bug where `gr.File` change event was not triggered when the value was changed by another event by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4811](https://github.com/gradio-app/gradio/pull/4811) + +### Other Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +## 3.36.0 + +### New Features: + +- The `gr.Video`, `gr.Audio`, `gr.Image`, `gr.Chatbot`, and `gr.Gallery` components now include a share icon when deployed on Spaces. This behavior can be modified by setting the `show_share_button` parameter in the component classes. by [@aliabid94](https://github.com/aliabid94) in [PR 4651](https://github.com/gradio-app/gradio/pull/4651) +- Allow the web component `space`, `src`, and `host` attributes to be updated dynamically by [@pngwn](https://github.com/pngwn) in [PR 4461](https://github.com/gradio-app/gradio/pull/4461) +- Suggestion for Spaces Duplication built into Gradio, by [@aliabid94](https://github.com/aliabid94) in [PR 4458](https://github.com/gradio-app/gradio/pull/4458) +- The `api_name` parameter now accepts `False` as a value, which means it does not show up in named or unnamed endpoints. By [@abidlabs](https://github.com/aliabid94) in [PR 4683](https://github.com/gradio-app/gradio/pull/4683) +- Added support for `pathlib.Path` in `gr.Video`, `gr.Gallery`, and `gr.Chatbot` by [sunilkumardash9](https://github.com/sunilkumardash9) in [PR 4581](https://github.com/gradio-app/gradio/pull/4581). + +### Bug Fixes: + +- Updated components with `info` attribute to update when `update()` is called on them. by [@jebarpg](https://github.com/jebarpg) in [PR 4715](https://github.com/gradio-app/gradio/pull/4715). +- Ensure the `Image` components undo button works mode is `mask` or `color-sketch` by [@amyorz](https://github.com/AmyOrz) in [PR 4692](https://github.com/gradio-app/gradio/pull/4692) +- Load the iframe resizer external asset asynchronously, by [@akx](https://github.com/akx) in [PR 4336](https://github.com/gradio-app/gradio/pull/4336) +- Restored missing imports in `gr.components` by [@abidlabs](https://github.com/abidlabs) in [PR 4566](https://github.com/gradio-app/gradio/pull/4566) +- Fix bug where `select` event was not triggered in `gr.Gallery` if `height` was set to be large with `allow_preview=False` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4551](https://github.com/gradio-app/gradio/pull/4551) +- Fix bug where setting `visible=False` in `gr.Group` event did not work by [@abidlabs](https://github.com/abidlabs) in [PR 4567](https://github.com/gradio-app/gradio/pull/4567) +- Fix `make_waveform` to work with paths that contain spaces [@akx](https://github.com/akx) in [PR 4570](https://github.com/gradio-app/gradio/pull/4570) & [PR 4578](https://github.com/gradio-app/gradio/pull/4578) +- Send captured data in `stop_recording` event for `gr.Audio` and `gr.Video` components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4554](https://github.com/gradio-app/gradio/pull/4554) +- Fix bug in `gr.Gallery` where `height` and `object_fit` parameters where being ignored by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4576](https://github.com/gradio-app/gradio/pull/4576) +- Fixes an HTML sanitization issue in DOMPurify where links in markdown were not opening in a new window by [@hannahblair] in [PR 4577](https://github.com/gradio-app/gradio/pull/4577) +- Fixed Dropdown height rendering in Columns by [@aliabid94](https://github.com/aliabid94) in [PR 4584](https://github.com/gradio-app/gradio/pull/4584) +- Fixed bug where `AnnotatedImage` css styling was causing the annotation masks to not be displayed correctly by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4628](https://github.com/gradio-app/gradio/pull/4628) +- Ensure that Gradio does not silently fail when running on a port that is occupied by [@abidlabs](https://github.com/abidlabs) in [PR 4624](https://github.com/gradio-app/gradio/pull/4624). +- Fix double upload bug that caused lag in file uploads by [@aliabid94](https://github.com/aliabid94) in [PR 4661](https://github.com/gradio-app/gradio/pull/4661) +- `Progress` component now appears even when no `iterable` is specified in `tqdm` constructor by [@itrushkin](https://github.com/itrushkin) in [PR 4475](https://github.com/gradio-app/gradio/pull/4475) +- Deprecation warnings now point at the user code using those deprecated features, instead of Gradio internals, by (https://github.com/akx) in [PR 4694](https://github.com/gradio-app/gradio/pull/4694) +- Adapt column widths in gr.Examples based on content by [@pngwn](https://github.com/pngwn) & [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4700](https://github.com/gradio-app/gradio/pull/4700) +- The `plot` parameter deprecation warnings should now only be emitted for `Image` components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4709](https://github.com/gradio-app/gradio/pull/4709) +- Removed uncessessary `type` deprecation warning by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4709](https://github.com/gradio-app/gradio/pull/4709) +- Ensure Audio autoplays works when `autoplay=True` and the video source is dynamically updated [@pngwn](https://github.com/pngwn) in [PR 4705](https://github.com/gradio-app/gradio/pull/4705) +- When an error modal is shown in spaces, ensure we scroll to the top so it can be seen by [@pngwn](https://github.com/pngwn) in [PR 4712](https://github.com/gradio-app/gradio/pull/4712) +- Update depedencies by [@pngwn](https://github.com/pngwn) in [PR 4675](https://github.com/gradio-app/gradio/pull/4675) +- Fixes `gr.Dropdown` being cutoff at the bottom by [@abidlabs](https://github.com/abidlabs) in [PR 4691](https://github.com/gradio-app/gradio/pull/4691). +- Scroll top when clicking "View API" in spaces by [@pngwn](https://github.com/pngwn) in [PR 4714](https://github.com/gradio-app/gradio/pull/4714) +- Fix bug where `show_label` was hiding the entire component for `gr.Label` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4713](https://github.com/gradio-app/gradio/pull/4713) +- Don't crash when uploaded image has broken EXIF data, by [@akx](https://github.com/akx) in [PR 4764](https://github.com/gradio-app/gradio/pull/4764) +- Place toast messages at the top of the screen by [@pngwn](https://github.com/pngwn) in [PR 4796](https://github.com/gradio-app/gradio/pull/4796) +- Fix regressed styling of Login page when auth is enabled by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4797](https://github.com/gradio-app/gradio/pull/4797) +- Prevent broken scrolling to output on Spaces by [@aliabid94](https://github.com/aliabid94) in [PR 4822](https://github.com/gradio-app/gradio/pull/4822) + +### Other Changes: + +- Add `.git-blame-ignore-revs` by [@akx](https://github.com/akx) in [PR 4586](https://github.com/gradio-app/gradio/pull/4586) +- Update frontend dependencies in [PR 4601](https://github.com/gradio-app/gradio/pull/4601) +- Use `typing.Literal` where possible in gradio library and client by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4608](https://github.com/gradio-app/gradio/pull/4608) +- Remove unnecessary mock json files for frontend E2E tests by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4625](https://github.com/gradio-app/gradio/pull/4625) +- Update dependencies by [@pngwn](https://github.com/pngwn) in [PR 4643](https://github.com/gradio-app/gradio/pull/4643) +- The theme builder now launches successfully, and the API docs are cleaned up. By [@abidlabs](https://github.com/aliabid94) in [PR 4683](https://github.com/gradio-app/gradio/pull/4683) +- Remove `cleared_value` from some components as its no longer used internally by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4685](https://github.com/gradio-app/gradio/pull/4685) +- Better errors when you define two Blocks and reference components in one Blocks from the events in the other Blocks [@abidlabs](https://github.com/abidlabs) in [PR 4738](https://github.com/gradio-app/gradio/pull/4738). +- Better message when share link is not created by [@abidlabs](https://github.com/abidlabs) in [PR 4773](https://github.com/gradio-app/gradio/pull/4773). +- Improve accessibility around selected images in gr.Gallery component by [@hannahblair](https://github.com/hannahblair) in [PR 4790](https://github.com/gradio-app/gradio/pull/4790) + +### Breaking Changes: + +[PR 4683](https://github.com/gradio-app/gradio/pull/4683) removes the explict named endpoint "load_examples" from gr.Interface that was introduced in [PR 4456](https://github.com/gradio-app/gradio/pull/4456). + +## 3.35.2 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fix chatbot streaming by [@aliabid94](https://github.com/aliabid94) in [PR 4537](https://github.com/gradio-app/gradio/pull/4537) +- Fix chatbot height and scrolling by [@aliabid94](https://github.com/aliabid94) in [PR 4540](https://github.com/gradio-app/gradio/pull/4540) + +### Other Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +## 3.35.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fix chatbot streaming by [@aliabid94](https://github.com/aliabid94) in [PR 4537](https://github.com/gradio-app/gradio/pull/4537) +- Fix error modal position and text size by [@pngwn](https://github.com/pngwn) in [PR 4538](https://github.com/gradio-app/gradio/pull/4538). + +### Other Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +## 3.35.0 + +### New Features: + +- A `gr.ClearButton` which allows users to easily clear the values of components by [@abidlabs](https://github.com/abidlabs) in [PR 4456](https://github.com/gradio-app/gradio/pull/4456) + +Example usage: + +```py +import gradio as gr + +with gr.Blocks() as demo: + chatbot = gr.Chatbot([("Hello", "How are you?")]) + with gr.Row(): + textbox = gr.Textbox(scale=3, interactive=True) + gr.ClearButton([textbox, chatbot], scale=1) + +demo.launch() +``` + +- Min and max value for gr.Number by [@artegoser](https://github.com/artegoser) and [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3991](https://github.com/gradio-app/gradio/pull/3991) +- Add `start_recording` and `stop_recording` events to `Video` and `Audio` components by [@pngwn](https://github.com/pngwn) in [PR 4422](https://github.com/gradio-app/gradio/pull/4422) +- Allow any function to generate an error message and allow multiple messages to appear at a time. Other error modal improvements such as auto dismiss after a time limit and a new layout on mobile [@pngwn](https://github.com/pngwn) in [PR 4459](https://github.com/gradio-app/gradio/pull/4459). +- Add `autoplay` kwarg to `Video` and `Audio` components by [@pngwn](https://github.com/pngwn) in [PR 4453](https://github.com/gradio-app/gradio/pull/4453) +- Add `allow_preview` parameter to `Gallery` to control whether a detailed preview is displayed on click by + [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4470](https://github.com/gradio-app/gradio/pull/4470) +- Add `latex_delimiters` parameter to `Chatbot` to control the delimiters used for LaTeX and to disable LaTeX in the `Chatbot` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4516](https://github.com/gradio-app/gradio/pull/4516) +- Can now issue `gr.Warning` and `gr.Info` modals. Simply put the code `gr.Warning("Your warning message")` or `gr.Info("Your info message")` as a standalone line in your function. By [@aliabid94](https://github.com/aliabid94) in [PR 4518](https://github.com/gradio-app/gradio/pull/4518). + +Example: + +```python +def start_process(name): + gr.Info("Starting process") + if name is None: + gr.Warning("Name is empty") + ... + if success == False: + raise gr.Error("Process failed") +``` + +### Bug Fixes: + +- Add support for PAUSED state in the JS client by [@abidlabs](https://github.com/abidlabs) in [PR 4438](https://github.com/gradio-app/gradio/pull/4438) +- Ensure Tabs only occupy the space required by [@pngwn](https://github.com/pngwn) in [PR 4419](https://github.com/gradio-app/gradio/pull/4419) +- Ensure components have the correct empty sizes to prevent empty containers from collapsing by [@pngwn](https://github.com/pngwn) in [PR 4447](https://github.com/gradio-app/gradio/pull/4447). +- Frontend code no longer crashes when there is a relative URL in an `` element, by [@akx](https://github.com/akx) in [PR 4449](https://github.com/gradio-app/gradio/pull/4449). +- Fix bug where setting `format='mp4'` on a video component would cause the function to error out if the uploaded video was not playable by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4467](https://github.com/gradio-app/gradio/pull/4467) +- Fix `_js` parameter to work even without backend function, by [@aliabid94](https://github.com/aliabid94) in [PR 4486](https://github.com/gradio-app/gradio/pull/4486). +- Fix new line issue with `gr.Chatbot()` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4491](https://github.com/gradio-app/gradio/pull/4491) +- Fixes issue with Clear button not working for `Label` component by [@abidlabs](https://github.com/abidlabs) in [PR 4456](https://github.com/gradio-app/gradio/pull/4456) +- Restores the ability to pass in a tuple (sample rate, audio array) to gr.Audio() by [@abidlabs](https://github.com/abidlabs) in [PR 4525](https://github.com/gradio-app/gradio/pull/4525) +- Ensure code is correctly formatted and copy button is always present in Chatbot by [@pngwn](https://github.com/pngwn) in [PR 4527](https://github.com/gradio-app/gradio/pull/4527) +- `show_label` will not automatically be set to `True` in `gr.BarPlot.update` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4531](https://github.com/gradio-app/gradio/pull/4531) +- `gr.BarPlot` group text now respects darkmode by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4531](https://github.com/gradio-app/gradio/pull/4531) +- Fix dispatched errors from within components [@aliabid94](https://github.com/aliabid94) in [PR 4786](https://github.com/gradio-app/gradio/pull/4786) + +### Other Changes: + +- Change styling of status and toast error components by [@hannahblair](https://github.com/hannahblair) in [PR 4454](https://github.com/gradio-app/gradio/pull/4454). +- Clean up unnecessary `new Promise()`s by [@akx](https://github.com/akx) in [PR 4442](https://github.com/gradio-app/gradio/pull/4442). +- Minor UI cleanup for Examples and Dataframe components [@aliabid94](https://github.com/aliabid94) in [PR 4455](https://github.com/gradio-app/gradio/pull/4455). +- Minor UI cleanup for Examples and Dataframe components [@aliabid94](https://github.com/aliabid94) in [PR 4455](https://github.com/gradio-app/gradio/pull/4455). +- Add Catalan translation [@jordimas](https://github.com/jordimas) in [PR 4483](https://github.com/gradio-app/gradio/pull/4483). +- The API endpoint that loads examples upon click has been given an explicit name ("/load_examples") by [@abidlabs](https://github.com/abidlabs) in [PR 4456](https://github.com/gradio-app/gradio/pull/4456). +- Allows configuration of FastAPI app when calling `mount_gradio_app`, by [@charlesfrye](https://github.com/charlesfrye) in [PR4519](https://github.com/gradio-app/gradio/pull/4519). + +### Breaking Changes: + +- The behavior of the `Clear` button has been changed for `Slider`, `CheckboxGroup`, `Radio`, `Dropdown` components by [@abidlabs](https://github.com/abidlabs) in [PR 4456](https://github.com/gradio-app/gradio/pull/4456). The Clear button now sets the value of these components to be empty as opposed to the original default set by the developer. This is to make them in line with the rest of the Gradio components. +- Python 3.7 end of life is June 27 2023. Gradio will no longer support python 3.7 by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4484](https://github.com/gradio-app/gradio/pull/4484) +- Removed `$` as a default LaTeX delimiter for the `Chatbot` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4516](https://github.com/gradio-app/gradio/pull/4516). The specific LaTeX delimeters can be set using the new `latex_delimiters` parameter in `Chatbot`. + +## 3.34.0 + +### New Features: + +- The `gr.UploadButton` component now supports the `variant` and `interactive` parameters by [@abidlabs](https://github.com/abidlabs) in [PR 4436](https://github.com/gradio-app/gradio/pull/4436). + +### Bug Fixes: + +- Remove target="\_blank" override on anchor tags with internal targets by [@hannahblair](https://github.com/hannahblair) in [PR 4405](https://github.com/gradio-app/gradio/pull/4405) +- Fixed bug where `gr.File(file_count='multiple')` could not be cached as output by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4421](https://github.com/gradio-app/gradio/pull/4421) +- Restricts the domains that can be proxied via `/proxy` route by [@abidlabs](https://github.com/abidlabs) in [PR 4406](https://github.com/gradio-app/gradio/pull/4406). +- Fixes issue where `gr.UploadButton` could not be used to upload the same file twice by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4437](https://github.com/gradio-app/gradio/pull/4437) +- Fixes bug where `/proxy` route was being incorrectly constructed by the frontend by [@abidlabs](https://github.com/abidlabs) in [PR 4430](https://github.com/gradio-app/gradio/pull/4430). +- Fix z-index of status component by [@hannahblair](https://github.com/hannahblair) in [PR 4429](https://github.com/gradio-app/gradio/pull/4429) +- Fix video rendering in Safari by [@aliabid94](https://github.com/aliabid94) in [PR 4433](https://github.com/gradio-app/gradio/pull/4433). +- The output directory for files downloaded when calling Blocks as a function is now set to a temporary directory by default (instead of the working directory in some cases) by [@abidlabs](https://github.com/abidlabs) in [PR 4501](https://github.com/gradio-app/gradio/pull/4501) + +### Other Changes: + +- When running on Spaces, handler functions will be transformed by the [PySpaces](https://pypi.org/project/spaces/) library in order to make them work with specific hardware. It will have no effect on standalone Gradio apps or regular Gradio Spaces and can be globally deactivated as follows : `import spaces; spaces.disable_gradio_auto_wrap()` by [@cbensimon](https://github.com/cbensimon) in [PR 4389](https://github.com/gradio-app/gradio/pull/4389). +- Deprecated `.style` parameter and moved arguments to constructor. Added support for `.update()` to all arguments initially in style. Added `scale` and `min_width` support to every Component. By [@aliabid94](https://github.com/aliabid94) in [PR 4374](https://github.com/gradio-app/gradio/pull/4374) + +### Breaking Changes: + +No changes to highlight. + +## 3.33.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Allow `every` to work with generators by [@dkjshk](https://github.com/dkjshk) in [PR 4434](https://github.com/gradio-app/gradio/pull/4434) +- Fix z-index of status component by [@hannahblair](https://github.com/hannahblair) in [PR 4429](https://github.com/gradio-app/gradio/pull/4429) +- Allow gradio to work offline, by [@aliabid94](https://github.com/aliabid94) in [PR 4398](https://github.com/gradio-app/gradio/pull/4398). +- Fixed `validate_url` to check for 403 errors and use a GET request in place of a HEAD by [@alvindaiyan](https://github.com/alvindaiyan) in [PR 4388](https://github.com/gradio-app/gradio/pull/4388). + +### Other Changes: + +- More explicit error message when share link binary is blocked by antivirus by [@abidlabs](https://github.com/abidlabs) in [PR 4380](https://github.com/gradio-app/gradio/pull/4380). + +### Breaking Changes: + +No changes to highlight. + +## 3.33.0 + +### New Features: + +- Introduced `gradio deploy` to launch a Gradio app to Spaces directly from your terminal. By [@aliabid94](https://github.com/aliabid94) in [PR 4033](https://github.com/gradio-app/gradio/pull/4033). +- Introduce `show_progress='corner'` argument to event listeners, which will not cover the output components with the progress animation, but instead show it in the corner of the components. By [@aliabid94](https://github.com/aliabid94) in [PR 4396](https://github.com/gradio-app/gradio/pull/4396). + +### Bug Fixes: + +- Fix bug where Label change event was triggering itself by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4371](https://github.com/gradio-app/gradio/pull/4371) +- Make `Blocks.load` behave like other event listeners (allows chaining `then` off of it) [@anentropic](https://github.com/anentropic/) in [PR 4304](https://github.com/gradio-app/gradio/pull/4304) +- Respect `interactive=True` in output components of a `gr.Interface` by [@abidlabs](https://github.com/abidlabs) in [PR 4356](https://github.com/gradio-app/gradio/pull/4356). +- Remove unused frontend code by [@akx](https://github.com/akx) in [PR 4275](https://github.com/gradio-app/gradio/pull/4275) +- Fixes favicon path on Windows by [@abidlabs](https://github.com/abidlabs) in [PR 4369](https://github.com/gradio-app/gradio/pull/4369). +- Prevent path traversal in `/file` routes by [@abidlabs](https://github.com/abidlabs) in [PR 4370](https://github.com/gradio-app/gradio/pull/4370). +- Do not send HF token to other domains via `/proxy` route by [@abidlabs](https://github.com/abidlabs) in [PR 4368](https://github.com/gradio-app/gradio/pull/4368). +- Replace default `markedjs` sanitize function with DOMPurify sanitizer for `gr.Chatbot()` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4360](https://github.com/gradio-app/gradio/pull/4360) +- Prevent the creation of duplicate copy buttons in the chatbot and ensure copy buttons work in non-secure contexts by [@binary-husky](https://github.com/binary-husky) in [PR 4350](https://github.com/gradio-app/gradio/pull/4350). + +### Other Changes: + +- Remove flicker of loading bar by adding opacity transition, by [@aliabid94](https://github.com/aliabid94) in [PR 4349](https://github.com/gradio-app/gradio/pull/4349). +- Performance optimization in the frontend's Blocks code by [@akx](https://github.com/akx) in [PR 4334](https://github.com/gradio-app/gradio/pull/4334) +- Upgrade the pnpm lock file format version from v6.0 to v6.1 by [@whitphx](https://github.com/whitphx) in [PR 4393](https://github.com/gradio-app/gradio/pull/4393) + +### Breaking Changes: + +- The `/file=` route no longer allows accessing dotfiles or files in "dot directories" by [@akx](https://github.com/akx) in [PR 4303](https://github.com/gradio-app/gradio/pull/4303) + +## 3.32.0 + +### New Features: + +- `Interface.launch()` and `Blocks.launch()` now accept an `app_kwargs` argument to allow customizing the configuration of the underlying FastAPI app, by [@akx](https://github.com/akx) in [PR 4282](https://github.com/gradio-app/gradio/pull/4282) + +### Bug Fixes: + +- Fixed Gallery/AnnotatedImage components not respecting GRADIO_DEFAULT_DIR variable by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4256](https://github.com/gradio-app/gradio/pull/4256) +- Fixed Gallery/AnnotatedImage components resaving identical images by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4256](https://github.com/gradio-app/gradio/pull/4256) +- Fixed Audio/Video/File components creating empty tempfiles on each run by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4256](https://github.com/gradio-app/gradio/pull/4256) +- Fixed the behavior of the `run_on_click` parameter in `gr.Examples` by [@abidlabs](https://github.com/abidlabs) in [PR 4258](https://github.com/gradio-app/gradio/pull/4258). +- Ensure error modal displays when the queue is enabled by [@pngwn](https://github.com/pngwn) in [PR 4273](https://github.com/gradio-app/gradio/pull/4273) +- Ensure js client respcts the full root when making requests to the server by [@pngwn](https://github.com/pngwn) in [PR 4271](https://github.com/gradio-app/gradio/pull/4271) + +### Other Changes: + +- Refactor web component `initial_height` attribute by [@whitphx](https://github.com/whitphx) in [PR 4223](https://github.com/gradio-app/gradio/pull/4223) +- Relocate `mount_css` fn to remove circular dependency [@whitphx](https://github.com/whitphx) in [PR 4222](https://github.com/gradio-app/gradio/pull/4222) +- Upgrade Black to 23.3 by [@akx](https://github.com/akx) in [PR 4259](https://github.com/gradio-app/gradio/pull/4259) +- Add frontend LaTeX support in `gr.Chatbot()` using `KaTeX` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4285](https://github.com/gradio-app/gradio/pull/4285). + +### Breaking Changes: + +No changes to highlight. + +## 3.31.0 + +### New Features: + +- The reloader command (`gradio app.py`) can now accept command line arguments by [@micky2be](https://github.com/micky2be) in [PR 4119](https://github.com/gradio-app/gradio/pull/4119) +- Added `format` argument to `Audio` component by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4178](https://github.com/gradio-app/gradio/pull/4178) +- Add JS client code snippets to use via api page by [@aliabd](https://github.com/aliabd) in [PR 3927](https://github.com/gradio-app/gradio/pull/3927). +- Update to the JS client by [@pngwn](https://github.com/pngwn) in [PR 4202](https://github.com/gradio-app/gradio/pull/4202) + +### Bug Fixes: + +- Fix "TypeError: issubclass() arg 1 must be a class" When use Optional[Types] by [@lingfengchencn](https://github.com/lingfengchencn) in [PR 4200](https://github.com/gradio-app/gradio/pull/4200). +- Gradio will no longer send any analytics or call home if analytics are disabled with the GRADIO_ANALYTICS_ENABLED environment variable. By [@akx](https://github.com/akx) in [PR 4194](https://github.com/gradio-app/gradio/pull/4194) and [PR 4236](https://github.com/gradio-app/gradio/pull/4236) +- The deprecation warnings for kwargs now show the actual stack level for the invocation, by [@akx](https://github.com/akx) in [PR 4203](https://github.com/gradio-app/gradio/pull/4203). +- Fix "TypeError: issubclass() arg 1 must be a class" When use Optional[Types] by [@lingfengchencn](https://github.com/lingfengchencn) in [PR 4200](https://github.com/gradio-app/gradio/pull/4200). +- Ensure cancelling functions work correctly by [@pngwn](https://github.com/pngwn) in [PR 4225](https://github.com/gradio-app/gradio/pull/4225) +- Fixes a bug with typing.get_type_hints() on Python 3.9 by [@abidlabs](https://github.com/abidlabs) in [PR 4228](https://github.com/gradio-app/gradio/pull/4228). +- Fixes JSONDecodeError by [@davidai](https://github.com/davidai) in [PR 4241](https://github.com/gradio-app/gradio/pull/4241) +- Fix `chatbot_dialogpt` demo by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4238](https://github.com/gradio-app/gradio/pull/4238). + +### Other Changes: + +- Change `gr.Chatbot()` markdown parsing to frontend using `marked` library and `prism` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4150](https://github.com/gradio-app/gradio/pull/4150) +- Update the js client by [@pngwn](https://github.com/pngwn) in [PR 3899](https://github.com/gradio-app/gradio/pull/3899) +- Fix documentation for the shape of the numpy array produced by the `Image` component by [@der3318](https://github.com/der3318) in [PR 4204](https://github.com/gradio-app/gradio/pull/4204). +- Updates the timeout for websocket messaging from 1 second to 5 seconds by [@abidlabs](https://github.com/abidlabs) in [PR 4235](https://github.com/gradio-app/gradio/pull/4235) + +### Breaking Changes: + +No changes to highlight. + +## 3.30.0 + +### New Features: + +- Adds a `root_path` parameter to `launch()` that allows running Gradio applications on subpaths (e.g. www.example.com/app) behind a proxy, by [@abidlabs](https://github.com/abidlabs) in [PR 4133](https://github.com/gradio-app/gradio/pull/4133) +- Fix dropdown change listener to trigger on change when updated as an output by [@aliabid94](https://github.com/aliabid94) in [PR 4128](https://github.com/gradio-app/gradio/pull/4128). +- Add `.input` event listener, which is only triggered when a user changes the component value (as compared to `.change`, which is also triggered when a component updates as the result of a function trigger), by [@aliabid94](https://github.com/aliabid94) in [PR 4157](https://github.com/gradio-app/gradio/pull/4157). + +### Bug Fixes: + +- Records username when flagging by [@abidlabs](https://github.com/abidlabs) in [PR 4135](https://github.com/gradio-app/gradio/pull/4135) +- Fix website build issue by [@aliabd](https://github.com/aliabd) in [PR 4142](https://github.com/gradio-app/gradio/pull/4142) +- Fix lang agnostic type info for `gr.File(file_count='multiple')` output components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4153](https://github.com/gradio-app/gradio/pull/4153) + +### Other Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +## 3.29.0 + +### New Features: + +- Returning language agnostic types in the `/info` route by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4039](https://github.com/gradio-app/gradio/pull/4039) + +### Bug Fixes: + +- Allow users to upload audio files in Audio component on iOS by by [@aliabid94](https://github.com/aliabid94) in [PR 4071](https://github.com/gradio-app/gradio/pull/4071). +- Fixes the gradio theme builder error that appeared on launch by [@aliabid94](https://github.com/aliabid94) and [@abidlabs](https://github.com/abidlabs) in [PR 4080](https://github.com/gradio-app/gradio/pull/4080) +- Keep Accordion content in DOM by [@aliabid94](https://github.com/aliabid94) in [PR 4070](https://github.com/gradio-app/gradio/pull/4073) +- Fixed bug where type hints in functions caused the event handler to crash by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4068](https://github.com/gradio-app/gradio/pull/4068) +- Fix dropdown default value not appearing by [@aliabid94](https://github.com/aliabid94) in [PR 4072](https://github.com/gradio-app/gradio/pull/4072). +- Soft theme label color fix by [@aliabid94](https://github.com/aliabid94) in [PR 4070](https://github.com/gradio-app/gradio/pull/4070) +- Fix `gr.Slider` `release` event not triggering on mobile by [@space-nuko](https://github.com/space-nuko) in [PR 4098](https://github.com/gradio-app/gradio/pull/4098) +- Removes extraneous `State` component info from the `/info` route by [@abidlabs](https://github.com/freddyaboulton) in [PR 4107](https://github.com/gradio-app/gradio/pull/4107) +- Make .then() work even if first event fails by [@aliabid94](https://github.com/aliabid94) in [PR 4115](https://github.com/gradio-app/gradio/pull/4115). + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Allow users to submit with enter in Interfaces with textbox / number inputs [@aliabid94](https://github.com/aliabid94) in [PR 4090](https://github.com/gradio-app/gradio/pull/4090). +- Updates gradio's requirements.txt to requires uvicorn>=0.14.0 by [@abidlabs](https://github.com/abidlabs) in [PR 4086](https://github.com/gradio-app/gradio/pull/4086) +- Updates some error messaging by [@abidlabs](https://github.com/abidlabs) in [PR 4086](https://github.com/gradio-app/gradio/pull/4086) +- Renames simplified Chinese translation file from `zh-cn.json` to `zh-CN.json` by [@abidlabs](https://github.com/abidlabs) in [PR 4086](https://github.com/gradio-app/gradio/pull/4086) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.28.3 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fixes issue with indentation in `gr.Code()` component with streaming by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4043](https://github.com/gradio-app/gradio/pull/4043) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.28.2 + +### Bug Fixes + +- Code component visual updates by [@pngwn](https://github.com/pngwn) in [PR 4051](https://github.com/gradio-app/gradio/pull/4051) + +### New Features: + +- Add support for `visual-question-answering`, `document-question-answering`, and `image-to-text` using `gr.Interface.load("models/...")` and `gr.Interface.from_pipeline` by [@osanseviero](https://github.com/osanseviero) in [PR 3887](https://github.com/gradio-app/gradio/pull/3887) +- Add code block support in `gr.Chatbot()`, by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4048](https://github.com/gradio-app/gradio/pull/4048) +- Adds the ability to blocklist filepaths (and also improves the allowlist mechanism) by [@abidlabs](https://github.com/abidlabs) in [PR 4047](https://github.com/gradio-app/gradio/pull/4047). +- Adds the ability to specify the upload directory via an environment variable by [@abidlabs](https://github.com/abidlabs) in [PR 4047](https://github.com/gradio-app/gradio/pull/4047). + +### Bug Fixes: + +- Fixes issue with `matplotlib` not rendering correctly if the backend was not set to `Agg` by [@abidlabs](https://github.com/abidlabs) in [PR 4029](https://github.com/gradio-app/gradio/pull/4029) +- Fixes bug where rendering the same `gr.State` across different Interfaces/Blocks within larger Blocks would not work by [@abidlabs](https://github.com/abidlabs) in [PR 4030](https://github.com/gradio-app/gradio/pull/4030) +- Code component visual updates by [@pngwn](https://github.com/pngwn) in [PR 4051](https://github.com/gradio-app/gradio/pull/4051) + +### Documentation Changes: + +- Adds a Guide on how to use the Python Client within a FastAPI app, by [@abidlabs](https://github.com/abidlabs) in [PR 3892](https://github.com/gradio-app/gradio/pull/3892) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +- `gr.HuggingFaceDatasetSaver` behavior changed internally. The `flagging/` folder is not a `.git/` folder anymore when using it. `organization` parameter is now ignored in favor of passing a full dataset id as `dataset_name` (e.g. `"username/my-dataset"`). +- New lines (`\n`) are not automatically converted to `
` in `gr.Markdown()` or `gr.Chatbot()`. For multiple new lines, a developer must add multiple `
` tags. + +### Full Changelog: + +- Safer version of `gr.HuggingFaceDatasetSaver` using HTTP methods instead of git pull/push by [@Wauplin](https://github.com/Wauplin) in [PR 3973](https://github.com/gradio-app/gradio/pull/3973) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.28.1 + +### New Features: + +- Add a "clear mask" button to `gr.Image` sketch modes, by [@space-nuko](https://github.com/space-nuko) in [PR 3615](https://github.com/gradio-app/gradio/pull/3615) + +### Bug Fixes: + +- Fix dropdown default value not appearing by [@aliabid94](https://github.com/aliabid94) in [PR 3996](https://github.com/gradio-app/gradio/pull/3996). +- Fix faded coloring of output textboxes in iOS / Safari by [@aliabid94](https://github.com/aliabid94) in [PR 3993](https://github.com/gradio-app/gradio/pull/3993) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +- CI: Simplified Python CI workflow by [@akx](https://github.com/akx) in [PR 3982](https://github.com/gradio-app/gradio/pull/3982) +- Upgrade pyright to 1.1.305 by [@akx](https://github.com/akx) in [PR 4042](https://github.com/gradio-app/gradio/pull/4042) +- More Ruff rules are enabled and lint errors fixed by [@akx](https://github.com/akx) in [PR 4038](https://github.com/gradio-app/gradio/pull/4038) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.28.0 + +### Bug Fixes: + +- Fix duplicate play commands in full-screen mode of 'video'. by [@tomchang25](https://github.com/tomchang25) in [PR 3968](https://github.com/gradio-app/gradio/pull/3968). +- Fix the issue of the UI stuck caused by the 'selected' of DataFrame not being reset. by [@tomchang25](https://github.com/tomchang25) in [PR 3916](https://github.com/gradio-app/gradio/pull/3916). +- Fix issue where `gr.Video()` would not work inside a `gr.Tab()` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3891](https://github.com/gradio-app/gradio/pull/3891) +- Fixed issue with old_value check in File. by [@tomchang25](https://github.com/tomchang25) in [PR 3859](https://github.com/gradio-app/gradio/pull/3859). +- Fixed bug where all bokeh plots appeared in the same div by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3896](https://github.com/gradio-app/gradio/pull/3896) +- Fixed image outputs to automatically take full output image height, unless explicitly set, by [@aliabid94](https://github.com/aliabid94) in [PR 3905](https://github.com/gradio-app/gradio/pull/3905) +- Fix issue in `gr.Gallery()` where setting height causes aspect ratio of images to collapse by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3830](https://github.com/gradio-app/gradio/pull/3830) +- Fix issue where requesting for a non-existing file would trigger a 500 error by [@micky2be](https://github.com/micky2be) in `[PR 3895](https://github.com/gradio-app/gradio/pull/3895)`. +- Fix bugs with abspath about symlinks, and unresolvable path on Windows by [@micky2be](https://github.com/micky2be) in `[PR 3895](https://github.com/gradio-app/gradio/pull/3895)`. +- Fixes type in client `Status` enum by [@10zinten](https://github.com/10zinten) in [PR 3931](https://github.com/gradio-app/gradio/pull/3931) +- Fix `gr.ChatBot` to handle image url [tye-singwa](https://github.com/tye-signwa) in [PR 3953](https://github.com/gradio-app/gradio/pull/3953) +- Move Google Tag Manager related initialization code to analytics-enabled block by [@akx](https://github.com/akx) in [PR 3956](https://github.com/gradio-app/gradio/pull/3956) +- Fix bug where port was not reused if the demo was closed and then re-launched by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3896](https://github.com/gradio-app/gradio/pull/3959) +- Fixes issue where dropdown does not position itself at selected element when opened [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3639](https://github.com/gradio-app/gradio/pull/3639) + +### Documentation Changes: + +- Make use of `gr` consistent across the docs by [@duerrsimon](https://github.com/duerrsimon) in [PR 3901](https://github.com/gradio-app/gradio/pull/3901) +- Fixed typo in theming-guide.md by [@eltociear](https://github.com/eltociear) in [PR 3952](https://github.com/gradio-app/gradio/pull/3952) + +### Testing and Infrastructure Changes: + +- CI: Python backend lint is only run once, by [@akx](https://github.com/akx) in [PR 3960](https://github.com/gradio-app/gradio/pull/3960) +- Format invocations and concatenations were replaced by f-strings where possible by [@akx](https://github.com/akx) in [PR 3984](https://github.com/gradio-app/gradio/pull/3984) +- Linting rules were made more strict and issues fixed by [@akx](https://github.com/akx) in [PR 3979](https://github.com/gradio-app/gradio/pull/3979). + +### Breaking Changes: + +- Some re-exports in `gradio.themes` utilities (introduced in 3.24.0) have been eradicated. + By [@akx](https://github.com/akx) in [PR 3958](https://github.com/gradio-app/gradio/pull/3958) + +### Full Changelog: + +- Add DESCRIPTION.md to image_segmentation demo by [@aliabd](https://github.com/aliabd) in [PR 3866](https://github.com/gradio-app/gradio/pull/3866) +- Fix error in running `gr.themes.builder()` by [@deepkyu](https://github.com/deepkyu) in [PR 3869](https://github.com/gradio-app/gradio/pull/3869) +- Fixed a JavaScript TypeError when loading custom JS with `_js` and setting `outputs` to `None` in `gradio.Blocks()` by [@DavG25](https://github.com/DavG25) in [PR 3883](https://github.com/gradio-app/gradio/pull/3883) +- Fixed bg_background_fill theme property to expand to whole background, block_radius to affect form elements as well, and added block_label_shadow theme property by [@aliabid94](https://github.com/aliabid94) in [PR 3590](https://github.com/gradio-app/gradio/pull/3590) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.27.0 + +### New Features: + +###### AnnotatedImage Component + +New AnnotatedImage component allows users to highlight regions of an image, either by providing bounding boxes, or 0-1 pixel masks. This component is useful for tasks such as image segmentation, object detection, and image captioning. + +![AnnotatedImage screenshot](https://user-images.githubusercontent.com/7870876/232142720-86e0020f-beaf-47b9-a843-689c9621f09c.gif) + +Example usage: + +```python +with gr.Blocks() as demo: + img = gr.Image() + img_section = gr.AnnotatedImage() + def mask(img): + top_left_corner = [0, 0, 20, 20] + random_mask = np.random.randint(0, 2, img.shape[:2]) + return (img, [(top_left_corner, "left corner"), (random_mask, "random")]) + img.change(mask, img, img_section) +``` + +See the [image_segmentation demo](https://github.com/gradio-app/gradio/tree/main/demo/image_segmentation) for a full example. By [@aliabid94](https://github.com/aliabid94) in [PR 3836](https://github.com/gradio-app/gradio/pull/3836) + +### Bug Fixes: + +No changes to highlight. + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.26.0 + +### New Features: + +###### `Video` component supports subtitles + +- Allow the video component to accept subtitles as input, by [@tomchang25](https://github.com/tomchang25) in [PR 3673](https://github.com/gradio-app/gradio/pull/3673). To provide subtitles, simply return a tuple consisting of `(path_to_video, path_to_subtitles)` from your function. Both `.srt` and `.vtt` formats are supported: + +```py +with gr.Blocks() as demo: + gr.Video(("video.mp4", "captions.srt")) +``` + +### Bug Fixes: + +- Fix code markdown support in `gr.Chatbot()` component by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3816](https://github.com/gradio-app/gradio/pull/3816) + +### Documentation Changes: + +- Updates the "view API" page in Gradio apps to use the `gradio_client` library by [@aliabd](https://github.com/aliabd) in [PR 3765](https://github.com/gradio-app/gradio/pull/3765) + +- Read more about how to use the `gradio_client` library here: https://gradio.app/getting-started-with-the-python-client/ + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.25.0 + +### New Features: + +- Improve error messages when number of inputs/outputs to event handlers mismatch, by [@space-nuko](https://github.com/space-nuko) in [PR 3519](https://github.com/gradio-app/gradio/pull/3519) + +- Add `select` listener to Images, allowing users to click on any part of an image and get the coordinates of the click by [@aliabid94](https://github.com/aliabid94) in [PR 3786](https://github.com/gradio-app/gradio/pull/3786). + +```python +with gr.Blocks() as demo: + img = gr.Image() + textbox = gr.Textbox() + + def select_handler(img, evt: gr.SelectData): + selected_pixel = img[evt.index[1], evt.index[0]] + return f"Selected pixel: {selected_pixel}" + + img.select(select_handler, img, textbox) +``` + +![Recording 2023-04-08 at 17 44 39](https://user-images.githubusercontent.com/7870876/230748572-90a2a8d5-116d-4769-bb53-5516555fbd0f.gif) + +### Bug Fixes: + +- Increase timeout for sending analytics data by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3647](https://github.com/gradio-app/gradio/pull/3647) +- Fix bug where http token was not accessed over websocket connections by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3735](https://github.com/gradio-app/gradio/pull/3735) +- Add ability to specify `rows`, `columns` and `object-fit` in `style()` for `gr.Gallery()` component by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3586](https://github.com/gradio-app/gradio/pull/3586) +- Fix bug where recording an audio file through the microphone resulted in a corrupted file name by [@abidlabs](https://github.com/abidlabs) in [PR 3770](https://github.com/gradio-app/gradio/pull/3770) +- Added "ssl_verify" to blocks.launch method to allow for use of self-signed certs by [@garrettsutula](https://github.com/garrettsutula) in [PR 3873](https://github.com/gradio-app/gradio/pull/3873) +- Fix bug where iterators where not being reset for processes that terminated early by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3777](https://github.com/gradio-app/gradio/pull/3777) +- Fix bug where the upload button was not properly handling the `file_count='multiple'` case by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3782](https://github.com/gradio-app/gradio/pull/3782) +- Fix bug where use Via API button was giving error by [@Devang-C](https://github.com/Devang-C) in [PR 3783](https://github.com/gradio-app/gradio/pull/3783) + +### Documentation Changes: + +- Fix invalid argument docstrings, by [@akx](https://github.com/akx) in [PR 3740](https://github.com/gradio-app/gradio/pull/3740) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fixed IPv6 listening to work with bracket [::1] notation, by [@dsully](https://github.com/dsully) in [PR 3695](https://github.com/gradio-app/gradio/pull/3695) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.24.1 + +### New Features: + +- No changes to highlight. + +### Bug Fixes: + +- Fixes Chatbot issue where new lines were being created every time a message was sent back and forth by [@aliabid94](https://github.com/aliabid94) in [PR 3717](https://github.com/gradio-app/gradio/pull/3717). +- Fixes data updating in DataFrame invoking a `select` event once the dataframe has been selected. By [@yiyuezhuo](https://github.com/yiyuezhuo) in [PR 3861](https://github.com/gradio-app/gradio/pull/3861) +- Fixes false positive warning which is due to too strict type checking by [@yiyuezhuo](https://github.com/yiyuezhuo) in [PR 3837](https://github.com/gradio-app/gradio/pull/3837). + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.24.0 + +### New Features: + +- Trigger the release event when Slider number input is released or unfocused by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3589](https://github.com/gradio-app/gradio/pull/3589) +- Created Theme Builder, which allows users to create themes without writing any code, by [@aliabid94](https://github.com/aliabid94) in [PR 3664](https://github.com/gradio-app/gradio/pull/3664). Launch by: + + ```python + import gradio as gr + gr.themes.builder() + ``` + + ![Theme Builder](https://user-images.githubusercontent.com/7870876/228204929-d71cbba5-69c2-45b3-bd20-e3a201d98b12.png) + +- The `Dropdown` component now has a `allow_custom_value` parameter that lets users type in custom values not in the original list of choices. +- The `Colorpicker` component now has a `.blur()` event + +###### Added a download button for videos! 📥 + +![download_video](https://user-images.githubusercontent.com/41651716/227009612-9bc5fb72-2a44-4c55-9b7b-a0fa098e7f25.gif) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3581](https://github.com/gradio-app/gradio/pull/3581). + +- Trigger the release event when Slider number input is released or unfocused by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3589](https://github.com/gradio-app/gradio/pull/3589) + +### Bug Fixes: + +- Fixed bug where text for altair plots was not legible in dark mode by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3555](https://github.com/gradio-app/gradio/pull/3555) +- Fixes `Chatbot` and `Image` components so that files passed during processing are added to a directory where they can be served from, by [@abidlabs](https://github.com/abidlabs) in [PR 3523](https://github.com/gradio-app/gradio/pull/3523) +- Use Gradio API server to send telemetry using `huggingface_hub` [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3488](https://github.com/gradio-app/gradio/pull/3488) +- Fixes an an issue where if the Blocks scope was not exited, then State could be shared across sessions, by [@abidlabs](https://github.com/abidlabs) in [PR 3600](https://github.com/gradio-app/gradio/pull/3600) +- Ensures that `gr.load()` loads and applies the upstream theme, by [@abidlabs](https://github.com/abidlabs) in [PR 3641](https://github.com/gradio-app/gradio/pull/3641) +- Fixed bug where "or" was not being localized in file upload text by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3599](https://github.com/gradio-app/gradio/pull/3599) +- Fixed bug where chatbot does not autoscroll inside of a tab, row or column by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3637](https://github.com/gradio-app/gradio/pull/3637) +- Fixed bug where textbox shrinks when `lines` set to larger than 20 by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3637](https://github.com/gradio-app/gradio/pull/3637) +- Ensure CSS has fully loaded before rendering the application, by [@pngwn](https://github.com/pngwn) in [PR 3573](https://github.com/gradio-app/gradio/pull/3573) +- Support using an empty list as `gr.Dataframe` value, by [@space-nuko](https://github.com/space-nuko) in [PR 3646](https://github.com/gradio-app/gradio/pull/3646) +- Fixed `gr.Image` not filling the entire element size, by [@space-nuko](https://github.com/space-nuko) in [PR 3649](https://github.com/gradio-app/gradio/pull/3649) +- Make `gr.Code` support the `lines` property, by [@space-nuko](https://github.com/space-nuko) in [PR 3651](https://github.com/gradio-app/gradio/pull/3651) +- Fixes certain `_js` return values being double wrapped in an array, by [@space-nuko](https://github.com/space-nuko) in [PR 3594](https://github.com/gradio-app/gradio/pull/3594) +- Correct the documentation of `gr.File` component to state that its preprocessing method converts the uploaded file to a temporary file, by @RussellLuo in [PR 3660](https://github.com/gradio-app/gradio/pull/3660) +- Fixed bug in Serializer ValueError text by [@osanseviero](https://github.com/osanseviero) in [PR 3669](https://github.com/gradio-app/gradio/pull/3669) +- Fix default parameter argument and `gr.Progress` used in same function, by [@space-nuko](https://github.com/space-nuko) in [PR 3671](https://github.com/gradio-app/gradio/pull/3671) +- Hide `Remove All` button in `gr.Dropdown` single-select mode by [@space-nuko](https://github.com/space-nuko) in [PR 3678](https://github.com/gradio-app/gradio/pull/3678) +- Fix broken spaces in docs by [@aliabd](https://github.com/aliabd) in [PR 3698](https://github.com/gradio-app/gradio/pull/3698) +- Fix items in `gr.Dropdown` besides the selected item receiving a checkmark, by [@space-nuko](https://github.com/space-nuko) in [PR 3644](https://github.com/gradio-app/gradio/pull/3644) +- Fix several `gr.Dropdown` issues and improve usability, by [@space-nuko](https://github.com/space-nuko) in [PR 3705](https://github.com/gradio-app/gradio/pull/3705) + +### Documentation Changes: + +- Makes some fixes to the Theme Guide related to naming of variables, by [@abidlabs](https://github.com/abidlabs) in [PR 3561](https://github.com/gradio-app/gradio/pull/3561) +- Documented `HuggingFaceDatasetJSONSaver` by [@osanseviero](https://github.com/osanseviero) in [PR 3604](https://github.com/gradio-app/gradio/pull/3604) +- Makes some additions to documentation of `Audio` and `State` components, and fixes the `pictionary` demo by [@abidlabs](https://github.com/abidlabs) in [PR 3611](https://github.com/gradio-app/gradio/pull/3611) +- Fix outdated sharing your app guide by [@aliabd](https://github.com/aliabd) in [PR 3699](https://github.com/gradio-app/gradio/pull/3699) + +### Testing and Infrastructure Changes: + +- Removed heavily-mocked tests related to comet_ml, wandb, and mlflow as they added a significant amount of test dependencies that prevented installation of test dependencies on Windows environments. By [@abidlabs](https://github.com/abidlabs) in [PR 3608](https://github.com/gradio-app/gradio/pull/3608) +- Added Windows continuous integration, by [@space-nuko](https://github.com/space-nuko) in [PR 3628](https://github.com/gradio-app/gradio/pull/3628) +- Switched linting from flake8 + isort to `ruff`, by [@akx](https://github.com/akx) in [PR 3710](https://github.com/gradio-app/gradio/pull/3710) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Mobile responsive iframes in themes guide by [@aliabd](https://github.com/aliabd) in [PR 3562](https://github.com/gradio-app/gradio/pull/3562) +- Remove extra $demo from theme guide by [@aliabd](https://github.com/aliabd) in [PR 3563](https://github.com/gradio-app/gradio/pull/3563) +- Set the theme name to be the upstream repo name when loading from the hub by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3595](https://github.com/gradio-app/gradio/pull/3595) +- Copy everything in website Dockerfile, fix build issues by [@aliabd](https://github.com/aliabd) in [PR 3659](https://github.com/gradio-app/gradio/pull/3659) +- Raise error when an event is queued but the queue is not configured by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3640](https://github.com/gradio-app/gradio/pull/3640) +- Allows users to apss in a string name for a built-in theme, by [@abidlabs](https://github.com/abidlabs) in [PR 3641](https://github.com/gradio-app/gradio/pull/3641) +- Added `orig_name` to Video output in the backend so that the front end can set the right name for downloaded video files by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3700](https://github.com/gradio-app/gradio/pull/3700) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.23.0 + +### New Features: + +###### Theme Sharing! + +Once you have created a theme, you can upload it to the HuggingFace Hub to let others view it, use it, and build off of it! You can also download, reuse, and remix other peoples' themes. See https://gradio.app/theming-guide/ for more details. + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3428](https://github.com/gradio-app/gradio/pull/3428) + +### Bug Fixes: + +- Removes leading spaces from all lines of code uniformly in the `gr.Code()` component. By [@abidlabs](https://github.com/abidlabs) in [PR 3556](https://github.com/gradio-app/gradio/pull/3556) +- Fixed broken login page, by [@aliabid94](https://github.com/aliabid94) in [PR 3529](https://github.com/gradio-app/gradio/pull/3529) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fix rendering of dropdowns to take more space, and related bugs, by [@aliabid94](https://github.com/aliabid94) in [PR 3549](https://github.com/gradio-app/gradio/pull/3549) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.22.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Restore label bars by [@aliabid94](https://github.com/aliabid94) in [PR 3507](https://github.com/gradio-app/gradio/pull/3507) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.22.0 + +### New Features: + +###### Official Theme release + +Gradio now supports a new theme system, which allows you to customize the look and feel of your app. You can now use the `theme=` kwarg to pass in a prebuilt theme, or customize your own! See https://gradio.app/theming-guide/ for more details. By [@aliabid94](https://github.com/aliabid94) in [PR 3470](https://github.com/gradio-app/gradio/pull/3470) and [PR 3497](https://github.com/gradio-app/gradio/pull/3497) + +###### `elem_classes` + +Add keyword argument `elem_classes` to Components to control class names of components, in the same manner as existing `elem_id`. +By [@aliabid94](https://github.com/aliabid94) in [PR 3466](https://github.com/gradio-app/gradio/pull/3466) + +### Bug Fixes: + +- Fixes the File.upload() event trigger which broke as part of the change in how we uploaded files by [@abidlabs](https://github.com/abidlabs) in [PR 3462](https://github.com/gradio-app/gradio/pull/3462) +- Fixed issue with `gr.Request` object failing to handle dictionaries when nested keys couldn't be converted to variable names [#3454](https://github.com/gradio-app/gradio/issues/3454) by [@radames](https://github.com/radames) in [PR 3459](https://github.com/gradio-app/gradio/pull/3459) +- Fixed bug where css and client api was not working properly when mounted in a subpath by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3482](https://github.com/gradio-app/gradio/pull/3482) + +### Documentation Changes: + +- Document gr.Error in the docs by [@aliabd](https://github.com/aliabd) in [PR 3465](https://github.com/gradio-app/gradio/pull/3465) + +### Testing and Infrastructure Changes: + +- Pinned `pyright==1.1.298` for stability by [@abidlabs](https://github.com/abidlabs) in [PR 3475](https://github.com/gradio-app/gradio/pull/3475) +- Removed `IOComponent.add_interactive_to_config()` by [@space-nuko](https://github.com/space-nuko) in [PR 3476](https://github.com/gradio-app/gradio/pull/3476) +- Removed `IOComponent.generate_sample()` by [@space-nuko](https://github.com/space-nuko) in [PR 3475](https://github.com/gradio-app/gradio/pull/3483) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Revert primary button background color in dark mode by [@aliabid94](https://github.com/aliabid94) in [PR 3468](https://github.com/gradio-app/gradio/pull/3468) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.21.0 + +### New Features: + +###### Theme Sharing 🎨 🤝 + +You can now share your gradio themes with the world! + +After creating a theme, you can upload it to the HuggingFace Hub to let others view it, use it, and build off of it! + +###### Uploading + +There are two ways to upload a theme, via the theme class instance or the command line. + +1. Via the class instance + +```python +my_theme.push_to_hub(repo_name="my_theme", + version="0.2.0", + hf_token="...") +``` + +2. Via the command line + +First save the theme to disk + +```python +my_theme.dump(filename="my_theme.json") +``` + +Then use the `upload_theme` command: + +```bash +upload_theme\ +"my_theme.json"\ +"my_theme"\ +"0.2.0"\ +"" +``` + +The `version` must be a valid [semantic version](https://www.geeksforgeeks.org/introduction-semantic-versioning/) string. + +This creates a space on the huggingface hub to host the theme files and show potential users a preview of your theme. + +An example theme space is here: https://huggingface.co/spaces/freddyaboulton/dracula_revamped + +###### Downloading + +To use a theme from the hub, use the `from_hub` method on the `ThemeClass` and pass it to your app: + +```python +my_theme = gr.Theme.from_hub("freddyaboulton/my_theme") + +with gr.Blocks(theme=my_theme) as demo: + .... +``` + +You can also pass the theme string directly to `Blocks` or `Interface` (`gr.Blocks(theme="freddyaboulton/my_theme")`) + +You can pin your app to an upstream theme version by using semantic versioning expressions. + +For example, the following would ensure the theme we load from the `my_theme` repo was between versions `0.1.0` and `0.2.0`: + +```python +with gr.Blocks(theme="freddyaboulton/my_theme@>=0.1.0,<0.2.0") as demo: + .... +``` + +by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3428](https://github.com/gradio-app/gradio/pull/3428) + +###### Code component 🦾 + +New code component allows you to enter, edit and display code with full syntax highlighting by [@pngwn](https://github.com/pngwn) in [PR 3421](https://github.com/gradio-app/gradio/pull/3421) + +###### The `Chatbot` component now supports audio, video, and images + +The `Chatbot` component now supports audio, video, and images with a simple syntax: simply +pass in a tuple with the URL or filepath (the second optional element of the tuple is alt text), and the image/audio/video will be displayed: + +```python +gr.Chatbot([ + (("driving.mp4",), "cool video"), + (("cantina.wav",), "cool audio"), + (("lion.jpg", "A lion"), "cool pic"), +]).style(height=800) +``` + +image + +Note: images were previously supported via Markdown syntax and that is still supported for backwards compatibility. By [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3413](https://github.com/gradio-app/gradio/pull/3413) + +- Allow consecutive function triggers with `.then` and `.success` by [@aliabid94](https://github.com/aliabid94) in [PR 3430](https://github.com/gradio-app/gradio/pull/3430) + +- New code component allows you to enter, edit and display code with full syntax highlighting by [@pngwn](https://github.com/pngwn) in [PR 3421](https://github.com/gradio-app/gradio/pull/3421) + +![](https://user-images.githubusercontent.com/12937446/224116643-5cfb94b3-93ce-43ee-bb7b-c25c3b66e0a1.png) + +- Added the `.select()` event listener, which also includes event data that can be passed as an argument to a function with type hint `gr.SelectData`. The following components support the `.select()` event listener: Chatbot, CheckboxGroup, Dataframe, Dropdown, File, Gallery, HighlightedText, Label, Radio, TabItem, Tab, Textbox. Example usage: + +```python +import gradio as gr + +with gr.Blocks() as demo: + gallery = gr.Gallery(["images/1.jpg", "images/2.jpg", "images/3.jpg"]) + selected_index = gr.Textbox() + + def on_select(evt: gr.SelectData): + return evt.index + + gallery.select(on_select, None, selected_index) +``` + +By [@aliabid94](https://github.com/aliabid94) in [PR 3399](https://github.com/gradio-app/gradio/pull/3399) + +- The `Textbox` component now includes a copy button by [@abidlabs](https://github.com/abidlabs) in [PR 3452](https://github.com/gradio-app/gradio/pull/3452) + +### Bug Fixes: + +- Use `huggingface_hub` to send telemetry on `interface` and `blocks`; eventually to replace segment by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3342](https://github.com/gradio-app/gradio/pull/3342) +- Ensure load events created by components (randomize for slider, callable values) are never queued unless every is passed by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3391](https://github.com/gradio-app/gradio/pull/3391) +- Prevent in-place updates of `generic_update` by shallow copying by [@gitgithan](https://github.com/gitgithan) in [PR 3405](https://github.com/gradio-app/gradio/pull/3405) to fix [#3282](https://github.com/gradio-app/gradio/issues/3282) +- Fix bug caused by not importing `BlockContext` in `utils.py` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3424](https://github.com/gradio-app/gradio/pull/3424) +- Ensure dropdown does not highlight partial matches by [@pngwn](https://github.com/pngwn) in [PR 3421](https://github.com/gradio-app/gradio/pull/3421) +- Fix mic button display by [@aliabid94](https://github.com/aliabid94) in [PR 3456](https://github.com/gradio-app/gradio/pull/3456) + +### Documentation Changes: + +- Added a section on security and access when sharing Gradio apps by [@abidlabs](https://github.com/abidlabs) in [PR 3408](https://github.com/gradio-app/gradio/pull/3408) +- Add Chinese README by [@uanu2002](https://github.com/uanu2002) in [PR 3394](https://github.com/gradio-app/gradio/pull/3394) +- Adds documentation for web components by [@abidlabs](https://github.com/abidlabs) in [PR 3407](https://github.com/gradio-app/gradio/pull/3407) +- Fixed link in Chinese readme by [@eltociear](https://github.com/eltociear) in [PR 3417](https://github.com/gradio-app/gradio/pull/3417) +- Document Blocks methods by [@aliabd](https://github.com/aliabd) in [PR 3427](https://github.com/gradio-app/gradio/pull/3427) +- Fixed bug where event handlers were not showing up in documentation by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3434](https://github.com/gradio-app/gradio/pull/3434) + +### Testing and Infrastructure Changes: + +- Fixes tests that were failing locally but passing on CI by [@abidlabs](https://github.com/abidlabs) in [PR 3411](https://github.com/gradio-app/gradio/pull/3411) +- Remove codecov from the repo by [@aliabd](https://github.com/aliabd) in [PR 3415](https://github.com/gradio-app/gradio/pull/3415) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Prevent in-place updates of `generic_update` by shallow copying by [@gitgithan](https://github.com/gitgithan) in [PR 3405](https://github.com/gradio-app/gradio/pull/3405) to fix [#3282](https://github.com/gradio-app/gradio/issues/3282) +- Persist file names of files uploaded through any Gradio component by [@abidlabs](https://github.com/abidlabs) in [PR 3412](https://github.com/gradio-app/gradio/pull/3412) +- Fix markdown embedded component in docs by [@aliabd](https://github.com/aliabd) in [PR 3410](https://github.com/gradio-app/gradio/pull/3410) +- Clean up event listeners code by [@aliabid94](https://github.com/aliabid94) in [PR 3420](https://github.com/gradio-app/gradio/pull/3420) +- Fix css issue with spaces logo by [@aliabd](https://github.com/aliabd) in [PR 3422](https://github.com/gradio-app/gradio/pull/3422) +- Makes a few fixes to the `JSON` component (show_label parameter, icons) in [@abidlabs](https://github.com/abidlabs) in [PR 3451](https://github.com/gradio-app/gradio/pull/3451) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.20.1 + +### New Features: + +- Add `height` kwarg to style in `gr.Chatbot()` component by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3369](https://github.com/gradio-app/gradio/pull/3369) + +```python +chatbot = gr.Chatbot().style(height=500) +``` + +### Bug Fixes: + +- Ensure uploaded images are always shown in the sketch tool by [@pngwn](https://github.com/pngwn) in [PR 3386](https://github.com/gradio-app/gradio/pull/3386) +- Fixes bug where when if fn is a non-static class member, then self should be ignored as the first param of the fn by [@or25](https://github.com/or25) in [PR #3227](https://github.com/gradio-app/gradio/pull/3227) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.20.0 + +### New Features: + +###### Release event for Slider + +Now you can trigger your python function to run when the slider is released as opposed to every slider change value! + +Simply use the `release` method on the slider + +```python +slider.release(function, inputs=[...], outputs=[...], api_name="predict") +``` + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3353](https://github.com/gradio-app/gradio/pull/3353) + +###### Dropdown Component Updates + +The standard dropdown component now supports searching for choices. Also when `multiselect` is `True`, you can specify `max_choices` to set the maximum number of choices you want the user to be able to select from the dropdown component. + +```python +gr.Dropdown(label="Choose your favorite colors", choices=["red", "blue", "green", "yellow", "orange"], multiselect=True, max_choices=2) +``` + +by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3211](https://github.com/gradio-app/gradio/pull/3211) + +###### Download button for images 🖼️ + +Output images will now automatically have a download button displayed to make it easier to save and share +the results of Machine Learning art models. + +![download_sketch](https://user-images.githubusercontent.com/41651716/221025113-e693bf41-eabd-42b3-a4f2-26f2708d98fe.gif) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3297](https://github.com/gradio-app/gradio/pull/3297) + +- Updated image upload component to accept all image formats, including lossless formats like .webp by [@fienestar](https://github.com/fienestar) in [PR 3225](https://github.com/gradio-app/gradio/pull/3225) +- Adds a disabled mode to the `gr.Button` component by setting `interactive=False` by [@abidlabs](https://github.com/abidlabs) in [PR 3266](https://github.com/gradio-app/gradio/pull/3266) and [PR 3288](https://github.com/gradio-app/gradio/pull/3288) +- Adds visual feedback to the when the Flag button is clicked, by [@abidlabs](https://github.com/abidlabs) in [PR 3289](https://github.com/gradio-app/gradio/pull/3289) +- Adds ability to set `flagging_options` display text and saved flag separately by [@abidlabs](https://github.com/abidlabs) in [PR 3289](https://github.com/gradio-app/gradio/pull/3289) +- Allow the setting of `brush_radius` for the `Image` component both as a default and via `Image.update()` by [@pngwn](https://github.com/pngwn) in [PR 3277](https://github.com/gradio-app/gradio/pull/3277) +- Added `info=` argument to form components to enable extra context provided to users, by [@aliabid94](https://github.com/aliabid94) in [PR 3291](https://github.com/gradio-app/gradio/pull/3291) +- Allow developers to access the username of a logged-in user from the `gr.Request()` object using the `.username` attribute by [@abidlabs](https://github.com/abidlabs) in [PR 3296](https://github.com/gradio-app/gradio/pull/3296) +- Add `preview` option to `Gallery.style` that launches the gallery in preview mode when first loaded by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3345](https://github.com/gradio-app/gradio/pull/3345) + +### Bug Fixes: + +- Ensure `mirror_webcam` is always respected by [@pngwn](https://github.com/pngwn) in [PR 3245](https://github.com/gradio-app/gradio/pull/3245) +- Fix issue where updated markdown links were not being opened in a new tab by [@gante](https://github.com/gante) in [PR 3236](https://github.com/gradio-app/gradio/pull/3236) +- API Docs Fixes by [@aliabd](https://github.com/aliabd) in [PR 3287](https://github.com/gradio-app/gradio/pull/3287) +- Added a timeout to queue messages as some demos were experiencing infinitely growing queues from active jobs waiting forever for clients to respond by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3196](https://github.com/gradio-app/gradio/pull/3196) +- Fixes the height of rendered LaTeX images so that they match the height of surrounding text by [@abidlabs](https://github.com/abidlabs) in [PR 3258](https://github.com/gradio-app/gradio/pull/3258) and in [PR 3276](https://github.com/gradio-app/gradio/pull/3276) +- Fix bug where matplotlib images where always too small on the front end by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3274](https://github.com/gradio-app/gradio/pull/3274) +- Remove embed's `initial_height` when loading is complete so the embed finds its natural height once it is loaded [@pngwn](https://github.com/pngwn) in [PR 3292](https://github.com/gradio-app/gradio/pull/3292) +- Prevent Sketch from crashing when a default image is provided by [@pngwn](https://github.com/pngwn) in [PR 3277](https://github.com/gradio-app/gradio/pull/3277) +- Respect the `shape` argument on the front end when creating Image Sketches by [@pngwn](https://github.com/pngwn) in [PR 3277](https://github.com/gradio-app/gradio/pull/3277) +- Fix infinite loop caused by setting `Dropdown's` value to be `[]` and adding a change event on the dropdown by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3295](https://github.com/gradio-app/gradio/pull/3295) +- Fix change event listed twice in image docs by [@aliabd](https://github.com/aliabd) in [PR 3318](https://github.com/gradio-app/gradio/pull/3318) +- Fix bug that cause UI to be vertically centered at all times by [@pngwn](https://github.com/pngwn) in [PR 3336](https://github.com/gradio-app/gradio/pull/3336) +- Fix bug where `height` set in `Gallery.style` was not respected by the front-end by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3343](https://github.com/gradio-app/gradio/pull/3343) +- Ensure markdown lists are rendered correctly by [@pngwn](https://github.com/pngwn) in [PR 3341](https://github.com/gradio-app/gradio/pull/3341) +- Ensure that the initial empty value for `gr.Dropdown(Multiselect=True)` is an empty list and the initial value for `gr.Dropdown(Multiselect=False)` is an empty string by [@pngwn](https://github.com/pngwn) in [PR 3338](https://github.com/gradio-app/gradio/pull/3338) +- Ensure uploaded images respect the shape property when the canvas is also enabled by [@pngwn](https://github.com/pngwn) in [PR 3351](https://github.com/gradio-app/gradio/pull/3351) +- Ensure that Google Analytics works correctly when gradio apps are created with `analytics_enabled=True` by [@abidlabs](https://github.com/abidlabs) in [PR 3349](https://github.com/gradio-app/gradio/pull/3349) +- Fix bug where files were being re-uploaded after updates by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3375](https://github.com/gradio-app/gradio/pull/3375) +- Fix error when using backen_fn and custom js at the same time by [@jialeicui](https://github.com/jialeicui) in [PR 3358](https://github.com/gradio-app/gradio/pull/3358) +- Support new embeds for huggingface spaces subdomains by [@pngwn](https://github.com/pngwn) in [PR 3367](https://github.com/gradio-app/gradio/pull/3367) + +### Documentation Changes: + +- Added the `types` field to the dependency field in the config by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3315](https://github.com/gradio-app/gradio/pull/3315) +- Gradio Status Page by [@aliabd](https://github.com/aliabd) in [PR 3331](https://github.com/gradio-app/gradio/pull/3331) +- Adds a Guide on setting up a dashboard from Supabase data using the `gr.BarPlot` + component by [@abidlabs](https://github.com/abidlabs) in [PR 3275](https://github.com/gradio-app/gradio/pull/3275) + +### Testing and Infrastructure Changes: + +- Adds a script to benchmark the performance of the queue and adds some instructions on how to use it. By [@freddyaboulton](https://github.com/freddyaboulton) and [@abidlabs](https://github.com/abidlabs) in [PR 3272](https://github.com/gradio-app/gradio/pull/3272) +- Flaky python tests no longer cancel non-flaky tests by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3344](https://github.com/gradio-app/gradio/pull/3344) + +### Breaking Changes: + +- Chatbot bubble colors can no longer be set by `chatbot.style(color_map=)` by [@aliabid94] in [PR 3370](https://github.com/gradio-app/gradio/pull/3370) + +### Full Changelog: + +- Fixed comment typo in components.py by [@eltociear](https://github.com/eltociear) in [PR 3235](https://github.com/gradio-app/gradio/pull/3235) +- Cleaned up chatbot ui look and feel by [@aliabid94] in [PR 3370](https://github.com/gradio-app/gradio/pull/3370) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.19.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- UI fixes including footer and API docs by [@aliabid94](https://github.com/aliabid94) in [PR 3242](https://github.com/gradio-app/gradio/pull/3242) +- Updated image upload component to accept all image formats, including lossless formats like .webp by [@fienestar](https://github.com/fienestar) in [PR 3225](https://github.com/gradio-app/gradio/pull/3225) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Added backend support for themes by [@aliabid94](https://github.com/aliabid94) in [PR 2931](https://github.com/gradio-app/gradio/pull/2931) +- Added support for button sizes "lg" (default) and "sm". + +### Contributors Shoutout: + +No changes to highlight. + +## 3.19.0 + +### New Features: + +###### Improved embedding experience + +When embedding a spaces-hosted gradio app as a web component, you now get an improved UI linking back to the original space, better error handling and more intelligent load performance. No changes are required to your code to benefit from this enhanced experience; simply upgrade your gradio SDK to the latest version. + +![](https://user-images.githubusercontent.com/12937446/219653294-86937632-72c1-4e93-a77c-af705d49382a.png) + +This behaviour is configurable. You can disable the info panel at the bottom by passing `info="false"`. You can disable the container entirely by passing `container="false"`. + +Error statuses are reported in the UI with an easy way for end-users to report problems to the original space author via the community tab of that Hugginface space: + +![](https://user-images.githubusercontent.com/12937446/219655499-88019443-d694-44e7-9e6d-242e19d10a5c.png) + +By default, gradio apps are lazy loaded, vastly improving performance when there are several demos on the page. Metadata is loaded ahead of time, but the space will only be loaded and rendered when it is in view. + +This behaviour is configurable. You can pass `eager="true"` to load and render the space regardless of whether or not it is currently on the screen. + +by [@pngwn](https://github.com/pngwn) in [PR 3205](https://github.com/gradio-app/gradio/pull/3205) + +###### New `gr.BarPlot` component! 📊 + +Create interactive bar plots from a high-level interface with `gr.BarPlot`. +No need to remember matplotlib syntax anymore! + +Example usage: + +```python +import gradio as gr +import pandas as pd + +simple = pd.DataFrame({ + 'a': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'], + 'b': [28, 55, 43, 91, 81, 53, 19, 87, 52] +}) + +with gr.Blocks() as demo: + gr.BarPlot( + simple, + x="a", + y="b", + title="Simple Bar Plot with made up data", + tooltip=['a', 'b'], + ) + +demo.launch() +``` + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3157](https://github.com/gradio-app/gradio/pull/3157) + +###### Bokeh plots are back! 🌠 + +Fixed a bug that prevented bokeh plots from being displayed on the front end and extended support for both 2.x and 3.x versions of bokeh! + +![image](https://user-images.githubusercontent.com/41651716/219468324-0d82e07f-8fb4-4ff9-b40c-8250b29e45f7.png) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3212](https://github.com/gradio-app/gradio/pull/3212) + +### Bug Fixes: + +- Adds ability to add a single message from the bot or user side. Ex: specify `None` as the second value in the tuple, to add a single message in the chatbot from the "bot" side. + +```python +gr.Chatbot([("Hi, I'm DialoGPT. Try asking me a question.", None)]) +``` + +By [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3165](https://github.com/gradio-app/gradio/pull/3165) + +- Fixes `gr.utils.delete_none` to only remove props whose values are `None` from the config by [@abidlabs](https://github.com/abidlabs) in [PR 3188](https://github.com/gradio-app/gradio/pull/3188) +- Fix bug where embedded demos were not loading files properly by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3177](https://github.com/gradio-app/gradio/pull/3177) +- The `change` event is now triggered when users click the 'Clear All' button of the multiselect DropDown component by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3195](https://github.com/gradio-app/gradio/pull/3195) +- Stops File component from freezing when a large file is uploaded by [@aliabid94](https://github.com/aliabid94) in [PR 3191](https://github.com/gradio-app/gradio/pull/3191) +- Support Chinese pinyin in Dataframe by [@aliabid94](https://github.com/aliabid94) in [PR 3206](https://github.com/gradio-app/gradio/pull/3206) +- The `clear` event is now triggered when images are cleared by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3218](https://github.com/gradio-app/gradio/pull/3218) +- Fix bug where auth cookies where not sent when connecting to an app via http by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3223](https://github.com/gradio-app/gradio/pull/3223) +- Ensure latext CSS is always applied in light and dark mode by [@pngwn](https://github.com/pngwn) in [PR 3233](https://github.com/gradio-app/gradio/pull/3233) + +### Documentation Changes: + +- Sort components in docs by alphabetic order by [@aliabd](https://github.com/aliabd) in [PR 3152](https://github.com/gradio-app/gradio/pull/3152) +- Changes to W&B guide by [@scottire](https://github.com/scottire) in [PR 3153](https://github.com/gradio-app/gradio/pull/3153) +- Keep pnginfo metadata for gallery by [@wfng92](https://github.com/wfng92) in [PR 3150](https://github.com/gradio-app/gradio/pull/3150) +- Add a section on how to run a Gradio app locally [@osanseviero](https://github.com/osanseviero) in [PR 3170](https://github.com/gradio-app/gradio/pull/3170) +- Fixed typos in gradio events function documentation by [@vidalmaxime](https://github.com/vidalmaxime) in [PR 3168](https://github.com/gradio-app/gradio/pull/3168) +- Added an example using Gradio's batch mode with the diffusers library by [@abidlabs](https://github.com/abidlabs) in [PR 3224](https://github.com/gradio-app/gradio/pull/3224) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fix demos page css and add close demos button by [@aliabd](https://github.com/aliabd) in [PR 3151](https://github.com/gradio-app/gradio/pull/3151) +- Caches temp files from base64 input data by giving them a deterministic path based on the contents of data by [@abidlabs](https://github.com/abidlabs) in [PR 3197](https://github.com/gradio-app/gradio/pull/3197) +- Better warnings (when there is a mismatch between the number of output components and values returned by a function, or when the `File` component or `UploadButton` component includes a `file_types` parameter along with `file_count=="dir"`) by [@abidlabs](https://github.com/abidlabs) in [PR 3194](https://github.com/gradio-app/gradio/pull/3194) +- Raises a `gr.Error` instead of a regular Python error when you use `gr.Interface.load()` to load a model and there's an error querying the HF API by [@abidlabs](https://github.com/abidlabs) in [PR 3194](https://github.com/gradio-app/gradio/pull/3194) +- Fixed gradio share links so that they are persistent and do not reset if network + connection is disrupted by by [XciD](https://github.com/XciD), [Wauplin](https://github.com/Wauplin), and [@abidlabs](https://github.com/abidlabs) in [PR 3149](https://github.com/gradio-app/gradio/pull/3149) and a follow-up to allow it to work for users upgrading from a previous Gradio version in [PR 3221](https://github.com/gradio-app/gradio/pull/3221) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.18.0 + +### New Features: + +###### Revamped Stop Button for Interfaces 🛑 + +If your Interface function is a generator, there used to be a separate `Stop` button displayed next +to the `Submit` button. + +We've revamed the `Submit` button so that it turns into a `Stop` button during the generation process. +Clicking on the `Stop` button will cancel the generation and turn it back to a `Submit` button. +The `Stop` button will automatically turn back to a `Submit` button at the end of the generation if you don't use it! + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3124](https://github.com/gradio-app/gradio/pull/3124) + +###### Queue now works with reload mode! + +You can now call `queue` on your `demo` outside of the `if __name__ == "__main__"` block and +run the script in reload mode with the `gradio` command. + +Any changes to the `app.py` file will be reflected in the webpage automatically and the queue will work +properly! + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3089](https://github.com/gradio-app/gradio/pull/3089) + +###### Allow serving files from additional directories + +```python +demo = gr.Interface(...) +demo.launch( + file_directories=["/var/lib/demo/path/to/resources"] +) +``` + +By [@maxaudron](https://github.com/maxaudron) in [PR 3075](https://github.com/gradio-app/gradio/pull/3075) + +### Bug Fixes: + +- Fixes URL resolution on Windows by [@abidlabs](https://github.com/abidlabs) in [PR 3108](https://github.com/gradio-app/gradio/pull/3108) +- Example caching now works with components without a label attribute (e.g. `Column`) by [@abidlabs](https://github.com/abidlabs) in [PR 3123](https://github.com/gradio-app/gradio/pull/3123) +- Ensure the Video component correctly resets the UI state when a new video source is loaded and reduce choppiness of UI by [@pngwn](https://github.com/abidlabs) in [PR 3117](https://github.com/gradio-app/gradio/pull/3117) +- Fixes loading private Spaces by [@abidlabs](https://github.com/abidlabs) in [PR 3068](https://github.com/gradio-app/gradio/pull/3068) +- Added a warning when attempting to launch an `Interface` via the `%%blocks` jupyter notebook magic command by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3126](https://github.com/gradio-app/gradio/pull/3126) +- Fixes bug where interactive output image cannot be set when in edit mode by [@dawoodkhan82](https://github.com/@dawoodkhan82) in [PR 3135](https://github.com/gradio-app/gradio/pull/3135) +- A share link will automatically be created when running on Sagemaker notebooks so that the front-end is properly displayed by [@abidlabs](https://github.com/abidlabs) in [PR 3137](https://github.com/gradio-app/gradio/pull/3137) +- Fixes a few dropdown component issues; hide checkmark next to options as expected, and keyboard hover is visible by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3145]https://github.com/gradio-app/gradio/pull/3145) +- Fixed bug where example pagination buttons were not visible in dark mode or displayed under the examples table. By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3144](https://github.com/gradio-app/gradio/pull/3144) +- Fixed bug where the font color of axis labels and titles for native plots did not respond to dark mode preferences. By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3146](https://github.com/gradio-app/gradio/pull/3146) + +### Documentation Changes: + +- Added a guide on the 4 kinds of Gradio Interfaces by [@yvrjsharma](https://github.com/yvrjsharma) and [@abidlabs](https://github.com/abidlabs) in [PR 3003](https://github.com/gradio-app/gradio/pull/3003) +- Explained that the parameters in `launch` will not be respected when using reload mode, e.g. `gradio` command by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3089](https://github.com/gradio-app/gradio/pull/3089) +- Added a demo to show how to set up variable numbers of outputs in Gradio by [@abidlabs](https://github.com/abidlabs) in [PR 3127](https://github.com/gradio-app/gradio/pull/3127) +- Updated docs to reflect that the `equal_height` parameter should be passed to the `.style()` method of `gr.Row()` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3125](https://github.com/gradio-app/gradio/pull/3125) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Changed URL of final image for `fake_diffusion` demos by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3120](https://github.com/gradio-app/gradio/pull/3120) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.17.1 + +### New Features: + +###### iOS image rotation fixed 🔄 + +Previously photos uploaded via iOS would be rotated after processing. This has been fixed by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3089](https://github.com/gradio-app/gradio/pull/3091) + +######### Before + +![image](https://user-images.githubusercontent.com/41651716/215846507-a36e9d05-1ac2-4867-8ab3-ce045a9415d9.png) + +######### After + +![image](https://user-images.githubusercontent.com/41651716/215846554-e41773ed-70f0-491a-9952-6a18babf91ef.png) + +###### Run on Kaggle kernels 🧪 + +A share link will automatically be created when running on Kaggle kernels (notebooks) so that the front-end is properly displayed. + +![image](https://user-images.githubusercontent.com/41651716/216104254-2cf55599-449c-436c-b57e-40f6a83f9eee.png) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3101](https://github.com/gradio-app/gradio/pull/3101) + +### Bug Fixes: + +- Fix bug where examples were not rendered correctly for demos created with Blocks api that had multiple input compinents by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3090](https://github.com/gradio-app/gradio/pull/3090) +- Fix change event listener for JSON, HighlightedText, Chatbot by [@aliabid94](https://github.com/aliabid94) in [PR 3095](https://github.com/gradio-app/gradio/pull/3095) +- Fixes bug where video and file change event not working [@tomchang25](https://github.com/tomchang25) in [PR 3098](https://github.com/gradio-app/gradio/pull/3098) +- Fixes bug where static_video play and pause event not working [@tomchang25](https://github.com/tomchang25) in [PR 3098](https://github.com/gradio-app/gradio/pull/3098) +- Fixed `Gallery.style(grid=...)` by by [@aliabd](https://github.com/aliabd) in [PR 3107](https://github.com/gradio-app/gradio/pull/3107) + +### Documentation Changes: + +- Update chatbot guide to include blocks demo and markdown support section by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3023](https://github.com/gradio-app/gradio/pull/3023) + +* Fix a broken link in the Quick Start guide, by [@cakiki](https://github.com/cakiki) in [PR 3109](https://github.com/gradio-app/gradio/pull/3109) +* Better docs navigation on mobile by [@aliabd](https://github.com/aliabd) in [PR 3112](https://github.com/gradio-app/gradio/pull/3112) +* Add a guide on using Gradio with [Comet](https://comet.com/), by [@DN6](https://github.com/DN6/) in [PR 3058](https://github.com/gradio-app/gradio/pull/3058) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Set minimum `markdown-it-py` version to `2.0.0` so that the dollar math plugin is compatible by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3102](https://github.com/gradio-app/gradio/pull/3102) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.17.0 + +### New Features: + +###### Extended support for Interface.load! 🏗️ + +You can now load `image-to-text` and `conversational` pipelines from the hub! + +###### Image-to-text Demo + +```python +io = gr.Interface.load("models/nlpconnect/vit-gpt2-image-captioning", + api_key="") +io.launch() +``` + +image + +###### conversational Demo + +```python +chatbot = gr.Interface.load("models/microsoft/DialoGPT-medium", + api_key="") +chatbot.launch() +``` + +![chatbot_load](https://user-images.githubusercontent.com/41651716/213260220-3eaa25b7-a38b-48c6-adeb-2718bdf297a2.gif) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3011](https://github.com/gradio-app/gradio/pull/3011) + +###### Download Button added to Model3D Output Component 📥 + +No need for an additional file output component to enable model3d file downloads anymore. We now added a download button to the model3d component itself. + +Screenshot 2023-01-18 at 3 52 45 PM + +By [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3014](https://github.com/gradio-app/gradio/pull/3014) + +###### Fixing Auth on Spaces 🔑 + +Authentication on spaces works now! Third party cookies must be enabled on your browser to be able +to log in. Some browsers disable third party cookies by default (Safari, Chrome Incognito). + +![auth_spaces](https://user-images.githubusercontent.com/41651716/215528417-09538933-0576-4d1d-b3b9-1e877ab01905.gif) + +### Bug Fixes: + +- Fixes bug where interpretation event was not configured correctly by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2993](https://github.com/gradio-app/gradio/pull/2993) +- Fix relative import bug in reload mode by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2992](https://github.com/gradio-app/gradio/pull/2992) +- Fixes bug where png files were not being recognized when uploading images by [@abidlabs](https://github.com/abidlabs) in [PR 3002](https://github.com/gradio-app/gradio/pull/3002) +- Fixes bug where external Spaces could not be loaded and used as functions if they returned files by [@abidlabs](https://github.com/abidlabs) in [PR 3004](https://github.com/gradio-app/gradio/pull/3004) +- Fix bug where file serialization output was not JSON serializable by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2999](https://github.com/gradio-app/gradio/pull/2999) +- Fixes bug where png files were not being recognized when uploading images by [@abidlabs](https://github.com/abidlabs) in [PR 3002](https://github.com/gradio-app/gradio/pull/3002) +- Fixes bug where temporary uploaded files were not being added to temp sets by [@abidlabs](https://github.com/abidlabs) in [PR 3005](https://github.com/gradio-app/gradio/pull/3005) +- Fixes issue where markdown support in chatbot breaks older demos [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3006](https://github.com/gradio-app/gradio/pull/3006) +- Fixes the `/file/` route that was broken in a recent change in [PR 3010](https://github.com/gradio-app/gradio/pull/3010) +- Fix bug where the Image component could not serialize image urls by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2957](https://github.com/gradio-app/gradio/pull/2957) +- Fix forwarding for guides after SEO renaming by [@aliabd](https://github.com/aliabd) in [PR 3017](https://github.com/gradio-app/gradio/pull/3017) +- Switch all pages on the website to use latest stable gradio by [@aliabd](https://github.com/aliabd) in [PR 3016](https://github.com/gradio-app/gradio/pull/3016) +- Fix bug related to deprecated parameters in `huggingface_hub` for the HuggingFaceDatasetSaver in [PR 3025](https://github.com/gradio-app/gradio/pull/3025) +- Added better support for symlinks in the way absolute paths are resolved by [@abidlabs](https://github.com/abidlabs) in [PR 3037](https://github.com/gradio-app/gradio/pull/3037) +- Fix several minor frontend bugs (loading animation, examples as gallery) frontend [@aliabid94](https://github.com/3026) in [PR 2961](https://github.com/gradio-app/gradio/pull/3026). +- Fixes bug that the chatbot sample code does not work with certain input value by [@petrov826](https://github.com/petrov826) in [PR 3039](https://github.com/gradio-app/gradio/pull/3039). +- Fix shadows for form element and ensure focus styles more visible in dark mode [@pngwn](https://github.com/pngwn) in [PR 3042](https://github.com/gradio-app/gradio/pull/3042). +- Fixed bug where the Checkbox and Dropdown change events were not triggered in response to other component changes by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3045](https://github.com/gradio-app/gradio/pull/3045) +- Fix bug where the queue was not properly restarted after launching a `closed` app by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3022](https://github.com/gradio-app/gradio/pull/3022) +- Adding missing embedded components on docs by [@aliabd](https://github.com/aliabd) in [PR 3027](https://github.com/gradio-app/gradio/pull/3027) +- Fixes bug where app would crash if the `file_types` parameter of `gr.File` or `gr.UploadButton` was not a list by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3048](https://github.com/gradio-app/gradio/pull/3048) +- Ensure CSS mounts correctly regardless of how many Gradio instances are on the page [@pngwn](https://github.com/pngwn) in [PR 3059](https://github.com/gradio-app/gradio/pull/3059). +- Fix bug where input component was not hidden in the frontend for `UploadButton` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3053](https://github.com/gradio-app/gradio/pull/3053) +- Fixes issue where after clicking submit or undo, the sketch output wouldn't clear. [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3047](https://github.com/gradio-app/gradio/pull/3047) +- Ensure spaces embedded via the web component always use the correct URLs for server requests and change ports for testing to avoid strange collisions when users are working with embedded apps locally by [@pngwn](https://github.com/pngwn) in [PR 3065](https://github.com/gradio-app/gradio/pull/3065) +- Preserve selected image of Gallery through updated by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3061](https://github.com/gradio-app/gradio/pull/3061) +- Fix bug where auth was not respected on HF spaces by [@freddyaboulton](https://github.com/freddyaboulton) and [@aliabid94](https://github.com/aliabid94) in [PR 3049](https://github.com/gradio-app/gradio/pull/3049) +- Fixes bug where tabs selected attribute not working if manually change tab by [@tomchang25](https://github.com/tomchang25) in [3055](https://github.com/gradio-app/gradio/pull/3055) +- Change chatbot to show dots on progress, and fix bug where chatbot would not stick to bottom in the case of images by [@aliabid94](https://github.com/aliabid94) in [PR 3067](https://github.com/gradio-app/gradio/pull/3079) + +### Documentation Changes: + +- SEO improvements to guides by[@aliabd](https://github.com/aliabd) in [PR 2915](https://github.com/gradio-app/gradio/pull/2915) +- Use `gr.LinePlot` for the `blocks_kinematics` demo by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2998](https://github.com/gradio-app/gradio/pull/2998) +- Updated the `interface_series_load` to include some inline markdown code by [@abidlabs](https://github.com/abidlabs) in [PR 3051](https://github.com/gradio-app/gradio/pull/3051) + +### Testing and Infrastructure Changes: + +- Adds a GitHub action to test if any large files (> 5MB) are present by [@abidlabs](https://github.com/abidlabs) in [PR 3013](https://github.com/gradio-app/gradio/pull/3013) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Rewrote frontend using CSS variables for themes by [@pngwn](https://github.com/pngwn) in [PR 2840](https://github.com/gradio-app/gradio/pull/2840) +- Moved telemetry requests to run on background threads by [@abidlabs](https://github.com/abidlabs) in [PR 3054](https://github.com/gradio-app/gradio/pull/3054) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.16.2 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fixed file upload fails for files with zero size by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2923](https://github.com/gradio-app/gradio/pull/2923) +- Fixed bug where `mount_gradio_app` would not launch if the queue was enabled in a gradio app by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2939](https://github.com/gradio-app/gradio/pull/2939) +- Fix custom long CSS handling in Blocks by [@anton-l](https://github.com/anton-l) in [PR 2953](https://github.com/gradio-app/gradio/pull/2953) +- Recovers the dropdown change event by [@abidlabs](https://github.com/abidlabs) in [PR 2954](https://github.com/gradio-app/gradio/pull/2954). +- Fix audio file output by [@aliabid94](https://github.com/aliabid94) in [PR 2961](https://github.com/gradio-app/gradio/pull/2961). +- Fixed bug where file extensions of really long files were not kept after download by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2929](https://github.com/gradio-app/gradio/pull/2929) +- Fix bug where outputs for examples where not being returned by the backend by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2955](https://github.com/gradio-app/gradio/pull/2955) +- Fix bug in `blocks_plug` demo that prevented switching tabs programmatically with python [@TashaSkyUp](https://github.com/https://github.com/TashaSkyUp) in [PR 2971](https://github.com/gradio-app/gradio/pull/2971). + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.16.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fix audio file output by [@aliabid94](https://github.com/aliabid94) in [PR 2950](https://github.com/gradio-app/gradio/pull/2950). + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.16.0 + +### New Features: + +###### Send custom progress updates by adding a `gr.Progress` argument after the input arguments to any function. Example: + +```python +def reverse(word, progress=gr.Progress()): + progress(0, desc="Starting") + time.sleep(1) + new_string = "" + for letter in progress.tqdm(word, desc="Reversing"): + time.sleep(0.25) + new_string = letter + new_string + return new_string + +demo = gr.Interface(reverse, gr.Text(), gr.Text()) +``` + +Progress indicator bar by [@aliabid94](https://github.com/aliabid94) in [PR 2750](https://github.com/gradio-app/gradio/pull/2750). + +- Added `title` argument to `TabbedInterface` by @MohamedAliRashad in [#2888](https://github.com/gradio-app/gradio/pull/2888) +- Add support for specifying file extensions for `gr.File` and `gr.UploadButton`, using `file_types` parameter (e.g `gr.File(file_count="multiple", file_types=["text", ".json", ".csv"])`) by @dawoodkhan82 in [#2901](https://github.com/gradio-app/gradio/pull/2901) +- Added `multiselect` option to `Dropdown` by @dawoodkhan82 in [#2871](https://github.com/gradio-app/gradio/pull/2871) + +###### With `multiselect` set to `true` a user can now select multiple options from the `gr.Dropdown` component. + +```python +gr.Dropdown(["angola", "pakistan", "canada"], multiselect=True, value=["angola"]) +``` + +Screenshot 2023-01-03 at 4 14 36 PM + +### Bug Fixes: + +- Fixed bug where an error opening an audio file led to a crash by [@FelixDombek](https://github.com/FelixDombek) in [PR 2898](https://github.com/gradio-app/gradio/pull/2898) +- Fixed bug where setting `default_enabled=False` made it so that the entire queue did not start by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2876](https://github.com/gradio-app/gradio/pull/2876) +- Fixed bug where csv preview for DataFrame examples would show filename instead of file contents by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2877](https://github.com/gradio-app/gradio/pull/2877) +- Fixed bug where an error raised after yielding iterative output would not be displayed in the browser by + [@JaySmithWpg](https://github.com/JaySmithWpg) in [PR 2889](https://github.com/gradio-app/gradio/pull/2889) +- Fixed bug in `blocks_style` demo that was preventing it from launching by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2890](https://github.com/gradio-app/gradio/pull/2890) +- Fixed bug where files could not be downloaded by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2926](https://github.com/gradio-app/gradio/pull/2926) +- Fixed bug where cached examples were not displaying properly by [@a-rogalska](https://github.com/a-rogalska) in [PR 2974](https://github.com/gradio-app/gradio/pull/2974) + +### Documentation Changes: + +- Added a Guide on using Google Sheets to create a real-time dashboard with Gradio's `DataFrame` and `LinePlot` component, by [@abidlabs](https://github.com/abidlabs) in [PR 2816](https://github.com/gradio-app/gradio/pull/2816) +- Add a components - events matrix on the docs by [@aliabd](https://github.com/aliabd) in [PR 2921](https://github.com/gradio-app/gradio/pull/2921) + +### Testing and Infrastructure Changes: + +- Deployed PRs from forks to spaces by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2895](https://github.com/gradio-app/gradio/pull/2895) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- The `default_enabled` parameter of the `Blocks.queue` method has no effect by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2876](https://github.com/gradio-app/gradio/pull/2876) +- Added typing to several Python files in codebase by [@abidlabs](https://github.com/abidlabs) in [PR 2887](https://github.com/gradio-app/gradio/pull/2887) +- Excluding untracked files from demo notebook check action by [@aliabd](https://github.com/aliabd) in [PR 2897](https://github.com/gradio-app/gradio/pull/2897) +- Optimize images and gifs by [@aliabd](https://github.com/aliabd) in [PR 2922](https://github.com/gradio-app/gradio/pull/2922) +- Updated typing by [@1nF0rmed](https://github.com/1nF0rmed) in [PR 2904](https://github.com/gradio-app/gradio/pull/2904) + +### Contributors Shoutout: + +- @JaySmithWpg for making their first contribution to gradio! +- @MohamedAliRashad for making their first contribution to gradio! + +## 3.15.0 + +### New Features: + +Gradio's newest plotting component `gr.LinePlot`! 📈 + +With this component you can easily create time series visualizations with customizable +appearance for your demos and dashboards ... all without having to know an external plotting library. + +For an example of the api see below: + +```python +gr.LinePlot(stocks, + x="date", + y="price", + color="symbol", + color_legend_position="bottom", + width=600, height=400, title="Stock Prices") +``` + +![image](https://user-images.githubusercontent.com/41651716/208711646-81ae3745-149b-46a3-babd-0569aecdd409.png) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2807](https://github.com/gradio-app/gradio/pull/2807) + +### Bug Fixes: + +- Fixed bug where the `examples_per_page` parameter of the `Examples` component was not passed to the internal `Dataset` component by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2861](https://github.com/gradio-app/gradio/pull/2861) +- Fixes loading Spaces that have components with default values by [@abidlabs](https://github.com/abidlabs) in [PR 2855](https://github.com/gradio-app/gradio/pull/2855) +- Fixes flagging when `allow_flagging="auto"` in `gr.Interface()` by [@abidlabs](https://github.com/abidlabs) in [PR 2695](https://github.com/gradio-app/gradio/pull/2695) +- Fixed bug where passing a non-list value to `gr.CheckboxGroup` would crash the entire app by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2866](https://github.com/gradio-app/gradio/pull/2866) + +### Documentation Changes: + +- Added a Guide on using BigQuery with Gradio's `DataFrame` and `ScatterPlot` component, + by [@abidlabs](https://github.com/abidlabs) in [PR 2794](https://github.com/gradio-app/gradio/pull/2794) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fixed importing gradio can cause PIL.Image.registered_extensions() to break by `[@aliencaocao](https://github.com/aliencaocao)` in `[PR 2846](https://github.com/gradio-app/gradio/pull/2846)` +- Fix css glitch and navigation in docs by [@aliabd](https://github.com/aliabd) in [PR 2856](https://github.com/gradio-app/gradio/pull/2856) +- Added the ability to set `x_lim`, `y_lim` and legend positions for `gr.ScatterPlot` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2807](https://github.com/gradio-app/gradio/pull/2807) +- Remove footers and min-height the correct way by [@aliabd](https://github.com/aliabd) in [PR 2860](https://github.com/gradio-app/gradio/pull/2860) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.14.0 + +### New Features: + +###### Add Waveform Visual Support to Audio + +Adds a `gr.make_waveform()` function that creates a waveform video by combining an audio and an optional background image by [@dawoodkhan82](http://github.com/dawoodkhan82) and [@aliabid94](http://github.com/aliabid94) in [PR 2706](https://github.com/gradio-app/gradio/pull/2706. Helpful for making audio outputs much more shareable. + +![waveform screenrecording](https://user-images.githubusercontent.com/7870876/206062396-164a5e71-451a-4fe0-94a7-cbe9269d57e6.gif) + +###### Allows Every Component to Accept an `every` Parameter + +When a component's initial value is a function, the `every` parameter re-runs the function every `every` seconds. By [@abidlabs](https://github.com/abidlabs) in [PR 2806](https://github.com/gradio-app/gradio/pull/2806). Here's a code example: + +```py +import gradio as gr + +with gr.Blocks() as demo: + df = gr.DataFrame(run_query, every=60*60) + +demo.queue().launch() +``` + +### Bug Fixes: + +- Fixed issue where too many temporary files were created, all with randomly generated + filepaths. Now fewer temporary files are created and are assigned a path that is a + hash based on the file contents by [@abidlabs](https://github.com/abidlabs) in [PR 2758](https://github.com/gradio-app/gradio/pull/2758) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.13.2 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +\*No changes to highlight. + +- + +### Documentation Changes: + +- Improves documentation of several queuing-related parameters by [@abidlabs](https://github.com/abidlabs) in [PR 2825](https://github.com/gradio-app/gradio/pull/2825) + +### Testing and Infrastructure Changes: + +- Remove h11 pinning by [@ecederstrand](https://github.com/ecederstrand) in [PR 2820](https://github.com/gradio-app/gradio/pull/2820) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.13.1 + +### New Features: + +###### New Shareable Links + +Replaces tunneling logic based on ssh port-forwarding to that based on `frp` by [XciD](https://github.com/XciD) and [Wauplin](https://github.com/Wauplin) in [PR 2509](https://github.com/gradio-app/gradio/pull/2509) + +You don't need to do anything differently, but when you set `share=True` in `launch()`, +you'll get this message and a public link that look a little bit different: + +```bash +Setting up a public link... we have recently upgraded the way public links are generated. If you encounter any problems, please downgrade to gradio version 3.13.0 +. +Running on public URL: https://bec81a83-5b5c-471e.gradio.live +``` + +These links are a more secure and scalable way to create shareable demos! + +### Bug Fixes: + +- Allows `gr.Dataframe()` to take a `pandas.DataFrame` that includes numpy array and other types as its initial value, by [@abidlabs](https://github.com/abidlabs) in [PR 2804](https://github.com/gradio-app/gradio/pull/2804) +- Add `altair` to requirements.txt by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2811](https://github.com/gradio-app/gradio/pull/2811) +- Added aria-labels to icon buttons that are built into UI components by [@emilyuhde](http://github.com/emilyuhde) in [PR 2791](https://github.com/gradio-app/gradio/pull/2791) + +### Documentation Changes: + +- Fixed some typos in the "Plot Component for Maps" guide by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2811](https://github.com/gradio-app/gradio/pull/2811) + +### Testing and Infrastructure Changes: + +- Fixed test for IP address by [@abidlabs](https://github.com/abidlabs) in [PR 2808](https://github.com/gradio-app/gradio/pull/2808) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fixed typo in parameter `visible` in classes in `templates.py` by [@abidlabs](https://github.com/abidlabs) in [PR 2805](https://github.com/gradio-app/gradio/pull/2805) +- Switched external service for getting IP address from `https://api.ipify.org` to `https://checkip.amazonaws.com/` by [@abidlabs](https://github.com/abidlabs) in [PR 2810](https://github.com/gradio-app/gradio/pull/2810) + +### Contributors Shoutout: + +No changes to highlight. + +- Fixed typo in parameter `visible` in classes in `templates.py` by [@abidlabs](https://github.com/abidlabs) in [PR 2805](https://github.com/gradio-app/gradio/pull/2805) +- Switched external service for getting IP address from `https://api.ipify.org` to `https://checkip.amazonaws.com/` by [@abidlabs](https://github.com/abidlabs) in [PR 2810](https://github.com/gradio-app/gradio/pull/2810) + +## 3.13.0 + +### New Features: + +###### Scatter plot component + +It is now possible to create a scatter plot natively in Gradio! + +The `gr.ScatterPlot` component accepts a pandas dataframe and some optional configuration parameters +and will automatically create a plot for you! + +This is the first of many native plotting components in Gradio! + +For an example of how to use `gr.ScatterPlot` see below: + +```python +import gradio as gr +from vega_datasets import data + +cars = data.cars() + +with gr.Blocks() as demo: + gr.ScatterPlot(show_label=False, + value=cars, + x="Horsepower", + y="Miles_per_Gallon", + color="Origin", + tooltip="Name", + title="Car Data", + y_title="Miles per Gallon", + color_legend_title="Origin of Car").style(container=False) + +demo.launch() +``` + +image + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2764](https://github.com/gradio-app/gradio/pull/2764) + +###### Support for altair plots + +The `Plot` component can now accept altair plots as values! +Simply return an altair plot from your event listener and gradio will display it in the front-end. +See the example below: + +```python +import gradio as gr +import altair as alt +from vega_datasets import data + +cars = data.cars() +chart = ( + alt.Chart(cars) + .mark_point() + .encode( + x="Horsepower", + y="Miles_per_Gallon", + color="Origin", + ) +) + +with gr.Blocks() as demo: + gr.Plot(value=chart) +demo.launch() +``` + +image + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2741](https://github.com/gradio-app/gradio/pull/2741) + +###### Set the background color of a Label component + +The `Label` component now accepts a `color` argument by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2736](https://github.com/gradio-app/gradio/pull/2736). +The `color` argument should either be a valid css color name or hexadecimal string. +You can update the color with `gr.Label.update`! + +This lets you create Alert and Warning boxes with the `Label` component. See below: + +```python +import gradio as gr +import random + +def update_color(value): + if value < 0: + # This is bad so use red + return "#FF0000" + elif 0 <= value <= 20: + # Ok but pay attention (use orange) + return "#ff9966" + else: + # Nothing to worry about + return None + +def update_value(): + choice = random.choice(['good', 'bad', 'so-so']) + color = update_color(choice) + return gr.Label.update(value=choice, color=color) + + +with gr.Blocks() as demo: + label = gr.Label(value=-10) + demo.load(lambda: update_value(), inputs=None, outputs=[label], every=1) +demo.queue().launch() +``` + +![label_bg_color_update](https://user-images.githubusercontent.com/41651716/204400372-80e53857-f26f-4a38-a1ae-1acadff75e89.gif) + +###### Add Brazilian Portuguese translation + +Add Brazilian Portuguese translation (pt-BR.json) by [@pstwh](http://github.com/pstwh) in [PR 2753](https://github.com/gradio-app/gradio/pull/2753): + +image + +### Bug Fixes: + +- Fixed issue where image thumbnails were not showing when an example directory was provided + by [@abidlabs](https://github.com/abidlabs) in [PR 2745](https://github.com/gradio-app/gradio/pull/2745) +- Fixed bug loading audio input models from the hub by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2779](https://github.com/gradio-app/gradio/pull/2779). +- Fixed issue where entities were not merged when highlighted text was generated from the + dictionary inputs [@payoto](https://github.com/payoto) in [PR 2767](https://github.com/gradio-app/gradio/pull/2767) +- Fixed bug where generating events did not finish running even if the websocket connection was closed by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2783](https://github.com/gradio-app/gradio/pull/2783). + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Images in the chatbot component are now resized if they exceed a max width by [@abidlabs](https://github.com/abidlabs) in [PR 2748](https://github.com/gradio-app/gradio/pull/2748) +- Missing parameters have been added to `gr.Blocks().load()` by [@abidlabs](https://github.com/abidlabs) in [PR 2755](https://github.com/gradio-app/gradio/pull/2755) +- Deindex share URLs from search by [@aliabd](https://github.com/aliabd) in [PR 2772](https://github.com/gradio-app/gradio/pull/2772) +- Redirect old links and fix broken ones by [@aliabd](https://github.com/aliabd) in [PR 2774](https://github.com/gradio-app/gradio/pull/2774) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.12.0 + +### New Features: + +###### The `Chatbot` component now supports a subset of Markdown (including bold, italics, code, images) + +You can now pass in some Markdown to the Chatbot component and it will show up, +meaning that you can pass in images as well! by [@abidlabs](https://github.com/abidlabs) in [PR 2731](https://github.com/gradio-app/gradio/pull/2731) + +Here's a simple example that references a local image `lion.jpg` that is in the same +folder as the Python script: + +```py +import gradio as gr + +with gr.Blocks() as demo: + gr.Chatbot([("hi", "hello **abubakar**"), ("![](/file=lion.jpg)", "cool pic")]) + +demo.launch() +``` + +![Alt text](https://user-images.githubusercontent.com/1778297/204357455-5c1a4002-eee7-479d-9a1e-ba2c12522723.png) + +To see a more realistic example, see the new demo `/demo/chatbot_multimodal/run.py`. + +###### Latex support + +Added mathtext (a subset of latex) support to gr.Markdown. Added by [@kashif](https://github.com/kashif) and [@aliabid94](https://github.com/aliabid94) in [PR 2696](https://github.com/gradio-app/gradio/pull/2696). + +Example of how it can be used: + +```python +gr.Markdown( + r""" + # Hello World! $\frac{\sqrt{x + y}}{4}$ is today's lesson. + """) +``` + +###### Update Accordion properties from the backend + +You can now update the Accordion `label` and `open` status with `gr.Accordion.update` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2690](https://github.com/gradio-app/gradio/pull/2690) + +```python +import gradio as gr + +with gr.Blocks() as demo: + with gr.Accordion(label="Open for greeting", open=False) as accordion: + gr.Textbox("Hello!") + open_btn = gr.Button(value="Open Accordion") + close_btn = gr.Button(value="Close Accordion") + open_btn.click( + lambda: gr.Accordion.update(open=True, label="Open Accordion"), + inputs=None, + outputs=[accordion], + ) + close_btn.click( + lambda: gr.Accordion.update(open=False, label="Closed Accordion"), + inputs=None, + outputs=[accordion], + ) +demo.launch() +``` + +![update_accordion](https://user-images.githubusercontent.com/41651716/203164176-b102eae3-babe-4986-ae30-3ab4f400cedc.gif) + +### Bug Fixes: + +- Fixed bug where requests timeout is missing from utils.version_check() by [@yujiehecs](https://github.com/yujiehecs) in [PR 2729](https://github.com/gradio-app/gradio/pull/2729) +- Fixed bug where so that the `File` component can properly preprocess files to "binary" byte-string format by [CoffeeVampir3](https://github.com/CoffeeVampir3) in [PR 2727](https://github.com/gradio-app/gradio/pull/2727) +- Fixed bug to ensure that filenames are less than 200 characters even for non-English languages by [@SkyTNT](https://github.com/SkyTNT) in [PR 2685](https://github.com/gradio-app/gradio/pull/2685) + +### Documentation Changes: + +- Performance improvements to docs on mobile by [@aliabd](https://github.com/aliabd) in [PR 2730](https://github.com/gradio-app/gradio/pull/2730) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Make try examples button more prominent by [@aliabd](https://github.com/aliabd) in [PR 2705](https://github.com/gradio-app/gradio/pull/2705) +- Fix id clashes in docs by [@aliabd](https://github.com/aliabd) in [PR 2713](https://github.com/gradio-app/gradio/pull/2713) +- Fix typos in guide docs by [@andridns](https://github.com/andridns) in [PR 2722](https://github.com/gradio-app/gradio/pull/2722) +- Add option to `include_audio` in Video component. When `True`, for `source="webcam"` this will record audio and video, for `source="upload"` this will retain the audio in an uploaded video by [@mandargogate](https://github.com/MandarGogate) in [PR 2721](https://github.com/gradio-app/gradio/pull/2721) + +### Contributors Shoutout: + +- [@andridns](https://github.com/andridns) made their first contribution in [PR 2722](https://github.com/gradio-app/gradio/pull/2722)! + +## 3.11.0 + +### New Features: + +###### Upload Button + +There is now a new component called the `UploadButton` which is a file upload component but in button form! You can also specify what file types it should accept in the form of a list (ex: `image`, `video`, `audio`, `text`, or generic `file`). Added by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2591](https://github.com/gradio-app/gradio/pull/2591). + +Example of how it can be used: + +```python +import gradio as gr + +def upload_file(files): + file_paths = [file.name for file in files] + return file_paths + +with gr.Blocks() as demo: + file_output = gr.File() + upload_button = gr.UploadButton("Click to Upload a File", file_types=["image", "video"], file_count="multiple") + upload_button.upload(upload_file, upload_button, file_output) + +demo.launch() +``` + +###### Revamped API documentation page + +New API Docs page with in-browser playground and updated aesthetics. [@gary149](https://github.com/gary149) in [PR 2652](https://github.com/gradio-app/gradio/pull/2652) + +###### Revamped Login page + +Previously our login page had its own CSS, had no dark mode, and had an ugly json message on the wrong credentials. Made the page more aesthetically consistent, added dark mode support, and a nicer error message. [@aliabid94](https://github.com/aliabid94) in [PR 2684](https://github.com/gradio-app/gradio/pull/2684) + +###### Accessing the Requests Object Directly + +You can now access the Request object directly in your Python function by [@abidlabs](https://github.com/abidlabs) in [PR 2641](https://github.com/gradio-app/gradio/pull/2641). This means that you can access request headers, the client IP address, and so on. In order to use it, add a parameter to your function and set its type hint to be `gr.Request`. Here's a simple example: + +```py +import gradio as gr + +def echo(name, request: gr.Request): + if request: + print("Request headers dictionary:", request.headers) + print("IP address:", request.client.host) + return name + +io = gr.Interface(echo, "textbox", "textbox").launch() +``` + +### Bug Fixes: + +- Fixed bug that limited files from being sent over websockets to 16MB. The new limit + is now 1GB by [@abidlabs](https://github.com/abidlabs) in [PR 2709](https://github.com/gradio-app/gradio/pull/2709) + +### Documentation Changes: + +- Updated documentation for embedding Gradio demos on Spaces as web components by + [@julien-c](https://github.com/julien-c) in [PR 2698](https://github.com/gradio-app/gradio/pull/2698) +- Updated IFrames in Guides to use the host URL instead of the Space name to be consistent with the new method for embedding Spaces, by + [@julien-c](https://github.com/julien-c) in [PR 2692](https://github.com/gradio-app/gradio/pull/2692) +- Colab buttons on every demo in the website! Just click open in colab, and run the demo there. + +https://user-images.githubusercontent.com/9021060/202878400-cb16ed47-f4dd-4cb0-b2f0-102a9ff64135.mov + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Better warnings and error messages for `gr.Interface.load()` by [@abidlabs](https://github.com/abidlabs) in [PR 2694](https://github.com/gradio-app/gradio/pull/2694) +- Add open in colab buttons to demos in docs and /demos by [@aliabd](https://github.com/aliabd) in [PR 2608](https://github.com/gradio-app/gradio/pull/2608) +- Apply different formatting for the types in component docstrings by [@aliabd](https://github.com/aliabd) in [PR 2707](https://github.com/gradio-app/gradio/pull/2707) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.10.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Passes kwargs into `gr.Interface.load()` by [@abidlabs](https://github.com/abidlabs) in [PR 2669](https://github.com/gradio-app/gradio/pull/2669) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Clean up printed statements in Embedded Colab Mode by [@aliabid94](https://github.com/aliabid94) in [PR 2612](https://github.com/gradio-app/gradio/pull/2612) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.10.0 + +- Add support for `'password'` and `'email'` types to `Textbox`. [@pngwn](https://github.com/pngwn) in [PR 2653](https://github.com/gradio-app/gradio/pull/2653) +- `gr.Textbox` component will now raise an exception if `type` is not "text", "email", or "password" [@pngwn](https://github.com/pngwn) in [PR 2653](https://github.com/gradio-app/gradio/pull/2653). This will cause demos using the deprecated `gr.Textbox(type="number")` to raise an exception. + +### Bug Fixes: + +- Updated the minimum FastApi used in tests to version 0.87 by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2647](https://github.com/gradio-app/gradio/pull/2647) +- Fixed bug where interfaces with examples could not be loaded with `gr.Interface.load` by [@freddyaboulton](https://github.com/freddyaboulton) [PR 2640](https://github.com/gradio-app/gradio/pull/2640) +- Fixed bug where the `interactive` property of a component could not be updated by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2639](https://github.com/gradio-app/gradio/pull/2639) +- Fixed bug where some URLs were not being recognized as valid URLs and thus were not + loading correctly in various components by [@abidlabs](https://github.com/abidlabs) in [PR 2659](https://github.com/gradio-app/gradio/pull/2659) + +### Documentation Changes: + +- Fix some typos in the embedded demo names in "05_using_blocks_like_functions.md" by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2656](https://github.com/gradio-app/gradio/pull/2656) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Add support for `'password'` and `'email'` types to `Textbox`. [@pngwn](https://github.com/pngwn) in [PR 2653](https://github.com/gradio-app/gradio/pull/2653) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.9.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Only set a min height on md and html when loading by [@pngwn](https://github.com/pngwn) in [PR 2623](https://github.com/gradio-app/gradio/pull/2623) + +### Documentation Changes: + +- See docs for the latest gradio commit to main as well the latest pip release: + +![main-vs-pip](https://user-images.githubusercontent.com/9021060/199607887-aab1ae4e-a070-4527-966d-024397abe15b.gif) + +- Modified the "Connecting To a Database Guide" to use `pd.read_sql` as opposed to low-level postgres connector by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2604](https://github.com/gradio-app/gradio/pull/2604) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Dropdown for seeing docs as latest or main by [@aliabd](https://github.com/aliabd) in [PR 2544](https://github.com/gradio-app/gradio/pull/2544) +- Allow `gr.Templates` to accept parameters to override the defaults by [@abidlabs](https://github.com/abidlabs) in [PR 2600](https://github.com/gradio-app/gradio/pull/2600) +- Components now throw a `ValueError()` if constructed with invalid parameters for `type` or `source` (for components that take those parameters) in [PR 2610](https://github.com/gradio-app/gradio/pull/2610) +- Allow auth with using queue by [@GLGDLY](https://github.com/GLGDLY) in [PR 2611](https://github.com/gradio-app/gradio/pull/2611) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.9 + +### New Features: + +- Gradio is now embedded directly in colab without requiring the share link by [@aliabid94](https://github.com/aliabid94) in [PR 2455](https://github.com/gradio-app/gradio/pull/2455) + +###### Calling functions by api_name in loaded apps + +When you load an upstream app with `gr.Blocks.load`, you can now specify which fn +to call with the `api_name` parameter. + +```python +import gradio as gr +english_translator = gr.Blocks.load(name="spaces/gradio/english-translator") +german = english_translator("My name is Freddy", api_name='translate-to-german') +``` + +The `api_name` parameter will take precedence over the `fn_index` parameter. + +### Bug Fixes: + +- Fixed bug where None could not be used for File,Model3D, and Audio examples by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2588](https://github.com/gradio-app/gradio/pull/2588) +- Fixed links in Plotly map guide + demo by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2578](https://github.com/gradio-app/gradio/pull/2578) +- `gr.Blocks.load()` now correctly loads example files from Spaces [@abidlabs](https://github.com/abidlabs) in [PR 2594](https://github.com/gradio-app/gradio/pull/2594) +- Fixed bug when image clear started upload dialog [@mezotaken](https://github.com/mezotaken) in [PR 2577](https://github.com/gradio-app/gradio/pull/2577) + +### Documentation Changes: + +- Added a Guide on how to configure the queue for maximum performance by [@abidlabs](https://github.com/abidlabs) in [PR 2558](https://github.com/gradio-app/gradio/pull/2558) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Add `api_name` to `Blocks.__call__` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2593](https://github.com/gradio-app/gradio/pull/2593) +- Update queue with using deque & update requirements by [@GLGDLY](https://github.com/GLGDLY) in [PR 2428](https://github.com/gradio-app/gradio/pull/2428) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.8.2 + +### Bug Fixes: + +- Ensure gradio apps embedded via spaces use the correct endpoint for predictions. [@pngwn](https://github.com/pngwn) in [PR 2567](https://github.com/gradio-app/gradio/pull/2567) +- Ensure gradio apps embedded via spaces use the correct websocket protocol. [@pngwn](https://github.com/pngwn) in [PR 2571](https://github.com/gradio-app/gradio/pull/2571) + +### New Features: + +###### Running Events Continuously + +Gradio now supports the ability to run an event continuously on a fixed schedule. To use this feature, +pass `every=# of seconds` to the event definition. This will run the event every given number of seconds! + +This can be used to: + +- Create live visualizations that show the most up to date data +- Refresh the state of the frontend automatically in response to changes in the backend + +Here is an example of a live plot that refreshes every half second: + +```python +import math +import gradio as gr +import plotly.express as px +import numpy as np + + +plot_end = 2 * math.pi + + +def get_plot(period=1): + global plot_end + x = np.arange(plot_end - 2 * math.pi, plot_end, 0.02) + y = np.sin(2*math.pi*period * x) + fig = px.line(x=x, y=y) + plot_end += 2 * math.pi + return fig + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + gr.Markdown("Change the value of the slider to automatically update the plot") + period = gr.Slider(label="Period of plot", value=1, minimum=0, maximum=10, step=1) + plot = gr.Plot(label="Plot (updates every half second)") + + dep = demo.load(get_plot, None, plot, every=0.5) + period.change(get_plot, period, plot, every=0.5, cancels=[dep]) + +demo.queue().launch() +``` + +![live_demo](https://user-images.githubusercontent.com/41651716/198357377-633ce460-4e31-47bd-8202-1440cdd6fe19.gif) + +### Bug Fixes: + +No changes to highlight. + +### Documentation Changes: + +- Explained how to set up `queue` and `auth` when working with reload mode by by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3089](https://github.com/gradio-app/gradio/pull/3089) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Allows loading private Spaces by passing an an `api_key` to `gr.Interface.load()` + by [@abidlabs](https://github.com/abidlabs) in [PR 2568](https://github.com/gradio-app/gradio/pull/2568) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.8 + +### New Features: + +- Allows event listeners to accept a single dictionary as its argument, where the keys are the components and the values are the component values. This is set by passing the input components in the event listener as a set instead of a list. [@aliabid94](https://github.com/aliabid94) in [PR 2550](https://github.com/gradio-app/gradio/pull/2550) + +### Bug Fixes: + +- Fix whitespace issue when using plotly. [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2548](https://github.com/gradio-app/gradio/pull/2548) +- Apply appropriate alt text to all gallery images. [@camenduru](https://github.com/camenduru) in [PR 2358](https://github.com/gradio-app/gradio/pull/2538) +- Removed erroneous tkinter import in gradio.blocks by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2555](https://github.com/gradio-app/gradio/pull/2555) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Added the `every` keyword to event listeners that runs events on a fixed schedule by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2512](https://github.com/gradio-app/gradio/pull/2512) +- Fix whitespace issue when using plotly. [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2548](https://github.com/gradio-app/gradio/pull/2548) +- Apply appropriate alt text to all gallery images. [@camenduru](https://github.com/camenduru) in [PR 2358](https://github.com/gradio-app/gradio/pull/2538) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.7 + +### New Features: + +###### Batched Functions + +Gradio now supports the ability to pass _batched_ functions. Batched functions are just +functions which take in a list of inputs and return a list of predictions. + +For example, here is a batched function that takes in two lists of inputs (a list of +words and a list of ints), and returns a list of trimmed words as output: + +```py +import time + +def trim_words(words, lens): + trimmed_words = [] + time.sleep(5) + for w, l in zip(words, lens): + trimmed_words.append(w[:l]) + return [trimmed_words] +``` + +The advantage of using batched functions is that if you enable queuing, the Gradio +server can automatically _batch_ incoming requests and process them in parallel, +potentially speeding up your demo. Here's what the Gradio code looks like (notice +the `batch=True` and `max_batch_size=16` -- both of these parameters can be passed +into event triggers or into the `Interface` class) + +```py +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + word = gr.Textbox(label="word", value="abc") + leng = gr.Number(label="leng", precision=0, value=1) + output = gr.Textbox(label="Output") + with gr.Row(): + run = gr.Button() + + event = run.click(trim_words, [word, leng], output, batch=True, max_batch_size=16) + +demo.queue() +demo.launch() +``` + +In the example above, 16 requests could be processed in parallel (for a total inference +time of 5 seconds), instead of each request being processed separately (for a total +inference time of 80 seconds). + +###### Upload Event + +`Video`, `Audio`, `Image`, and `File` components now support a `upload()` event that is triggered when a user uploads a file into any of these components. + +Example usage: + +```py +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + input_video = gr.Video() + output_video = gr.Video() + + # Clears the output video when an input video is uploaded + input_video.upload(lambda : None, None, output_video) +``` + +### Bug Fixes: + +- Fixes issue where plotly animations, interactivity, titles, legends, were not working properly. [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2486](https://github.com/gradio-app/gradio/pull/2486) +- Prevent requests to the `/api` endpoint from skipping the queue if the queue is enabled for that event by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2493](https://github.com/gradio-app/gradio/pull/2493) +- Fixes a bug with `cancels` in event triggers so that it works properly if multiple + Blocks are rendered by [@abidlabs](https://github.com/abidlabs) in [PR 2530](https://github.com/gradio-app/gradio/pull/2530) +- Prevent invalid targets of events from crashing the whole application. [@pngwn](https://github.com/pngwn) in [PR 2534](https://github.com/gradio-app/gradio/pull/2534) +- Properly dequeue cancelled events when multiple apps are rendered by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2540](https://github.com/gradio-app/gradio/pull/2540) +- Fixes videos being cropped due to height/width params not being used [@hannahblair](https://github.com/hannahblair) in [PR 4946](https://github.com/gradio-app/gradio/pull/4946) + +### Documentation Changes: + +- Added an example interactive dashboard to the "Tabular & Plots" section of the Demos page by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2508](https://github.com/gradio-app/gradio/pull/2508) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fixes the error message if a user builds Gradio locally and tries to use `share=True` by [@abidlabs](https://github.com/abidlabs) in [PR 2502](https://github.com/gradio-app/gradio/pull/2502) +- Allows the render() function to return self by [@Raul9595](https://github.com/Raul9595) in [PR 2514](https://github.com/gradio-app/gradio/pull/2514) +- Fixes issue where plotly animations, interactivity, titles, legends, were not working properly. [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2486](https://github.com/gradio-app/gradio/pull/2486) +- Gradio now supports batched functions by [@abidlabs](https://github.com/abidlabs) in [PR 2218](https://github.com/gradio-app/gradio/pull/2218) +- Add `upload` event for `Video`, `Audio`, `Image`, and `File` components [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2448](https://github.com/gradio-app/gradio/pull/2456) +- Changes websocket path for Spaces as it is no longer necessary to have a different URL for websocket connections on Spaces by [@abidlabs](https://github.com/abidlabs) in [PR 2528](https://github.com/gradio-app/gradio/pull/2528) +- Clearer error message when events are defined outside of a Blocks scope, and a warning if you + try to use `Series` or `Parallel` with `Blocks` by [@abidlabs](https://github.com/abidlabs) in [PR 2543](https://github.com/gradio-app/gradio/pull/2543) +- Adds support for audio samples that are in `float64`, `float16`, or `uint16` formats by [@abidlabs](https://github.com/abidlabs) in [PR 2545](https://github.com/gradio-app/gradio/pull/2545) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.6 + +### New Features: + +###### Cancelling Running Events + +Running events can be cancelled when other events are triggered! To test this feature, pass the `cancels` parameter to the event listener. +For this feature to work, the queue must be enabled. + +![cancel_on_change_rl](https://user-images.githubusercontent.com/41651716/195952623-61a606bd-e82b-4e1a-802e-223154cb8727.gif) + +Code: + +```python +import time +import gradio as gr + +def fake_diffusion(steps): + for i in range(steps): + time.sleep(1) + yield str(i) + +def long_prediction(*args, **kwargs): + time.sleep(10) + return 42 + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + n = gr.Slider(1, 10, value=9, step=1, label="Number Steps") + run = gr.Button() + output = gr.Textbox(label="Iterative Output") + stop = gr.Button(value="Stop Iterating") + with gr.Column(): + prediction = gr.Number(label="Expensive Calculation") + run_pred = gr.Button(value="Run Expensive Calculation") + with gr.Column(): + cancel_on_change = gr.Textbox(label="Cancel Iteration and Expensive Calculation on Change") + + click_event = run.click(fake_diffusion, n, output) + stop.click(fn=None, inputs=None, outputs=None, cancels=[click_event]) + pred_event = run_pred.click(fn=long_prediction, inputs=None, outputs=prediction) + + cancel_on_change.change(None, None, None, cancels=[click_event, pred_event]) + + +demo.queue(concurrency_count=1, max_size=20).launch() +``` + +For interfaces, a stop button will be added automatically if the function uses a `yield` statement. + +```python +import gradio as gr +import time + +def iteration(steps): + for i in range(steps): + time.sleep(0.5) + yield i + +gr.Interface(iteration, + inputs=gr.Slider(minimum=1, maximum=10, step=1, value=5), + outputs=gr.Number()).queue().launch() +``` + +![stop_interface_rl](https://user-images.githubusercontent.com/41651716/195952883-e7ca4235-aae3-4852-8f28-96d01d0c5822.gif) + +### Bug Fixes: + +- Add loading status tracker UI to HTML and Markdown components. [@pngwn](https://github.com/pngwn) in [PR 2474](https://github.com/gradio-app/gradio/pull/2474) +- Fixed videos being mirrored in the front-end if source is not webcam by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2475](https://github.com/gradio-app/gradio/pull/2475) +- Add clear button for timeseries component [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2487](https://github.com/gradio-app/gradio/pull/2487) +- Removes special characters from temporary filenames so that the files can be served by components [@abidlabs](https://github.com/abidlabs) in [PR 2480](https://github.com/gradio-app/gradio/pull/2480) +- Fixed infinite reload loop when mounting gradio as a sub application by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2477](https://github.com/gradio-app/gradio/pull/2477) + +### Documentation Changes: + +- Adds a demo to show how a sound alert can be played upon completion of a prediction by [@abidlabs](https://github.com/abidlabs) in [PR 2478](https://github.com/gradio-app/gradio/pull/2478) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Enable running events to be cancelled from other events by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2433](https://github.com/gradio-app/gradio/pull/2433) +- Small fix for version check before reuploading demos by [@aliabd](https://github.com/aliabd) in [PR 2469](https://github.com/gradio-app/gradio/pull/2469) +- Add loading status tracker UI to HTML and Markdown components. [@pngwn](https://github.com/pngwn) in [PR 2400](https://github.com/gradio-app/gradio/pull/2474) +- Add clear button for timeseries component [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2487](https://github.com/gradio-app/gradio/pull/2487) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.5 + +### Bug Fixes: + +- Ensure that Gradio does not take control of the HTML page title when embedding a gradio app as a web component, this behaviour flipped by adding `control_page_title="true"` to the webcomponent. [@pngwn](https://github.com/pngwn) in [PR 2400](https://github.com/gradio-app/gradio/pull/2400) +- Decreased latency in iterative-output demos by making the iteration asynchronous [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2409](https://github.com/gradio-app/gradio/pull/2409) +- Fixed queue getting stuck under very high load by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2374](https://github.com/gradio-app/gradio/pull/2374) +- Ensure that components always behave as if `interactive=True` were set when the following conditions are true: + + - no default value is provided, + - they are not set as the input or output of an event, + - `interactive` kwarg is not set. + + [@pngwn](https://github.com/pngwn) in [PR 2459](https://github.com/gradio-app/gradio/pull/2459) + +### New Features: + +- When an `Image` component is set to `source="upload"`, it is now possible to drag and drop and image to replace a previously uploaded image by [@pngwn](https://github.com/pngwn) in [PR 1711](https://github.com/gradio-app/gradio/issues/1711) +- The `gr.Dataset` component now accepts `HTML` and `Markdown` components by [@abidlabs](https://github.com/abidlabs) in [PR 2437](https://github.com/gradio-app/gradio/pull/2437) + +### Documentation Changes: + +- Improved documentation for the `gr.Dataset` component by [@abidlabs](https://github.com/abidlabs) in [PR 2437](https://github.com/gradio-app/gradio/pull/2437) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +- The `Carousel` component is officially deprecated. Since gradio 3.0, code containing the `Carousel` component would throw warnings. As of the next release, the `Carousel` component will raise an exception. + +### Full Changelog: + +- Speeds up Gallery component by using temporary files instead of base64 representation in the front-end by [@proxyphi](https://github.com/proxyphi), [@pngwn](https://github.com/pngwn), and [@abidlabs](https://github.com/abidlabs) in [PR 2265](https://github.com/gradio-app/gradio/pull/2265) +- Fixed some embedded demos in the guides by not loading the gradio web component in some guides by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2403](https://github.com/gradio-app/gradio/pull/2403) +- When an `Image` component is set to `source="upload"`, it is now possible to drag and drop and image to replace a previously uploaded image by [@pngwn](https://github.com/pngwn) in [PR 2400](https://github.com/gradio-app/gradio/pull/2410) +- Improve documentation of the `Blocks.load()` event by [@abidlabs](https://github.com/abidlabs) in [PR 2413](https://github.com/gradio-app/gradio/pull/2413) +- Decreased latency in iterative-output demos by making the iteration asynchronous [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2409](https://github.com/gradio-app/gradio/pull/2409) +- Updated share link message to reference new Spaces Hardware [@abidlabs](https://github.com/abidlabs) in [PR 2423](https://github.com/gradio-app/gradio/pull/2423) +- Automatically restart spaces if they're down by [@aliabd](https://github.com/aliabd) in [PR 2405](https://github.com/gradio-app/gradio/pull/2405) +- Carousel component is now deprecated by [@abidlabs](https://github.com/abidlabs) in [PR 2434](https://github.com/gradio-app/gradio/pull/2434) +- Build Gradio from source in ui tests by by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2440](https://github.com/gradio-app/gradio/pull/2440) +- Change "return ValueError" to "raise ValueError" by [@vzakharov](https://github.com/vzakharov) in [PR 2445](https://github.com/gradio-app/gradio/pull/2445) +- Add guide on creating a map demo using the `gr.Plot()` component [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2402](https://github.com/gradio-app/gradio/pull/2402) +- Add blur event for `Textbox` and `Number` components [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2448](https://github.com/gradio-app/gradio/pull/2448) +- Stops a gradio launch from hogging a port even after it's been killed [@aliabid94](https://github.com/aliabid94) in [PR 2453](https://github.com/gradio-app/gradio/pull/2453) +- Fix embedded interfaces on touch screen devices by [@aliabd](https://github.com/aliabd) in [PR 2457](https://github.com/gradio-app/gradio/pull/2457) +- Upload all demos to spaces by [@aliabd](https://github.com/aliabd) in [PR 2281](https://github.com/gradio-app/gradio/pull/2281) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.4.1 + +### New Features: + +###### 1. See Past and Upcoming Changes in the Release History 👀 + +You can now see gradio's release history directly on the website, and also keep track of upcoming changes. Just go [here](https://gradio.app/changelog/). + +![release-history](https://user-images.githubusercontent.com/9021060/193145458-3de699f7-7620-45de-aa73-a1c1b9b96257.gif) + +### Bug Fixes: + +1. Fix typo in guide image path by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2357](https://github.com/gradio-app/gradio/pull/2357) +2. Raise error if Blocks has duplicate component with same IDs by [@abidlabs](https://github.com/abidlabs) in [PR 2359](https://github.com/gradio-app/gradio/pull/2359) +3. Catch the permission exception on the audio component by [@Ian-GL](https://github.com/Ian-GL) in [PR 2330](https://github.com/gradio-app/gradio/pull/2330) +4. Fix image_classifier_interface_load demo by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2365](https://github.com/gradio-app/gradio/pull/2365) +5. Fix combining adjacent components without gaps by introducing `gr.Row(variant="compact")` by [@aliabid94](https://github.com/aliabid94) in [PR 2291](https://github.com/gradio-app/gradio/pull/2291) This comes with deprecation of the following arguments for `Component.style`: `round`, `margin`, `border`. +6. Fix audio streaming, which was previously choppy in [PR 2351](https://github.com/gradio-app/gradio/pull/2351). Big thanks to [@yannickfunk](https://github.com/yannickfunk) for the proposed solution. +7. Fix bug where new typeable slider doesn't respect the minimum and maximum values [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2380](https://github.com/gradio-app/gradio/pull/2380) + +### Documentation Changes: + +1. New Guide: Connecting to a Database 🗄️ + + A new guide by [@freddyaboulton](https://github.com/freddyaboulton) that explains how you can use Gradio to connect your app to a database. Read more [here](https://gradio.app/connecting_to_a_database/). + +2. New Guide: Running Background Tasks 🥷 + + A new guide by [@freddyaboulton](https://github.com/freddyaboulton) that explains how you can run background tasks from your gradio app. Read more [here](https://gradio.app/running_background_tasks/). + +3. Small fixes to docs for `Image` component by [@abidlabs](https://github.com/abidlabs) in [PR 2372](https://github.com/gradio-app/gradio/pull/2372) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Create a guide on how to connect an app to a database hosted on the cloud by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2341](https://github.com/gradio-app/gradio/pull/2341) +- Removes `analytics` dependency by [@abidlabs](https://github.com/abidlabs) in [PR 2347](https://github.com/gradio-app/gradio/pull/2347) +- Add guide on launching background tasks from your app by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2350](https://github.com/gradio-app/gradio/pull/2350) +- Fix typo in guide image path by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2357](https://github.com/gradio-app/gradio/pull/2357) +- Raise error if Blocks has duplicate component with same IDs by [@abidlabs](https://github.com/abidlabs) in [PR 2359](https://github.com/gradio-app/gradio/pull/2359) +- Hotfix: fix version back to 3.4 by [@abidlabs](https://github.com/abidlabs) in [PR 2361](https://github.com/gradio-app/gradio/pull/2361) +- Change version.txt to 3.4 instead of 3.4.0 by [@aliabd](https://github.com/aliabd) in [PR 2363](https://github.com/gradio-app/gradio/pull/2363) +- Catch the permission exception on the audio component by [@Ian-GL](https://github.com/Ian-GL) in [PR 2330](https://github.com/gradio-app/gradio/pull/2330) +- Fix image_classifier_interface_load demo by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2365](https://github.com/gradio-app/gradio/pull/2365) +- Small fixes to docs for `Image` component by [@abidlabs](https://github.com/abidlabs) in [PR 2372](https://github.com/gradio-app/gradio/pull/2372) +- Automated Release Notes by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2306](https://github.com/gradio-app/gradio/pull/2306) +- Fixed small typos in the docs [@julien-c](https://github.com/julien-c) in [PR 2373](https://github.com/gradio-app/gradio/pull/2373) +- Adds ability to disable pre/post-processing for examples [@abidlabs](https://github.com/abidlabs) in [PR 2383](https://github.com/gradio-app/gradio/pull/2383) +- Copy changelog file in website docker by [@aliabd](https://github.com/aliabd) in [PR 2384](https://github.com/gradio-app/gradio/pull/2384) +- Lets users provide a `gr.update()` dictionary even if post-processing is disabled [@abidlabs](https://github.com/abidlabs) in [PR 2385](https://github.com/gradio-app/gradio/pull/2385) +- Fix bug where errors would cause apps run in reload mode to hang forever by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2394](https://github.com/gradio-app/gradio/pull/2394) +- Fix bug where new typeable slider doesn't respect the minimum and maximum values [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2380](https://github.com/gradio-app/gradio/pull/2380) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.4 + +### New Features: + +###### 1. Gallery Captions 🖼️ + +You can now pass captions to images in the Gallery component. To do so you need to pass a {List} of (image, {str} caption) tuples. This is optional and the component also accepts just a list of the images. + +Here's an example: + +```python +import gradio as gr + +images_with_captions = [ + ("https://images.unsplash.com/photo-1551969014-7d2c4cddf0b6", "Cheetah by David Groves"), + ("https://images.unsplash.com/photo-1546182990-dffeafbe841d", "Lion by Francesco"), + ("https://images.unsplash.com/photo-1561731216-c3a4d99437d5", "Tiger by Mike Marrah") + ] + +with gr.Blocks() as demo: + gr.Gallery(value=images_with_captions) + +demo.launch() +``` + +gallery_captions + +###### 2. Type Values into the Slider 🔢 + +You can now type values directly on the Slider component! Here's what it looks like: + +![type-slider](https://user-images.githubusercontent.com/9021060/192399877-76b662a1-fede-4417-a932-fc15f0da7360.gif) + +###### 3. Better Sketching and Inpainting 🎨 + +We've made a lot of changes to our Image component so that it can support better sketching and inpainting. + +Now supports: + +- A standalone black-and-white sketch + +```python +import gradio as gr +demo = gr.Interface(lambda x: x, gr.Sketchpad(), gr.Image()) +demo.launch() +``` + +![bw](https://user-images.githubusercontent.com/9021060/192410264-b08632b5-7b2a-4f86-afb0-5760e7b474cf.gif) + +- A standalone color sketch + +```python +import gradio as gr +demo = gr.Interface(lambda x: x, gr.Paint(), gr.Image()) +demo.launch() +``` + +![color-sketch](https://user-images.githubusercontent.com/9021060/192410500-3c8c3e64-a5fd-4df2-a991-f0a5cef93728.gif) + +- An uploadable image with black-and-white or color sketching + +```python +import gradio as gr +demo = gr.Interface(lambda x: x, gr.Image(source='upload', tool='color-sketch'), gr.Image()) # for black and white, tool = 'sketch' +demo.launch() +``` + +![sketch-new](https://user-images.githubusercontent.com/9021060/192402422-e53cb7b6-024e-448c-87eb-d6a35a63c476.gif) + +- Webcam with black-and-white or color sketching + +```python +import gradio as gr +demo = gr.Interface(lambda x: x, gr.Image(source='webcam', tool='color-sketch'), gr.Image()) # for black and white, tool = 'sketch' +demo.launch() +``` + +![webcam-sketch](https://user-images.githubusercontent.com/9021060/192410820-0ffaf324-776e-4e1f-9de6-0fdbbf4940fa.gif) + +As well as other fixes + +### Bug Fixes: + +1. Fix bug where max concurrency count is not respected in queue by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2286](https://github.com/gradio-app/gradio/pull/2286) +2. fix : queue could be blocked by [@SkyTNT](https://github.com/SkyTNT) in [PR 2288](https://github.com/gradio-app/gradio/pull/2288) +3. Supports `gr.update()` in example caching by [@abidlabs](https://github.com/abidlabs) in [PR 2309](https://github.com/gradio-app/gradio/pull/2309) +4. Clipboard fix for iframes by [@abidlabs](https://github.com/abidlabs) in [PR 2321](https://github.com/gradio-app/gradio/pull/2321) +5. Fix: Dataframe column headers are reset when you add a new column by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2318](https://github.com/gradio-app/gradio/pull/2318) +6. Added support for URLs for Video, Audio, and Image by [@abidlabs](https://github.com/abidlabs) in [PR 2256](https://github.com/gradio-app/gradio/pull/2256) +7. Add documentation about how to create and use the Gradio FastAPI app by [@abidlabs](https://github.com/abidlabs) in [PR 2263](https://github.com/gradio-app/gradio/pull/2263) + +### Documentation Changes: + +1. Adding a Playground Tab to the Website by [@aliabd](https://github.com/aliabd) in [PR 1860](https://github.com/gradio-app/gradio/pull/1860) +2. Gradio for Tabular Data Science Workflows Guide by [@merveenoyan](https://github.com/merveenoyan) in [PR 2199](https://github.com/gradio-app/gradio/pull/2199) +3. Promotes `postprocess` and `preprocess` to documented parameters by [@abidlabs](https://github.com/abidlabs) in [PR 2293](https://github.com/gradio-app/gradio/pull/2293) +4. Update 2)key_features.md by [@voidxd](https://github.com/voidxd) in [PR 2326](https://github.com/gradio-app/gradio/pull/2326) +5. Add docs to blocks context postprocessing function by [@Ian-GL](https://github.com/Ian-GL) in [PR 2332](https://github.com/gradio-app/gradio/pull/2332) + +### Testing and Infrastructure Changes + +1. Website fixes and refactoring by [@aliabd](https://github.com/aliabd) in [PR 2280](https://github.com/gradio-app/gradio/pull/2280) +2. Don't deploy to spaces on release by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2313](https://github.com/gradio-app/gradio/pull/2313) + +### Full Changelog: + +- Website fixes and refactoring by [@aliabd](https://github.com/aliabd) in [PR 2280](https://github.com/gradio-app/gradio/pull/2280) +- Fix bug where max concurrency count is not respected in queue by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2286](https://github.com/gradio-app/gradio/pull/2286) +- Promotes `postprocess` and `preprocess` to documented parameters by [@abidlabs](https://github.com/abidlabs) in [PR 2293](https://github.com/gradio-app/gradio/pull/2293) +- Raise warning when trying to cache examples but not all inputs have examples by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2279](https://github.com/gradio-app/gradio/pull/2279) +- fix : queue could be blocked by [@SkyTNT](https://github.com/SkyTNT) in [PR 2288](https://github.com/gradio-app/gradio/pull/2288) +- Don't deploy to spaces on release by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2313](https://github.com/gradio-app/gradio/pull/2313) +- Supports `gr.update()` in example caching by [@abidlabs](https://github.com/abidlabs) in [PR 2309](https://github.com/gradio-app/gradio/pull/2309) +- Respect Upstream Queue when loading interfaces/blocks from Spaces by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2294](https://github.com/gradio-app/gradio/pull/2294) +- Clipboard fix for iframes by [@abidlabs](https://github.com/abidlabs) in [PR 2321](https://github.com/gradio-app/gradio/pull/2321) +- Sketching + Inpainting Capabilities to Gradio by [@abidlabs](https://github.com/abidlabs) in [PR 2144](https://github.com/gradio-app/gradio/pull/2144) +- Update 2)key_features.md by [@voidxd](https://github.com/voidxd) in [PR 2326](https://github.com/gradio-app/gradio/pull/2326) +- release 3.4b3 by [@abidlabs](https://github.com/abidlabs) in [PR 2328](https://github.com/gradio-app/gradio/pull/2328) +- Fix: Dataframe column headers are reset when you add a new column by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2318](https://github.com/gradio-app/gradio/pull/2318) +- Start queue when gradio is a sub application by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2319](https://github.com/gradio-app/gradio/pull/2319) +- Fix Web Tracker Script by [@aliabd](https://github.com/aliabd) in [PR 2308](https://github.com/gradio-app/gradio/pull/2308) +- Add docs to blocks context postprocessing function by [@Ian-GL](https://github.com/Ian-GL) in [PR 2332](https://github.com/gradio-app/gradio/pull/2332) +- Fix typo in iterator variable name in run_predict function by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2340](https://github.com/gradio-app/gradio/pull/2340) +- Add captions to galleries by [@aliabid94](https://github.com/aliabid94) in [PR 2284](https://github.com/gradio-app/gradio/pull/2284) +- Typeable value on gradio.Slider by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2329](https://github.com/gradio-app/gradio/pull/2329) + +### Contributors Shoutout: + +- [@SkyTNT](https://github.com/SkyTNT) made their first contribution in [PR 2288](https://github.com/gradio-app/gradio/pull/2288) +- [@voidxd](https://github.com/voidxd) made their first contribution in [PR 2326](https://github.com/gradio-app/gradio/pull/2326) + +## 3.3 + +### New Features: + +###### 1. Iterative Outputs ⏳ + +You can now create an iterative output simply by having your function return a generator! + +Here's (part of) an example that was used to generate the interface below it. [See full code](https://colab.research.google.com/drive/1m9bWS6B82CT7bw-m4L6AJR8za7fEK7Ov?usp=sharing). + +```python +def predict(steps, seed): + generator = torch.manual_seed(seed) + for i in range(1,steps): + yield pipeline(generator=generator, num_inference_steps=i)["sample"][0] +``` + +![example](https://user-images.githubusercontent.com/9021060/189086273-f5e7087d-71fa-4158-90a9-08e84da0421c.mp4) + +###### 2. Accordion Layout 🆕 + +This version of Gradio introduces a new layout component to Blocks: the Accordion. Wrap your elements in a neat, expandable layout that allows users to toggle them as needed. + +Usage: ([Read the docs](https://gradio.app/docs/#accordion)) + +```python +with gr.Accordion("open up"): +# components here +``` + +![accordion](https://user-images.githubusercontent.com/9021060/189088465-f0ffd7f0-fc6a-42dc-9249-11c5e1e0529b.gif) + +###### 3. Skops Integration 📈 + +Our new integration with [skops](https://huggingface.co/blog/skops) allows you to load tabular classification and regression models directly from the [hub](https://huggingface.co/models). + +Here's a classification example showing how quick it is to set up an interface for a [model](https://huggingface.co/scikit-learn/tabular-playground). + +```python +import gradio as gr +gr.Interface.load("models/scikit-learn/tabular-playground").launch() +``` + +![187936493-5c90c01d-a6dd-400f-aa42-833a096156a1](https://user-images.githubusercontent.com/9021060/189090519-328fbcb4-120b-43c8-aa54-d6fccfa6b7e8.png) + +### Bug Fixes: + +No changes to highlight. + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- safari fixes by [@pngwn](https://github.com/pngwn) in [PR 2138](https://github.com/gradio-app/gradio/pull/2138) +- Fix roundedness and form borders by [@aliabid94](https://github.com/aliabid94) in [PR 2147](https://github.com/gradio-app/gradio/pull/2147) +- Better processing of example data prior to creating dataset component by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2147](https://github.com/gradio-app/gradio/pull/2147) +- Show error on Connection drops by [@aliabid94](https://github.com/aliabid94) in [PR 2147](https://github.com/gradio-app/gradio/pull/2147) +- 3.2 release! by [@abidlabs](https://github.com/abidlabs) in [PR 2139](https://github.com/gradio-app/gradio/pull/2139) +- Fixed Named API Requests by [@abidlabs](https://github.com/abidlabs) in [PR 2151](https://github.com/gradio-app/gradio/pull/2151) +- Quick Fix: Cannot upload Model3D image after clearing it by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2168](https://github.com/gradio-app/gradio/pull/2168) +- Fixed misleading log when server_name is '0.0.0.0' by [@lamhoangtung](https://github.com/lamhoangtung) in [PR 2176](https://github.com/gradio-app/gradio/pull/2176) +- Keep embedded PngInfo metadata by [@cobryan05](https://github.com/cobryan05) in [PR 2170](https://github.com/gradio-app/gradio/pull/2170) +- Skops integration: Load tabular classification and regression models from the hub by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2126](https://github.com/gradio-app/gradio/pull/2126) +- Respect original filename when cached example files are downloaded by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2145](https://github.com/gradio-app/gradio/pull/2145) +- Add manual trigger to deploy to pypi by [@abidlabs](https://github.com/abidlabs) in [PR 2192](https://github.com/gradio-app/gradio/pull/2192) +- Fix bugs with gr.update by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2157](https://github.com/gradio-app/gradio/pull/2157) +- Make queue per app by [@aliabid94](https://github.com/aliabid94) in [PR 2193](https://github.com/gradio-app/gradio/pull/2193) +- Preserve Labels In Interpretation Components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2166](https://github.com/gradio-app/gradio/pull/2166) +- Quick Fix: Multiple file download not working by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2169](https://github.com/gradio-app/gradio/pull/2169) +- use correct MIME type for js-script file by [@daspartho](https://github.com/daspartho) in [PR 2200](https://github.com/gradio-app/gradio/pull/2200) +- Add accordion component by [@aliabid94](https://github.com/aliabid94) in [PR 2208](https://github.com/gradio-app/gradio/pull/2208) + +### Contributors Shoutout: + +- [@lamhoangtung](https://github.com/lamhoangtung) made their first contribution in [PR 2176](https://github.com/gradio-app/gradio/pull/2176) +- [@cobryan05](https://github.com/cobryan05) made their first contribution in [PR 2170](https://github.com/gradio-app/gradio/pull/2170) +- [@daspartho](https://github.com/daspartho) made their first contribution in [PR 2200](https://github.com/gradio-app/gradio/pull/2200) + +## 3.2 + +### New Features: + +###### 1. Improvements to Queuing 🥇 + +We've implemented a brand new queuing system based on **web sockets** instead of HTTP long polling. Among other things, this allows us to manage queue sizes better on Hugging Face Spaces. There are also additional queue-related parameters you can add: + +- Now supports concurrent workers (parallelization) + +```python +demo = gr.Interface(...) +demo.queue(concurrency_count=3) +demo.launch() +``` + +- Configure a maximum queue size + +```python +demo = gr.Interface(...) +demo.queue(max_size=100) +demo.launch() +``` + +- If a user closes their tab / browser, they leave the queue, which means the demo will run faster for everyone else + +###### 2. Fixes to Examples + +- Dataframe examples will render properly, and look much clearer in the UI: (thanks to PR #2125) + +![Screen Shot 2022-08-30 at 8 29 58 PM](https://user-images.githubusercontent.com/9021060/187586561-d915bafb-f968-4966-b9a2-ef41119692b2.png) + +- Image and Video thumbnails are cropped to look neater and more uniform: (thanks to PR #2109) + +![Screen Shot 2022-08-30 at 8 32 15 PM](https://user-images.githubusercontent.com/9021060/187586890-56e1e4f0-1b84-42d9-a82f-911772c41030.png) + +- Other fixes in PR #2131 and #2064 make it easier to design and use Examples + +###### 3. Component Fixes 🧱 + +- Specify the width and height of an image in its style tag (thanks to PR #2133) + +```python +components.Image().style(height=260, width=300) +``` + +- Automatic conversion of videos so they are playable in the browser (thanks to PR #2003). Gradio will check if a video's format is playable in the browser and, if it isn't, will automatically convert it to a format that is (mp4). +- Pass in a json filepath to the Label component (thanks to PR #2083) +- Randomize the default value of a Slider (thanks to PR #1935) + +![slider-random](https://user-images.githubusercontent.com/9021060/187596230-3db9697f-9f4d-42f5-9387-d77573513448.gif) + +- Improvements to State in PR #2100 + +###### 4. Ability to Randomize Input Sliders and Reload Data whenever the Page Loads + +- In some cases, you want to be able to show a different set of input data to every user as they load the page app. For example, you might want to randomize the value of a "seed" `Slider` input. Or you might want to show a `Textbox` with the current date. We now supporting passing _functions_ as the default value in input components. When you pass in a function, it gets **re-evaluated** every time someone loads the demo, allowing you to reload / change data for different users. + +Here's an example loading the current date time into an input Textbox: + +```python +import gradio as gr +import datetime + +with gr.Blocks() as demo: + gr.Textbox(datetime.datetime.now) + +demo.launch() +``` + +Note that we don't evaluate the function -- `datetime.datetime.now()` -- we pass in the function itself to get this behavior -- `datetime.datetime.now` + +Because randomizing the initial value of `Slider` is a common use case, we've added a `randomize` keyword argument you can use to randomize its initial value: + +```python +import gradio as gr +demo = gr.Interface(lambda x:x, gr.Slider(0, 10, randomize=True), "number") +demo.launch() +``` + +###### 5. New Guide 🖊️ + +- [Gradio and W&B Integration](https://gradio.app/Gradio_and_Wandb_Integration/) + +### Full Changelog: + +- Reset components to original state by setting value to None by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2044](https://github.com/gradio-app/gradio/pull/2044) +- Cleaning up the way data is processed for components by [@abidlabs](https://github.com/abidlabs) in [PR 1967](https://github.com/gradio-app/gradio/pull/1967) +- version 3.1.8b by [@abidlabs](https://github.com/abidlabs) in [PR 2063](https://github.com/gradio-app/gradio/pull/2063) +- Wandb guide by [@AK391](https://github.com/AK391) in [PR 1898](https://github.com/gradio-app/gradio/pull/1898) +- Add a flagging callback to save json files to a hugging face dataset by [@chrisemezue](https://github.com/chrisemezue) in [PR 1821](https://github.com/gradio-app/gradio/pull/1821) +- Add data science demos to landing page by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2067](https://github.com/gradio-app/gradio/pull/2067) +- Hide time series + xgboost demos by default by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2079](https://github.com/gradio-app/gradio/pull/2079) +- Encourage people to keep trying when queue full by [@apolinario](https://github.com/apolinario) in [PR 2076](https://github.com/gradio-app/gradio/pull/2076) +- Updated our analytics on creation of Blocks/Interface by [@abidlabs](https://github.com/abidlabs) in [PR 2082](https://github.com/gradio-app/gradio/pull/2082) +- `Label` component now accepts file paths to `.json` files by [@abidlabs](https://github.com/abidlabs) in [PR 2083](https://github.com/gradio-app/gradio/pull/2083) +- Fix issues related to demos in Spaces by [@abidlabs](https://github.com/abidlabs) in [PR 2086](https://github.com/gradio-app/gradio/pull/2086) +- Fix TimeSeries examples not properly displayed in UI by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2064](https://github.com/gradio-app/gradio/pull/2064) +- Fix infinite requests when doing tab item select by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2070](https://github.com/gradio-app/gradio/pull/2070) +- Accept deprecated `file` route as well by [@abidlabs](https://github.com/abidlabs) in [PR 2099](https://github.com/gradio-app/gradio/pull/2099) +- Allow frontend method execution on Block.load event by [@codedealer](https://github.com/codedealer) in [PR 2108](https://github.com/gradio-app/gradio/pull/2108) +- Improvements to `State` by [@abidlabs](https://github.com/abidlabs) in [PR 2100](https://github.com/gradio-app/gradio/pull/2100) +- Catch IndexError, KeyError in video_is_playable by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2113](https://github.com/gradio-app/gradio/pull/2113) +- Fix: Download button does not respect the filepath returned by the function by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2073](https://github.com/gradio-app/gradio/pull/2073) +- Refactoring Layout: Adding column widths, forms, and more. by [@aliabid94](https://github.com/aliabid94) in [PR 2097](https://github.com/gradio-app/gradio/pull/2097) +- Update CONTRIBUTING.md by [@abidlabs](https://github.com/abidlabs) in [PR 2118](https://github.com/gradio-app/gradio/pull/2118) +- 2092 df ex by [@pngwn](https://github.com/pngwn) in [PR 2125](https://github.com/gradio-app/gradio/pull/2125) +- feat(samples table/gallery): Crop thumbs to square by [@ronvoluted](https://github.com/ronvoluted) in [PR 2109](https://github.com/gradio-app/gradio/pull/2109) +- Some enhancements to `gr.Examples` by [@abidlabs](https://github.com/abidlabs) in [PR 2131](https://github.com/gradio-app/gradio/pull/2131) +- Image size fix by [@aliabid94](https://github.com/aliabid94) in [PR 2133](https://github.com/gradio-app/gradio/pull/2133) + +### Contributors Shoutout: + +- [@chrisemezue](https://github.com/chrisemezue) made their first contribution in [PR 1821](https://github.com/gradio-app/gradio/pull/1821) +- [@apolinario](https://github.com/apolinario) made their first contribution in [PR 2076](https://github.com/gradio-app/gradio/pull/2076) +- [@codedealer](https://github.com/codedealer) made their first contribution in [PR 2108](https://github.com/gradio-app/gradio/pull/2108) + +## 3.1 + +### New Features: + +###### 1. Embedding Demos on Any Website 💻 + +With PR #1444, Gradio is now distributed as a web component. This means demos can be natively embedded on websites. You'll just need to add two lines: one to load the gradio javascript, and one to link to the demos backend. + +Here's a simple example that embeds the demo from a Hugging Face space: + +```html + + +``` + +But you can also embed demos that are running anywhere, you just need to link the demo to `src` instead of `space`. In fact, all the demos on the gradio website are embedded this way: + +Screen Shot 2022-07-14 at 2 41 44 PM + +Read more in the [Embedding Gradio Demos](https://gradio.app/embedding_gradio_demos) guide. + +###### 2. Reload Mode 👨‍💻 + +Reload mode helps developers create gradio demos faster by automatically reloading the demo whenever the code changes. It can support development on Python IDEs (VS Code, PyCharm, etc), the terminal, as well as Jupyter notebooks. + +If your demo code is in a script named `app.py`, instead of running `python app.py` you can now run `gradio app.py` and that will launch the demo in reload mode: + +```bash +Launching in reload mode on: http://127.0.0.1:7860 (Press CTRL+C to quit) +Watching... +WARNING: The --reload flag should not be used in production on Windows. +``` + +If you're working from a Jupyter or Colab Notebook, use these magic commands instead: `%load_ext gradio` when you import gradio, and `%%blocks` in the top of the cell with the demo code. Here's an example that shows how much faster the development becomes: + +![Blocks](https://user-images.githubusercontent.com/9021060/178986488-ed378cc8-5141-4330-ba41-672b676863d0.gif) + +###### 3. Inpainting Support on `gr.Image()` 🎨 + +We updated the Image component to add support for inpainting demos. It works by adding `tool="sketch"` as a parameter, that passes both an image and a sketchable mask to your prediction function. + +Here's an example from the [LAMA space](https://huggingface.co/spaces/akhaliq/lama): + +![FXApVlFVsAALSD-](https://user-images.githubusercontent.com/9021060/178989479-549867c8-7fb0-436a-a97d-1e91c9f5e611.jpeg) + +###### 4. Markdown and HTML support in Dataframes 🔢 + +We upgraded the Dataframe component in PR #1684 to support rendering Markdown and HTML inside the cells. + +This means you can build Dataframes that look like the following: + +![image (8)](https://user-images.githubusercontent.com/9021060/178991233-41cb07a5-e7a3-433e-89b8-319bc78eb9c2.png) + +###### 5. `gr.Examples()` for Blocks 🧱 + +We've added the `gr.Examples` component helper to allow you to add examples to any Blocks demo. This class is a wrapper over the `gr.Dataset` component. + +Screen Shot 2022-07-14 at 2 23 50 PM + +gr.Examples takes two required parameters: + +- `examples` which takes in a nested list +- `inputs` which takes in a component or list of components + +You can read more in the [Examples docs](https://gradio.app/docs/#examples) or the [Adding Examples to your Demos guide](https://gradio.app/adding_examples_to_your_app/). + +###### 6. Fixes to Audio Streaming + +With [PR 1828](https://github.com/gradio-app/gradio/pull/1828) we now hide the status loading animation, as well as remove the echo in streaming. Check out the [stream_audio](https://github.com/gradio-app/gradio/blob/main/demo/stream_audio/run.py) demo for more or read through our [Real Time Speech Recognition](https://gradio.app/real_time_speech_recognition/) guide. + +Screen Shot 2022-07-19 at 6 02 35 PM + +### Full Changelog: + +- File component: list multiple files and allow for download #1446 by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1681](https://github.com/gradio-app/gradio/pull/1681) +- Add ColorPicker to docs by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1768](https://github.com/gradio-app/gradio/pull/1768) +- Mock out requests in TestRequest unit tests by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1794](https://github.com/gradio-app/gradio/pull/1794) +- Add requirements.txt and test_files to source dist by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1817](https://github.com/gradio-app/gradio/pull/1817) +- refactor: f-string for tunneling.py by [@nhankiet](https://github.com/nhankiet) in [PR 1819](https://github.com/gradio-app/gradio/pull/1819) +- Miscellaneous formatting improvements to website by [@aliabd](https://github.com/aliabd) in [PR 1754](https://github.com/gradio-app/gradio/pull/1754) +- `integrate()` method moved to `Blocks` by [@abidlabs](https://github.com/abidlabs) in [PR 1776](https://github.com/gradio-app/gradio/pull/1776) +- Add python-3.7 tests by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1818](https://github.com/gradio-app/gradio/pull/1818) +- Copy test dir in website dockers by [@aliabd](https://github.com/aliabd) in [PR 1827](https://github.com/gradio-app/gradio/pull/1827) +- Add info to docs on how to set default values for components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1788](https://github.com/gradio-app/gradio/pull/1788) +- Embedding Components on Docs by [@aliabd](https://github.com/aliabd) in [PR 1726](https://github.com/gradio-app/gradio/pull/1726) +- Remove usage of deprecated gr.inputs and gr.outputs from website by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1796](https://github.com/gradio-app/gradio/pull/1796) +- Some cleanups to the docs page by [@abidlabs](https://github.com/abidlabs) in [PR 1822](https://github.com/gradio-app/gradio/pull/1822) + +### Contributors Shoutout: + +- [@nhankiet](https://github.com/nhankiet) made their first contribution in [PR 1819](https://github.com/gradio-app/gradio/pull/1819) + +## 3.0 + +###### 🔥 Gradio 3.0 is the biggest update to the library, ever. + +### New Features: + +###### 1. Blocks 🧱 + +Blocks is a new, low-level API that allows you to have full control over the data flows and layout of your application. It allows you to build very complex, multi-step applications. For example, you might want to: + +- Group together related demos as multiple tabs in one web app +- Change the layout of your demo instead of just having all of the inputs on the left and outputs on the right +- Have multi-step interfaces, in which the output of one model becomes the input to the next model, or have more flexible data flows in general +- Change a component's properties (for example, the choices in a Dropdown) or its visibility based on user input + +Here's a simple example that creates the demo below it: + +```python +import gradio as gr + +def update(name): + return f"Welcome to Gradio, {name}!" + +demo = gr.Blocks() + +with 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(fn=update, + inputs=inp, + outputs=out) + +demo.launch() +``` + +![hello-blocks](https://user-images.githubusercontent.com/9021060/168684108-78cbd24b-e6bd-4a04-a8d9-20d535203434.gif) + +Read our [Introduction to Blocks](http://gradio.app/introduction_to_blocks/) guide for more, and join the 🎈 [Gradio Blocks Party](https://huggingface.co/spaces/Gradio-Blocks/README)! + +###### 2. Our Revamped Design 🎨 + +We've upgraded our design across the entire library: from components, and layouts all the way to dark mode. + +![kitchen_sink](https://user-images.githubusercontent.com/9021060/168686333-7a6e3096-3e23-4309-abf2-5cd7736e0463.gif) + +###### 3. A New Website 💻 + +We've upgraded [gradio.app](https://gradio.app) to make it cleaner, faster and easier to use. Our docs now come with components and demos embedded directly on the page. So you can quickly get up to speed with what you're looking for. + +![website](https://user-images.githubusercontent.com/9021060/168687191-10d6a3bd-101f-423a-8193-48f47a5e077d.gif) + +###### 4. New Components: Model3D, Dataset, and More.. + +We've introduced a lot of new components in `3.0`, including `Model3D`, `Dataset`, `Markdown`, `Button` and `Gallery`. You can find all the components and play around with them [here](https://gradio.app/docs/#components). + +![Model3d](https://user-images.githubusercontent.com/9021060/168689062-6ad77151-8cc5-467d-916c-f7c78e52ec0c.gif) + +### Full Changelog: + +- Gradio dash fe by [@pngwn](https://github.com/pngwn) in [PR 807](https://github.com/gradio-app/gradio/pull/807) +- Blocks components by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 765](https://github.com/gradio-app/gradio/pull/765) +- Blocks components V2 by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 843](https://github.com/gradio-app/gradio/pull/843) +- Blocks-Backend-Events by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 844](https://github.com/gradio-app/gradio/pull/844) +- Interfaces from Blocks by [@aliabid94](https://github.com/aliabid94) in [PR 849](https://github.com/gradio-app/gradio/pull/849) +- Blocks dev by [@aliabid94](https://github.com/aliabid94) in [PR 853](https://github.com/gradio-app/gradio/pull/853) +- Started updating demos to use the new `gradio.components` syntax by [@abidlabs](https://github.com/abidlabs) in [PR 848](https://github.com/gradio-app/gradio/pull/848) +- add test infra + add browser tests to CI by [@pngwn](https://github.com/pngwn) in [PR 852](https://github.com/gradio-app/gradio/pull/852) +- 854 textbox by [@pngwn](https://github.com/pngwn) in [PR 859](https://github.com/gradio-app/gradio/pull/859) +- Getting old Python unit tests to pass on `blocks-dev` by [@abidlabs](https://github.com/abidlabs) in [PR 861](https://github.com/gradio-app/gradio/pull/861) +- initialise chatbot with empty array of messages by [@pngwn](https://github.com/pngwn) in [PR 867](https://github.com/gradio-app/gradio/pull/867) +- add test for output to input by [@pngwn](https://github.com/pngwn) in [PR 866](https://github.com/gradio-app/gradio/pull/866) +- More Interface -> Blocks features by [@aliabid94](https://github.com/aliabid94) in [PR 864](https://github.com/gradio-app/gradio/pull/864) +- Fixing external.py in blocks-dev to reflect the new HF Spaces paths by [@abidlabs](https://github.com/abidlabs) in [PR 879](https://github.com/gradio-app/gradio/pull/879) +- backend_default_value_refactoring by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 871](https://github.com/gradio-app/gradio/pull/871) +- fix default_value by [@pngwn](https://github.com/pngwn) in [PR 869](https://github.com/gradio-app/gradio/pull/869) +- fix buttons by [@aliabid94](https://github.com/aliabid94) in [PR 883](https://github.com/gradio-app/gradio/pull/883) +- Checking and updating more demos to use 3.0 syntax by [@abidlabs](https://github.com/abidlabs) in [PR 892](https://github.com/gradio-app/gradio/pull/892) +- Blocks Tests by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 902](https://github.com/gradio-app/gradio/pull/902) +- Interface fix by [@pngwn](https://github.com/pngwn) in [PR 901](https://github.com/gradio-app/gradio/pull/901) +- Quick fix: Issue 893 by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 907](https://github.com/gradio-app/gradio/pull/907) +- 3d Image Component by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 775](https://github.com/gradio-app/gradio/pull/775) +- fix endpoint url in prod by [@pngwn](https://github.com/pngwn) in [PR 911](https://github.com/gradio-app/gradio/pull/911) +- rename Model3d to Image3D by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 912](https://github.com/gradio-app/gradio/pull/912) +- update pypi to 2.9.1 by [@abidlabs](https://github.com/abidlabs) in [PR 916](https://github.com/gradio-app/gradio/pull/916) +- blocks-with-fix by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 917](https://github.com/gradio-app/gradio/pull/917) +- Restore Interpretation, Live, Auth, Queueing by [@aliabid94](https://github.com/aliabid94) in [PR 915](https://github.com/gradio-app/gradio/pull/915) +- Allow `Blocks` instances to be used like a `Block` in other `Blocks` by [@abidlabs](https://github.com/abidlabs) in [PR 919](https://github.com/gradio-app/gradio/pull/919) +- Redesign 1 by [@pngwn](https://github.com/pngwn) in [PR 918](https://github.com/gradio-app/gradio/pull/918) +- blocks-components-tests by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 904](https://github.com/gradio-app/gradio/pull/904) +- fix unit + browser tests by [@pngwn](https://github.com/pngwn) in [PR 926](https://github.com/gradio-app/gradio/pull/926) +- blocks-move-test-data by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 927](https://github.com/gradio-app/gradio/pull/927) +- remove debounce from form inputs by [@pngwn](https://github.com/pngwn) in [PR 932](https://github.com/gradio-app/gradio/pull/932) +- reimplement webcam video by [@pngwn](https://github.com/pngwn) in [PR 928](https://github.com/gradio-app/gradio/pull/928) +- blocks-move-test-data by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 941](https://github.com/gradio-app/gradio/pull/941) +- allow audio components to take a string value by [@pngwn](https://github.com/pngwn) in [PR 930](https://github.com/gradio-app/gradio/pull/930) +- static mode for textbox by [@pngwn](https://github.com/pngwn) in [PR 929](https://github.com/gradio-app/gradio/pull/929) +- fix file upload text by [@pngwn](https://github.com/pngwn) in [PR 931](https://github.com/gradio-app/gradio/pull/931) +- tabbed-interface-rewritten by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 958](https://github.com/gradio-app/gradio/pull/958) +- Gan demo fix by [@abidlabs](https://github.com/abidlabs) in [PR 965](https://github.com/gradio-app/gradio/pull/965) +- Blocks analytics by [@abidlabs](https://github.com/abidlabs) in [PR 947](https://github.com/gradio-app/gradio/pull/947) +- Blocks page load by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 963](https://github.com/gradio-app/gradio/pull/963) +- add frontend for page load events by [@pngwn](https://github.com/pngwn) in [PR 967](https://github.com/gradio-app/gradio/pull/967) +- fix i18n and some tweaks by [@pngwn](https://github.com/pngwn) in [PR 966](https://github.com/gradio-app/gradio/pull/966) +- add jinja2 to reqs by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 969](https://github.com/gradio-app/gradio/pull/969) +- Cleaning up `Launchable()` by [@abidlabs](https://github.com/abidlabs) in [PR 968](https://github.com/gradio-app/gradio/pull/968) +- Fix #944 by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 971](https://github.com/gradio-app/gradio/pull/971) +- New Blocks Demo: neural instrument cloning by [@abidlabs](https://github.com/abidlabs) in [PR 975](https://github.com/gradio-app/gradio/pull/975) +- Add huggingface_hub client library by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 973](https://github.com/gradio-app/gradio/pull/973) +- State and variables by [@aliabid94](https://github.com/aliabid94) in [PR 977](https://github.com/gradio-app/gradio/pull/977) +- update-components by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 986](https://github.com/gradio-app/gradio/pull/986) +- ensure dataframe updates as expected by [@pngwn](https://github.com/pngwn) in [PR 981](https://github.com/gradio-app/gradio/pull/981) +- test-guideline by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 990](https://github.com/gradio-app/gradio/pull/990) +- Issue #785: add footer by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 972](https://github.com/gradio-app/gradio/pull/972) +- indentation fix by [@abidlabs](https://github.com/abidlabs) in [PR 993](https://github.com/gradio-app/gradio/pull/993) +- missing quote by [@aliabd](https://github.com/aliabd) in [PR 996](https://github.com/gradio-app/gradio/pull/996) +- added interactive parameter to components by [@abidlabs](https://github.com/abidlabs) in [PR 992](https://github.com/gradio-app/gradio/pull/992) +- custom-components by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 985](https://github.com/gradio-app/gradio/pull/985) +- Refactor component shortcuts by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 995](https://github.com/gradio-app/gradio/pull/995) +- Plot Component by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 805](https://github.com/gradio-app/gradio/pull/805) +- updated PyPi version to 2.9.2 by [@abidlabs](https://github.com/abidlabs) in [PR 1002](https://github.com/gradio-app/gradio/pull/1002) +- Release 2.9.3 by [@abidlabs](https://github.com/abidlabs) in [PR 1003](https://github.com/gradio-app/gradio/pull/1003) +- Image3D Examples Fix by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1001](https://github.com/gradio-app/gradio/pull/1001) +- release 2.9.4 by [@abidlabs](https://github.com/abidlabs) in [PR 1006](https://github.com/gradio-app/gradio/pull/1006) +- templates import hotfix by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1008](https://github.com/gradio-app/gradio/pull/1008) +- Progress indicator bar by [@aliabid94](https://github.com/aliabid94) in [PR 997](https://github.com/gradio-app/gradio/pull/997) +- Fixed image input for absolute path by [@JefferyChiang](https://github.com/JefferyChiang) in [PR 1004](https://github.com/gradio-app/gradio/pull/1004) +- Model3D + Plot Components by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1010](https://github.com/gradio-app/gradio/pull/1010) +- Gradio Guides: Creating CryptoPunks with GANs by [@NimaBoscarino](https://github.com/NimaBoscarino) in [PR 1000](https://github.com/gradio-app/gradio/pull/1000) +- [BIG PR] Gradio blocks & redesigned components by [@abidlabs](https://github.com/abidlabs) in [PR 880](https://github.com/gradio-app/gradio/pull/880) +- fixed failing test on main by [@abidlabs](https://github.com/abidlabs) in [PR 1023](https://github.com/gradio-app/gradio/pull/1023) +- Use smaller ASR model in external test by [@abidlabs](https://github.com/abidlabs) in [PR 1024](https://github.com/gradio-app/gradio/pull/1024) +- updated PyPi version to 2.9.0b by [@abidlabs](https://github.com/abidlabs) in [PR 1026](https://github.com/gradio-app/gradio/pull/1026) +- Fixing import issues so that the package successfully installs on colab notebooks by [@abidlabs](https://github.com/abidlabs) in [PR 1027](https://github.com/gradio-app/gradio/pull/1027) +- Update website tracker slackbot by [@aliabd](https://github.com/aliabd) in [PR 1037](https://github.com/gradio-app/gradio/pull/1037) +- textbox-autoheight by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1009](https://github.com/gradio-app/gradio/pull/1009) +- Model3D Examples fixes by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1035](https://github.com/gradio-app/gradio/pull/1035) +- GAN Gradio Guide: Adjustments to iframe heights by [@NimaBoscarino](https://github.com/NimaBoscarino) in [PR 1042](https://github.com/gradio-app/gradio/pull/1042) +- added better default labels to form components by [@abidlabs](https://github.com/abidlabs) in [PR 1040](https://github.com/gradio-app/gradio/pull/1040) +- Slackbot web tracker fix by [@aliabd](https://github.com/aliabd) in [PR 1043](https://github.com/gradio-app/gradio/pull/1043) +- Plot fixes by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1044](https://github.com/gradio-app/gradio/pull/1044) +- Small fixes to the demos by [@abidlabs](https://github.com/abidlabs) in [PR 1030](https://github.com/gradio-app/gradio/pull/1030) +- fixing demo issue with website by [@aliabd](https://github.com/aliabd) in [PR 1047](https://github.com/gradio-app/gradio/pull/1047) +- [hotfix] HighlightedText by [@aliabid94](https://github.com/aliabid94) in [PR 1046](https://github.com/gradio-app/gradio/pull/1046) +- Update text by [@ronvoluted](https://github.com/ronvoluted) in [PR 1050](https://github.com/gradio-app/gradio/pull/1050) +- Update CONTRIBUTING.md by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1052](https://github.com/gradio-app/gradio/pull/1052) +- fix(ui): Increase contrast for footer by [@ronvoluted](https://github.com/ronvoluted) in [PR 1048](https://github.com/gradio-app/gradio/pull/1048) +- UI design update by [@gary149](https://github.com/gary149) in [PR 1041](https://github.com/gradio-app/gradio/pull/1041) +- updated PyPi version to 2.9.0b8 by [@abidlabs](https://github.com/abidlabs) in [PR 1059](https://github.com/gradio-app/gradio/pull/1059) +- Running, testing, and fixing demos by [@abidlabs](https://github.com/abidlabs) in [PR 1060](https://github.com/gradio-app/gradio/pull/1060) +- Form layout by [@pngwn](https://github.com/pngwn) in [PR 1054](https://github.com/gradio-app/gradio/pull/1054) +- inputless-interfaces by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1038](https://github.com/gradio-app/gradio/pull/1038) +- Update PULL_REQUEST_TEMPLATE.md by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1068](https://github.com/gradio-app/gradio/pull/1068) +- Upgrading node memory to 4gb in website Docker by [@aliabd](https://github.com/aliabd) in [PR 1069](https://github.com/gradio-app/gradio/pull/1069) +- Website reload error by [@aliabd](https://github.com/aliabd) in [PR 1079](https://github.com/gradio-app/gradio/pull/1079) +- fixed favicon issue by [@abidlabs](https://github.com/abidlabs) in [PR 1064](https://github.com/gradio-app/gradio/pull/1064) +- remove-queue-from-events by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1056](https://github.com/gradio-app/gradio/pull/1056) +- Enable vertex colors for OBJs files by [@radames](https://github.com/radames) in [PR 1074](https://github.com/gradio-app/gradio/pull/1074) +- Dark text by [@ronvoluted](https://github.com/ronvoluted) in [PR 1049](https://github.com/gradio-app/gradio/pull/1049) +- Scroll to output by [@pngwn](https://github.com/pngwn) in [PR 1077](https://github.com/gradio-app/gradio/pull/1077) +- Explicitly list pnpm version 6 in contributing guide by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1085](https://github.com/gradio-app/gradio/pull/1085) +- hotfix for encrypt issue by [@abidlabs](https://github.com/abidlabs) in [PR 1096](https://github.com/gradio-app/gradio/pull/1096) +- Release 2.9b9 by [@abidlabs](https://github.com/abidlabs) in [PR 1098](https://github.com/gradio-app/gradio/pull/1098) +- tweak node circleci settings by [@pngwn](https://github.com/pngwn) in [PR 1091](https://github.com/gradio-app/gradio/pull/1091) +- Website Reload Error by [@aliabd](https://github.com/aliabd) in [PR 1099](https://github.com/gradio-app/gradio/pull/1099) +- Website Reload: README in demos docker by [@aliabd](https://github.com/aliabd) in [PR 1100](https://github.com/gradio-app/gradio/pull/1100) +- Flagging fixes by [@abidlabs](https://github.com/abidlabs) in [PR 1081](https://github.com/gradio-app/gradio/pull/1081) +- Backend for optional labels by [@abidlabs](https://github.com/abidlabs) in [PR 1080](https://github.com/gradio-app/gradio/pull/1080) +- Optional labels fe by [@pngwn](https://github.com/pngwn) in [PR 1105](https://github.com/gradio-app/gradio/pull/1105) +- clean-deprecated-parameters by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1090](https://github.com/gradio-app/gradio/pull/1090) +- Blocks rendering fix by [@abidlabs](https://github.com/abidlabs) in [PR 1102](https://github.com/gradio-app/gradio/pull/1102) +- Redos #1106 by [@abidlabs](https://github.com/abidlabs) in [PR 1112](https://github.com/gradio-app/gradio/pull/1112) +- Interface types: handle input-only, output-only, and unified interfaces by [@abidlabs](https://github.com/abidlabs) in [PR 1108](https://github.com/gradio-app/gradio/pull/1108) +- Hotfix + New pypi release 2.9b11 by [@abidlabs](https://github.com/abidlabs) in [PR 1118](https://github.com/gradio-app/gradio/pull/1118) +- issue-checkbox by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1122](https://github.com/gradio-app/gradio/pull/1122) +- issue-checkbox-hotfix by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1127](https://github.com/gradio-app/gradio/pull/1127) +- Fix demos in website by [@aliabd](https://github.com/aliabd) in [PR 1130](https://github.com/gradio-app/gradio/pull/1130) +- Guide for Gradio ONNX model zoo on Huggingface by [@AK391](https://github.com/AK391) in [PR 1073](https://github.com/gradio-app/gradio/pull/1073) +- ONNX guide fixes by [@aliabd](https://github.com/aliabd) in [PR 1131](https://github.com/gradio-app/gradio/pull/1131) +- Stacked form inputs css by [@gary149](https://github.com/gary149) in [PR 1134](https://github.com/gradio-app/gradio/pull/1134) +- made default value in textbox empty string by [@abidlabs](https://github.com/abidlabs) in [PR 1135](https://github.com/gradio-app/gradio/pull/1135) +- Examples UI by [@gary149](https://github.com/gary149) in [PR 1121](https://github.com/gradio-app/gradio/pull/1121) +- Chatbot custom color support by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1092](https://github.com/gradio-app/gradio/pull/1092) +- highlighted text colors by [@pngwn](https://github.com/pngwn) in [PR 1119](https://github.com/gradio-app/gradio/pull/1119) +- pin to pnpm 6 for now by [@pngwn](https://github.com/pngwn) in [PR 1147](https://github.com/gradio-app/gradio/pull/1147) +- Restore queue in Blocks by [@aliabid94](https://github.com/aliabid94) in [PR 1137](https://github.com/gradio-app/gradio/pull/1137) +- add select event for tabitems by [@pngwn](https://github.com/pngwn) in [PR 1154](https://github.com/gradio-app/gradio/pull/1154) +- max_lines + autoheight for textbox by [@pngwn](https://github.com/pngwn) in [PR 1153](https://github.com/gradio-app/gradio/pull/1153) +- use color palette for chatbot by [@pngwn](https://github.com/pngwn) in [PR 1152](https://github.com/gradio-app/gradio/pull/1152) +- Timeseries improvements by [@pngwn](https://github.com/pngwn) in [PR 1149](https://github.com/gradio-app/gradio/pull/1149) +- move styling for interface panels to frontend by [@pngwn](https://github.com/pngwn) in [PR 1146](https://github.com/gradio-app/gradio/pull/1146) +- html tweaks by [@pngwn](https://github.com/pngwn) in [PR 1145](https://github.com/gradio-app/gradio/pull/1145) +- Issue #768: Support passing none to resize and crop image by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1144](https://github.com/gradio-app/gradio/pull/1144) +- image gallery component + img css by [@aliabid94](https://github.com/aliabid94) in [PR 1140](https://github.com/gradio-app/gradio/pull/1140) +- networking tweak by [@abidlabs](https://github.com/abidlabs) in [PR 1143](https://github.com/gradio-app/gradio/pull/1143) +- Allow enabling queue per event listener by [@aliabid94](https://github.com/aliabid94) in [PR 1155](https://github.com/gradio-app/gradio/pull/1155) +- config hotfix and v. 2.9b23 by [@abidlabs](https://github.com/abidlabs) in [PR 1158](https://github.com/gradio-app/gradio/pull/1158) +- Custom JS calls by [@aliabid94](https://github.com/aliabid94) in [PR 1082](https://github.com/gradio-app/gradio/pull/1082) +- Small fixes: queue default fix, ffmpeg installation message by [@abidlabs](https://github.com/abidlabs) in [PR 1159](https://github.com/gradio-app/gradio/pull/1159) +- formatting by [@abidlabs](https://github.com/abidlabs) in [PR 1161](https://github.com/gradio-app/gradio/pull/1161) +- enable flex grow for gr-box by [@radames](https://github.com/radames) in [PR 1165](https://github.com/gradio-app/gradio/pull/1165) +- 1148 loading by [@pngwn](https://github.com/pngwn) in [PR 1164](https://github.com/gradio-app/gradio/pull/1164) +- Put enable_queue kwarg back in launch() by [@aliabid94](https://github.com/aliabid94) in [PR 1167](https://github.com/gradio-app/gradio/pull/1167) +- A few small fixes by [@abidlabs](https://github.com/abidlabs) in [PR 1171](https://github.com/gradio-app/gradio/pull/1171) +- Hotfix for dropdown component by [@abidlabs](https://github.com/abidlabs) in [PR 1172](https://github.com/gradio-app/gradio/pull/1172) +- use secondary buttons in interface by [@pngwn](https://github.com/pngwn) in [PR 1173](https://github.com/gradio-app/gradio/pull/1173) +- 1183 component height by [@pngwn](https://github.com/pngwn) in [PR 1185](https://github.com/gradio-app/gradio/pull/1185) +- 962 dataframe by [@pngwn](https://github.com/pngwn) in [PR 1186](https://github.com/gradio-app/gradio/pull/1186) +- update-contributing by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1188](https://github.com/gradio-app/gradio/pull/1188) +- Table tweaks by [@pngwn](https://github.com/pngwn) in [PR 1195](https://github.com/gradio-app/gradio/pull/1195) +- wrap tab content in column by [@pngwn](https://github.com/pngwn) in [PR 1200](https://github.com/gradio-app/gradio/pull/1200) +- WIP: Add dark mode support by [@gary149](https://github.com/gary149) in [PR 1187](https://github.com/gradio-app/gradio/pull/1187) +- Restored /api/predict/ endpoint for Interfaces by [@abidlabs](https://github.com/abidlabs) in [PR 1199](https://github.com/gradio-app/gradio/pull/1199) +- hltext-label by [@pngwn](https://github.com/pngwn) in [PR 1204](https://github.com/gradio-app/gradio/pull/1204) +- add copy functionality to json by [@pngwn](https://github.com/pngwn) in [PR 1205](https://github.com/gradio-app/gradio/pull/1205) +- Update component config by [@aliabid94](https://github.com/aliabid94) in [PR 1089](https://github.com/gradio-app/gradio/pull/1089) +- fix placeholder prompt by [@pngwn](https://github.com/pngwn) in [PR 1215](https://github.com/gradio-app/gradio/pull/1215) +- ensure webcam video value is propagated correctly by [@pngwn](https://github.com/pngwn) in [PR 1218](https://github.com/gradio-app/gradio/pull/1218) +- Automatic word-break in highlighted text, combine_adjacent support by [@aliabid94](https://github.com/aliabid94) in [PR 1209](https://github.com/gradio-app/gradio/pull/1209) +- async-function-support by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1190](https://github.com/gradio-app/gradio/pull/1190) +- Sharing fix for assets by [@aliabid94](https://github.com/aliabid94) in [PR 1208](https://github.com/gradio-app/gradio/pull/1208) +- Hotfixes for course demos by [@abidlabs](https://github.com/abidlabs) in [PR 1222](https://github.com/gradio-app/gradio/pull/1222) +- Allow Custom CSS by [@aliabid94](https://github.com/aliabid94) in [PR 1170](https://github.com/gradio-app/gradio/pull/1170) +- share-hotfix by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1226](https://github.com/gradio-app/gradio/pull/1226) +- tweaks by [@pngwn](https://github.com/pngwn) in [PR 1229](https://github.com/gradio-app/gradio/pull/1229) +- white space for class concatenation by [@radames](https://github.com/radames) in [PR 1228](https://github.com/gradio-app/gradio/pull/1228) +- Tweaks by [@pngwn](https://github.com/pngwn) in [PR 1230](https://github.com/gradio-app/gradio/pull/1230) +- css tweaks by [@pngwn](https://github.com/pngwn) in [PR 1235](https://github.com/gradio-app/gradio/pull/1235) +- ensure defaults height match for media inputs by [@pngwn](https://github.com/pngwn) in [PR 1236](https://github.com/gradio-app/gradio/pull/1236) +- Default Label label value by [@radames](https://github.com/radames) in [PR 1239](https://github.com/gradio-app/gradio/pull/1239) +- update-shortcut-syntax by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1234](https://github.com/gradio-app/gradio/pull/1234) +- Update version.txt by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1244](https://github.com/gradio-app/gradio/pull/1244) +- Layout bugs by [@pngwn](https://github.com/pngwn) in [PR 1246](https://github.com/gradio-app/gradio/pull/1246) +- Update demo by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1253](https://github.com/gradio-app/gradio/pull/1253) +- Button default name by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1243](https://github.com/gradio-app/gradio/pull/1243) +- Labels spacing by [@gary149](https://github.com/gary149) in [PR 1254](https://github.com/gradio-app/gradio/pull/1254) +- add global loader for gradio app by [@pngwn](https://github.com/pngwn) in [PR 1251](https://github.com/gradio-app/gradio/pull/1251) +- ui apis for dalle-mini by [@pngwn](https://github.com/pngwn) in [PR 1258](https://github.com/gradio-app/gradio/pull/1258) +- Add precision to Number, backend only by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1125](https://github.com/gradio-app/gradio/pull/1125) +- Website Design Changes by [@abidlabs](https://github.com/abidlabs) in [PR 1015](https://github.com/gradio-app/gradio/pull/1015) +- Small fixes for multiple demos compatible with 3.0 by [@radames](https://github.com/radames) in [PR 1257](https://github.com/gradio-app/gradio/pull/1257) +- Issue #1160: Model 3D component not destroyed correctly by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1219](https://github.com/gradio-app/gradio/pull/1219) +- Fixes to components by [@abidlabs](https://github.com/abidlabs) in [PR 1260](https://github.com/gradio-app/gradio/pull/1260) +- layout docs by [@abidlabs](https://github.com/abidlabs) in [PR 1263](https://github.com/gradio-app/gradio/pull/1263) +- Static forms by [@pngwn](https://github.com/pngwn) in [PR 1264](https://github.com/gradio-app/gradio/pull/1264) +- Cdn assets by [@pngwn](https://github.com/pngwn) in [PR 1265](https://github.com/gradio-app/gradio/pull/1265) +- update logo by [@gary149](https://github.com/gary149) in [PR 1266](https://github.com/gradio-app/gradio/pull/1266) +- fix slider by [@aliabid94](https://github.com/aliabid94) in [PR 1268](https://github.com/gradio-app/gradio/pull/1268) +- maybe fix auth in iframes by [@pngwn](https://github.com/pngwn) in [PR 1261](https://github.com/gradio-app/gradio/pull/1261) +- Improves "Getting Started" guide by [@abidlabs](https://github.com/abidlabs) in [PR 1269](https://github.com/gradio-app/gradio/pull/1269) +- Add embedded demos to website by [@aliabid94](https://github.com/aliabid94) in [PR 1270](https://github.com/gradio-app/gradio/pull/1270) +- Label hotfixes by [@abidlabs](https://github.com/abidlabs) in [PR 1281](https://github.com/gradio-app/gradio/pull/1281) +- General tweaks by [@pngwn](https://github.com/pngwn) in [PR 1276](https://github.com/gradio-app/gradio/pull/1276) +- only affect links within the document by [@pngwn](https://github.com/pngwn) in [PR 1282](https://github.com/gradio-app/gradio/pull/1282) +- release 3.0b9 by [@abidlabs](https://github.com/abidlabs) in [PR 1283](https://github.com/gradio-app/gradio/pull/1283) +- Dm by [@pngwn](https://github.com/pngwn) in [PR 1284](https://github.com/gradio-app/gradio/pull/1284) +- Website fixes by [@aliabd](https://github.com/aliabd) in [PR 1286](https://github.com/gradio-app/gradio/pull/1286) +- Create Streamables by [@aliabid94](https://github.com/aliabid94) in [PR 1279](https://github.com/gradio-app/gradio/pull/1279) +- ensure table works on mobile by [@pngwn](https://github.com/pngwn) in [PR 1277](https://github.com/gradio-app/gradio/pull/1277) +- changes by [@aliabid94](https://github.com/aliabid94) in [PR 1287](https://github.com/gradio-app/gradio/pull/1287) +- demo alignment on landing page by [@aliabd](https://github.com/aliabd) in [PR 1288](https://github.com/gradio-app/gradio/pull/1288) +- New meta img by [@aliabd](https://github.com/aliabd) in [PR 1289](https://github.com/gradio-app/gradio/pull/1289) +- updated PyPi version to 3.0 by [@abidlabs](https://github.com/abidlabs) in [PR 1290](https://github.com/gradio-app/gradio/pull/1290) +- Fix site by [@aliabid94](https://github.com/aliabid94) in [PR 1291](https://github.com/gradio-app/gradio/pull/1291) +- Mobile responsive guides by [@aliabd](https://github.com/aliabd) in [PR 1293](https://github.com/gradio-app/gradio/pull/1293) +- Update readme by [@abidlabs](https://github.com/abidlabs) in [PR 1292](https://github.com/gradio-app/gradio/pull/1292) +- gif by [@abidlabs](https://github.com/abidlabs) in [PR 1296](https://github.com/gradio-app/gradio/pull/1296) +- Allow decoding headerless b64 string [@1lint](https://github.com/1lint) in [PR 4031](https://github.com/gradio-app/gradio/pull/4031) + +### Contributors Shoutout: + +- [@JefferyChiang](https://github.com/JefferyChiang) made their first contribution in [PR 1004](https://github.com/gradio-app/gradio/pull/1004) +- [@NimaBoscarino](https://github.com/NimaBoscarino) made their first contribution in [PR 1000](https://github.com/gradio-app/gradio/pull/1000) +- [@ronvoluted](https://github.com/ronvoluted) made their first contribution in [PR 1050](https://github.com/gradio-app/gradio/pull/1050) +- [@radames](https://github.com/radames) made their first contribution in [PR 1074](https://github.com/gradio-app/gradio/pull/1074) +- [@freddyaboulton](https://github.com/freddyaboulton) made their first contribution in [PR 1085](https://github.com/gradio-app/gradio/pull/1085) +- [@liteli1987gmail](https://github.com/liteli1987gmail) & [@chenglu](https://github.com/chenglu) made their first contribution in [PR 4767](https://github.com/gradio-app/gradio/pull/4767) \ No newline at end of file diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..0768f8c --- /dev/null +++ b/CITATION.cff @@ -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 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ae3a11f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b47d424 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,526 @@ +# Contributing to Gradio + +![GitHub issues by-label](https://img.shields.io/github/issues/gradio-app/gradio/good%20first%20issue?color=fe7c01&link=https%3A%2F%2Fgithub.com%2Fgradio-app%2Fgradio%2Fissues%3Fq%3Dis%253Aopen%2Bis%253Aissue%2Blabel%253A%2522good%2Bfirst%2Bissue%2522) + + +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: + + + + + + + + + +
MacOS / LinuxWindows
+ +```bash +bash scripts/install_gradio.sh +``` + + +```bash +scripts\install_gradio.bat +``` +
+ +- Run the frontend (only required if you are making changes to the frontend and would like to preview them) + + + + + + + + + +
MacOS / LinuxWindows
+ +```bash +bash scripts/run_frontend.sh +``` + + +```bash +scripts\run_frontend.bat +``` +
+ +- 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) + + + + + + + + + + +
MacOS / LinuxWindows
+ +```bash +bash scripts/install_test_requirements.sh +``` + + +```bash +scripts\install_test_requirements.bat +``` +
+ +If you have a different Python version and conflicting packages during the installation, please first run: + + + + + + + + + + +
MacOS / LinuxWindows
+ +```bash +bash scripts/create_test_requirements.sh +``` + + +```bash +scripts\create_test_requirements.bat +``` +
+ + +### 📦 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: + + + + + + + + + + + +
MacOS / LinuxWindows
+ +```bash +bash scripts/run_frontend.sh +``` + + +```bash +scripts\run_frontend.bat +``` +
+ +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: + + + + + + + + + + +
MacOS / LinuxWindows
+ +``` +bash scripts/run_backend_tests.sh +``` + + +```bash +scripts\run_backend_tests.bat +``` +
+ +- 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: + + + + + + + + + + +
MacOS / LinuxWindows
+ +``` +bash scripts/format_backend.sh +``` + + +```bash +scripts\format_backend.bat +``` +
+ +And if you made changes to the frontend: + + + + + + + + + + + +
MacOS / LinuxWindows
+ +``` +bash scripts/format_frontend.sh +``` + + +```bash +scripts\format_frontend.bat +``` +
+ +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 ""` + +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: + + + + + + + + + + + +
MacOS / LinuxWindows
+ +``` +bash scripts/install_gradio.sh +bash scripts/build_frontend.sh +``` + + +```bash +scripts\install_gradio.bat +scripts\build_frontend.bat +``` +
+--- + +```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 , however 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!_ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d8bfbd --- /dev/null +++ b/README.md @@ -0,0 +1,227 @@ + + +
+ +
+ +Gradio 5.0 - the easiest way to build AI web apps | Product Hunt +gradio-app%2Fgradio | Trendshift + + +[![gradio-backend](https://github.com/gradio-app/gradio/actions/workflows/test-python.yml/badge.svg)](https://github.com/gradio-app/gradio/actions/workflows/test-python.yml) +[![gradio-ui](https://github.com/gradio-app/gradio/actions/workflows/tests-js.yml/badge.svg)](https://github.com/gradio-app/gradio/actions/workflows/tests-js.yml) +[![PyPI](https://img.shields.io/pypi/v/gradio)](https://pypi.org/project/gradio/) +[![PyPI downloads](https://img.shields.io/pypi/dm/gradio)](https://pypi.org/project/gradio/) +![Python version](https://img.shields.io/badge/python-3.10+-important) +[![Twitter follow](https://img.shields.io/twitter/follow/gradio?style=social&label=follow)](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/) + +
+ +
+ +English | [中文](readme_files/zh-cn#readme) + +
+ +# 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!* + + + +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 are provided here. + +### 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 gradio to gr. 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. + +![`hello_world_4` demo](demo/hello_world_4/screenshot.gif) + +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 hot reload mode, which automatically reloads the Gradio app whenever you make changes to the file. To do this, simply type in gradio before the name of the file instead of python. In the example above, you would type: `gradio app.py` in your terminal. You can also enable vibe mode by using the --vibe flag, e.g. gradio --vibe app.py, which provides an in-browser chat that can be used to write or edit your Gradio app using natural language. Learn more in the Hot Reloading Guide. + + +**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! + +[huggingface](https://huggingface.co) +[python](https://www.python.org) +[fastapi](https://fastapi.tiangolo.com) +[encode](https://www.encode.io) +[svelte](https://svelte.dev) +[vite](https://vitejs.dev) +[pnpm](https://pnpm.io) +[tailwind](https://tailwindcss.com) +[storybook](https://storybook.js.org/) +[chromatic](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}, +} +``` diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..8eccd67 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`gradio-app/gradio` +- 原始仓库:https://github.com/gradio-app/gradio +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..3e1e99f --- /dev/null +++ b/SECURITY.md @@ -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. diff --git a/_worker.bundle b/_worker.bundle new file mode 100644 index 0000000..8f12d3f --- /dev/null +++ b/_worker.bundle @@ -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-- \ No newline at end of file diff --git a/build_pypi.sh b/build_pypi.sh new file mode 100755 index 0000000..11e7e1a --- /dev/null +++ b/build_pypi.sh @@ -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 diff --git a/client/js/CHANGELOG.md b/client/js/CHANGELOG.md new file mode 100644 index 0000000..f0a4166 --- /dev/null +++ b/client/js/CHANGELOG.md @@ -0,0 +1,1049 @@ +# @gradio/client + +## 2.3.1 + +### Fixes + +- [#13588](https://github.com/gradio-app/gradio/pull/13588) [`2e80558`](https://github.com/gradio-app/gradio/commit/2e805588c8dc84a4a584898fd43b267c72209f78) - Fire `state.change()` for streaming (`.stream()`) events. Thanks @hysts! +- [#13581](https://github.com/gradio-app/gradio/pull/13581) [`461d82d`](https://github.com/gradio-app/gradio/commit/461d82df2689ec0c53e84b8722eaeae58fd2a6ae) - Use same-origin credentials in the JS client so cross-origin embeds work again. Thanks @hysts! +- [#13579](https://github.com/gradio-app/gradio/pull/13579) [`3d26f41`](https://github.com/gradio-app/gradio/commit/3d26f41c4a3bfa31ea01c4ae0d082a9fc9ed0bc7) - Make `Client.predict()` reject its returned promise on unknown endpoints so the error is catchable. Thanks @abidlabs! + +## 2.3.0 + +### Features + +- [#13526](https://github.com/gradio-app/gradio/pull/13526) [`53cb4ca`](https://github.com/gradio-app/gradio/commit/53cb4cae1ec3521e9170d12867253516413ba37a) - Run `pnpm lint` and `pnpm ts:check` on CI. Thanks @abidlabs! + +## 2.2.2 + +### Fixes + +- [#13500](https://github.com/gradio-app/gradio/pull/13500) [`751a397`](https://github.com/gradio-app/gradio/commit/751a397ce519732526863ee84e311f7f64195f9d) - Close iterator on terminal error in JS client. Thanks @hysts! + +## 2.2.1 + +### Fixes + +- [#13403](https://github.com/gradio-app/gradio/pull/13403) [`df56862`](https://github.com/gradio-app/gradio/commit/df56862a1531fc372a3248716dc4bd07e6db1c52) - [js-client] close submit iterator on next/close race. Thanks @hysts! + +## 2.2.0 + +### Features + +- [#13176](https://github.com/gradio-app/gradio/pull/13176) [`45c4ecd`](https://github.com/gradio-app/gradio/commit/45c4ecd25fc83c7ee0210d216df1bc4bd509e622) - Add `@gr.cache()` decorator for caching deterministic functions, as as well as a lower-level `gr.Cache` that uses dependency injection. Thanks @abidlabs! + +### Fixes + +- [#13210](https://github.com/gradio-app/gradio/pull/13210) [`4005b93`](https://github.com/gradio-app/gradio/commit/4005b93dd59d7e0d144619986f01e900659b3d0b) - Fix ZeroGPU handling for `gr.Server`. Thanks @abidlabs! + +## 2.1.0 + +### Features + +- [#12929](https://github.com/gradio-app/gradio/pull/12929) [`978bc6e`](https://github.com/gradio-app/gradio/commit/978bc6ea5094aa11e10994cdd662c4c663a86a83) - Add server functions support to gr.HTML. Thanks @aliabid94! +- [#12907](https://github.com/gradio-app/gradio/pull/12907) [`3e625a0`](https://github.com/gradio-app/gradio/commit/3e625a0ecfab6e74b7561b68adbe55341ecbc47a) - Better error handling when connection to server is lost. Thanks @abidlabs! + +## 2.0.4 + +### Fixes + +- [#12828](https://github.com/gradio-app/gradio/pull/12828) [`151cbd1`](https://github.com/gradio-app/gradio/commit/151cbd1aac0da3aeb5f0b7b33585223d2bc47138) - Fix private spaces. Thanks @freddyaboulton! +- [#12835](https://github.com/gradio-app/gradio/pull/12835) [`5ecf6d2`](https://github.com/gradio-app/gradio/commit/5ecf6d27c50a20e2329c1aca0634924479ceb6cd) - Fix CSS root in spaces. Thanks @freddyaboulton! +- [#12817](https://github.com/gradio-app/gradio/pull/12817) [`05acc66`](https://github.com/gradio-app/gradio/commit/05acc6627de866db6a742e0540c2733041d76a86) - Fix Login. Thanks @freddyaboulton! + +## 2.0.3 + +### Fixes + +- [#12773](https://github.com/gradio-app/gradio/pull/12773) [`6edcea5`](https://github.com/gradio-app/gradio/commit/6edcea5d74be661d37c70ccca08900be2ceb8e1e) - Fix dev mode. Thanks @pngwn! +- [#12472](https://github.com/gradio-app/gradio/pull/12472) [`9a2bc0d`](https://github.com/gradio-app/gradio/commit/9a2bc0dacdd2b3f670fae815093c61ad08eee7e3) - Re-enable SSR mode. Thanks @pngwn! + +## 2.0.2 + +### Fixes + +- [#12608](https://github.com/gradio-app/gradio/pull/12608) [`ab20c59`](https://github.com/gradio-app/gradio/commit/ab20c592e1fab33e96ab2698bbbb942d0972501b) - feat(client): add generic type parameter to predict() method. Thanks @majiayu000! + +## 2.0.1 + +### Fixes + +- [#12600](https://github.com/gradio-app/gradio/pull/12600) [`1fafaba`](https://github.com/gradio-app/gradio/commit/1fafabaace315b1c699855cadb69eb17488de957) - Add x-gradio-user-header. Thanks @freddyaboulton! + +## 2.0.0 + +### Features + +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - [No Merge] Gradio 6.0 +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Pass component props as input +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Rename show_api +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Fix error toast type definition for exception handling +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Fix queue false render issue +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Fix some e2e tests +- [#12438](https://github.com/gradio-app/gradio/pull/12438) [`25ffc03`](https://github.com/gradio-app/gradio/commit/25ffc0398f8feb43d817c02b2ab970c16de6d797) - Svelte5 migration and bugfix + +## 2.0.0-dev.2 + +### Features + +- [#12363](https://github.com/gradio-app/gradio/pull/12363) [`4845bfd`](https://github.com/gradio-app/gradio/commit/4845bfdab53096a609d80485e87d5cc70f3f0521) - Fix queue false render issue. Thanks @freddyaboulton! +- [#12378](https://github.com/gradio-app/gradio/pull/12378) [`f5e5344`](https://github.com/gradio-app/gradio/commit/f5e5344db61eba28253719928577dc2c495958e1) - Fix some e2e tests. Thanks @freddyaboulton! + +## 2.0.0-dev.1 + +### Features + +- [#12069](https://github.com/gradio-app/gradio/pull/12069) [`9de88ca`](https://github.com/gradio-app/gradio/commit/9de88ca470ce529366d259f0deaa955f658000b9) - Rename show_api. Thanks @freddyaboulton! + +## 2.0.0-dev.0 + +### Features + +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`53d9098`](https://github.com/gradio-app/gradio/commit/53d9098cca378f6ebff9dec15d2faa8a3d2fb510) - [No Merge] Gradio 6.0. Thanks @freddyaboulton! + +## 1.19.1 + +### Fixes + +- [#11955](https://github.com/gradio-app/gradio/pull/11955) [`64046cc`](https://github.com/gradio-app/gradio/commit/64046ccd76a41f06ef9eed0e3aa1ec66348f55bd) - Fix Multi-page demo in Dev Mode. Thanks @ftoh! + +## 1.19.0 + +### Features + +- [#11858](https://github.com/gradio-app/gradio/pull/11858) [`3f8ea13`](https://github.com/gradio-app/gradio/commit/3f8ea13a8ca92abf0ad34392e403a449fda3c6c2) - remove lite. Thanks @pngwn! +- [#11902](https://github.com/gradio-app/gradio/pull/11902) [`6d39644`](https://github.com/gradio-app/gradio/commit/6d39644ee1000b04728eb4909cee405e7ee3b5b2) - Add navbar visibility controls and customization options. Thanks @abidlabs! + +### Fixes + +- [#11784](https://github.com/gradio-app/gradio/pull/11784) [`d9dd3f5`](https://github.com/gradio-app/gradio/commit/d9dd3f54b7fb34cf7118e549d39fc63937ca3489) - Add "hidden" option to component's `visible` kwarg to render but visually hide the component. Thanks @pngwn! + +## 1.18.0 + +### Features + +- [#11814](https://github.com/gradio-app/gradio/pull/11814) [`013784a`](https://github.com/gradio-app/gradio/commit/013784a7086047651e8e661a38bde7d5c7f10db7) - add validation support. Thanks @pngwn! + +## 1.17.1 + +### Fixes + +- [#11709](https://github.com/gradio-app/gradio/pull/11709) [`48f1dcf`](https://github.com/gradio-app/gradio/commit/48f1dcf58cd6437daa03a8cf1c9942aae2cec421) - Fix output paths for the @gradio/client browser build. Thanks @pngwn! + +## 1.17.0 + +### Features + +- [#11691](https://github.com/gradio-app/gradio/pull/11691) [`2605a99`](https://github.com/gradio-app/gradio/commit/2605a99bf29bebbbb0a97cc8e0015b5bf8d8e79b) - Add .failure() event listener for error handling. Thanks @elanehan! + +## 1.16.0 + +### Features + +- [#11662](https://github.com/gradio-app/gradio/pull/11662) [`a78f5fa`](https://github.com/gradio-app/gradio/commit/a78f5fa466a4b11ffaaafc5099a64df49afb6e41) - Gradio vibe editor. Thanks @aliabid94! + +## 1.15.7 + +### Fixes + +- [#11604](https://github.com/gradio-app/gradio/pull/11604) [`83e14bc`](https://github.com/gradio-app/gradio/commit/83e14bcfa653e2ec477f0d09b497adf605734e46) - Fix event cancelling bug. Thanks @freddyaboulton! + +## 1.15.6 + +### Features + +- [#11543](https://github.com/gradio-app/gradio/pull/11543) [`ac95ac0`](https://github.com/gradio-app/gradio/commit/ac95ac0d8c2e65d1632376e632fb7d16131334b6) - Connection handling messaging. Thanks @aliabid94! + +## 1.15.5 + +### Fixes + +- [#11430](https://github.com/gradio-app/gradio/pull/11430) [`4d4cd4b`](https://github.com/gradio-app/gradio/commit/4d4cd4b08d6daafc66e0189a2b274916da00d480) - fix exports + add browser build. Thanks @pngwn! +- [#11435](https://github.com/gradio-app/gradio/pull/11435) [`50d43a8`](https://github.com/gradio-app/gradio/commit/50d43a8e8bc2a4862668a5d46a5e6482b4f75fe3) - SSR Auth Fix. Thanks @dawoodkhan82! + +## 1.15.4 + +### Fixes + +- [#11432](https://github.com/gradio-app/gradio/pull/11432) [`dd1eee5`](https://github.com/gradio-app/gradio/commit/dd1eee5f9cd3d70773912fd6444d093bdcea321a) - Fix bug where cancelling an event would close the event stream. Thanks @freddyaboulton! +- [#11421](https://github.com/gradio-app/gradio/pull/11421) [`c2acf6e`](https://github.com/gradio-app/gradio/commit/c2acf6e33025fe7bbfe0660c182006651cc95090) - Preserve value in reload mode. Thanks @aliabid94! + +## 1.15.3 + +### Fixes + +- [#11387](https://github.com/gradio-app/gradio/pull/11387) [`8245afc`](https://github.com/gradio-app/gradio/commit/8245afc669501e1e5f0d619f452455f68a3b7667) - Define root URL in frontend. Thanks @aliabid94! + +## 1.15.2 + +### Fixes + +- [#11325](https://github.com/gradio-app/gradio/pull/11325) [`2b571e1`](https://github.com/gradio-app/gradio/commit/2b571e13afdc8031ce9c1291abf0fc7062340064) - Fix image streaming - wait for ws to open. Thanks @freddyaboulton! + +## 1.15.1 + +### Fixes + +- [#11243](https://github.com/gradio-app/gradio/pull/11243) [`35afa21`](https://github.com/gradio-app/gradio/commit/35afa21f0d6647e4fbda711f3f22a2fd54eedaf9) - Only show parameters warning when valid `endpoint_info` exists. Thanks @hannahblair! + +## 1.15.0 + +### Features + +- [#11155](https://github.com/gradio-app/gradio/pull/11155) [`30a1d9e`](https://github.com/gradio-app/gradio/commit/30a1d9e2ac3013d9c844b236410010bce97ffaf5) - Improvements to MCP page. Thanks @abidlabs! +- [#11047](https://github.com/gradio-app/gradio/pull/11047) [`6d4b8a7`](https://github.com/gradio-app/gradio/commit/6d4b8a7f10daefc9c79aa224635da23fbaeebb76) - Implement custom i18n. Thanks @hannahblair! + +## 1.14.2 + +### Fixes + +- [#11017](https://github.com/gradio-app/gradio/pull/11017) [`734b309`](https://github.com/gradio-app/gradio/commit/734b3099d79647695e635d87726666d4b28d1bcf) - Include HF token in stream requests. Thanks @nostalgebraist! + +## 1.14.1 + +### Features + +- [#10890](https://github.com/gradio-app/gradio/pull/10890) [`01b88c7`](https://github.com/gradio-app/gradio/commit/01b88c7fdedb413ba92ef6191967a8aed25e185f) - Improve API error handling in JS Client. Thanks @l2dy! + +## 1.14.0 + +### Features + +- [#10834](https://github.com/gradio-app/gradio/pull/10834) [`c05610c`](https://github.com/gradio-app/gradio/commit/c05610c87dd7f9e9fe5d0aed2fe93e40fdd32648) - Add Deep Links. Thanks @freddyaboulton! + +## 1.13.1 + +### Features + +- [#10694](https://github.com/gradio-app/gradio/pull/10694) [`16244f3`](https://github.com/gradio-app/gradio/commit/16244f3c1cb1a65ac1f719142f8fab67512fbb25) - Event Listeners in gradio sketch. Thanks @aliabid94! + +### Fixes + +- [#10719](https://github.com/gradio-app/gradio/pull/10719) [`b710d7c`](https://github.com/gradio-app/gradio/commit/b710d7cf13c1277fd18c7809cad0f707b880ef70) - Fix error display. Thanks @aliabid94! + +## 1.13.0 + +### Features + +- [#10500](https://github.com/gradio-app/gradio/pull/10500) [`16d419b`](https://github.com/gradio-app/gradio/commit/16d419b9f1f18ae4507d18a4739eb83ac4f3fae9) - Allow functions that solely update component properties to run in the frontend by setting `js=True`. Thanks @abidlabs! + +## 1.12.0 + +### Features + +- [#10492](https://github.com/gradio-app/gradio/pull/10492) [`29880d5`](https://github.com/gradio-app/gradio/commit/29880d51fbe7fbd222b0765a83c95134dc7d0e90) - Allow showing progress updates on arbitrary components. Thanks @abidlabs! + +### Fixes + +- [#10547](https://github.com/gradio-app/gradio/pull/10547) [`083d68b`](https://github.com/gradio-app/gradio/commit/083d68b223be82a65f18c553df9ae690a8118a49) - quick_fix_client. Thanks @aliabid94! + +## 1.11.0 + +### Features + +- [#10433](https://github.com/gradio-app/gradio/pull/10433) [`2e8dc74`](https://github.com/gradio-app/gradio/commit/2e8dc74f751be02f7217f78d241806b42fcdca04) - Allow building multipage Gradio apps. Thanks @aliabid94! + +## 1.10.0 + +### Features + +- [#10270](https://github.com/gradio-app/gradio/pull/10270) [`bb11a2a`](https://github.com/gradio-app/gradio/commit/bb11a2a702ca04fde245e7d54d155cbcbde7791e) - [ZeroGPU] Handshake-based postMessage. Thanks @cbensimon! + +### Fixes + +- [#10332](https://github.com/gradio-app/gradio/pull/10332) [`e742dcc`](https://github.com/gradio-app/gradio/commit/e742dcccb376692c9ddd5a6c251080e7c5936574) - Allow users to add a custom API route. Thanks @aliabid94! + +## 1.9.0 + +### Features + +- [#10262](https://github.com/gradio-app/gradio/pull/10262) [`f3bedd4`](https://github.com/gradio-app/gradio/commit/f3bedd4011bdfdecc952eb1275a9dd96af3e8d71) - add gr.Success and update windows contributing. Thanks @not-lain! +- [#10254](https://github.com/gradio-app/gradio/pull/10254) [`da07707`](https://github.com/gradio-app/gradio/commit/da0770748db9ea40194a43c9138ee2c6536b1247) - Add a `settings` link to the footer with i18n options & pwa instructions. Thanks @abidlabs! + +## 1.8.0 + +### Features + +- [#9930](https://github.com/gradio-app/gradio/pull/9930) [`eae345e`](https://github.com/gradio-app/gradio/commit/eae345e5fde39aea220b57c6a954cd7d72ff32d5) - Allow settings custom headers in js client. Thanks @elgiano! +- [#9950](https://github.com/gradio-app/gradio/pull/9950) [`fc06fe4`](https://github.com/gradio-app/gradio/commit/fc06fe41f015678a0545f4e5c99f6ae2704f0031) - Add ability to read and write from LocalStorage. Thanks @abidlabs! + +## 1.7.1 + +### Fixes + +- [#9814](https://github.com/gradio-app/gradio/pull/9814) [`6505d42`](https://github.com/gradio-app/gradio/commit/6505d4289a3e3d27d9133b1c8af41697fdc1476d) - support gradio apps on spaces served on subpaths. Thanks @pngwn! + +## 1.7.0 + +### Features + +- [#9681](https://github.com/gradio-app/gradio/pull/9681) [`2ed2361`](https://github.com/gradio-app/gradio/commit/2ed236187a9aab18e17fc4a8079eddef7dd195a5) - Allow setting title in gr.Info/Warning/Error. Thanks @ABucket! + +## 1.6.0 + +### Features + +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Disable liking user message in chatbot by default but make it configurable +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Open audio/image input stream only when queue is ready +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Send Streaming data over Websocket if possible. Also support base64 output format for images. +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Streaming inputs for 5.0 +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - fix SSR apps on spaces +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Ssr part 2 +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - prefix api routes + +### Fixes + + +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Trigger state change event on iterators + +## 1.6.0-beta.4 + +### Features + +- [#9483](https://github.com/gradio-app/gradio/pull/9483) [`8dc7c12`](https://github.com/gradio-app/gradio/commit/8dc7c12389311b60efcde1b9d3e3668a34d2dc00) - Send Streaming data over Websocket if possible. Also support base64 output format for images. Thanks @freddyaboulton! + +## 1.6.0-beta.3 + +### Features + +- [#9412](https://github.com/gradio-app/gradio/pull/9412) [`c2c2fd9`](https://github.com/gradio-app/gradio/commit/c2c2fd989348f826566773c07c0e0bda200199ff) - fix SSR apps on spaces. Thanks @pngwn! + +## 1.6.0-beta.2 + +### Features + +- [#9323](https://github.com/gradio-app/gradio/pull/9323) [`06babda`](https://github.com/gradio-app/gradio/commit/06babda0395fd3fbd323c1c3cb33704ecfd6deb0) - Disable liking user message in chatbot by default but make it configurable. Thanks @freddyaboulton! +- [#9339](https://github.com/gradio-app/gradio/pull/9339) [`4c8c6f2`](https://github.com/gradio-app/gradio/commit/4c8c6f2fe603081941c5fdc43f48a0632b9f31ad) - Ssr part 2. Thanks @pngwn! + +### Fixes + +- [#9299](https://github.com/gradio-app/gradio/pull/9299) [`aa35b07`](https://github.com/gradio-app/gradio/commit/aa35b0788e613fdd45446d267513e6f94fa208ea) - Trigger state change event on iterators. Thanks @freddyaboulton! + +## 1.6.0-beta.1 + +### Features + +- [#9200](https://github.com/gradio-app/gradio/pull/9200) [`2e179d3`](https://github.com/gradio-app/gradio/commit/2e179d35be6ed60a5a6bfc7303178d63e41781ad) - prefix api routes. Thanks @pngwn! + +## 1.6.0-beta.0 + +### Features + +- [#9149](https://github.com/gradio-app/gradio/pull/9149) [`3d7a9b8`](https://github.com/gradio-app/gradio/commit/3d7a9b81f6fef06187eca832471dc1692eb493a0) - Open audio/image input stream only when queue is ready. Thanks @freddyaboulton! +- [#8941](https://github.com/gradio-app/gradio/pull/8941) [`97a7bf6`](https://github.com/gradio-app/gradio/commit/97a7bf66a79179d1b91a3199d68e5c11216ca500) - Streaming inputs for 5.0. Thanks @freddyaboulton! + +## 1.5.2 + +### Fixes + +- [#9163](https://github.com/gradio-app/gradio/pull/9163) [`2b6cbf2`](https://github.com/gradio-app/gradio/commit/2b6cbf25908e42cf027324e54ef2cc0baad11a91) - fix exports and generate types. Thanks @pngwn! + +## 1.5.1 + +### Features + +- [#9118](https://github.com/gradio-app/gradio/pull/9118) [`e1c404d`](https://github.com/gradio-app/gradio/commit/e1c404da1143fb52b659d03e028bdba1badf443d) - setup npm-previews of all packages. Thanks @pngwn! + +## 1.5.0 + +### Features + +- [#8965](https://github.com/gradio-app/gradio/pull/8965) [`d30432e`](https://github.com/gradio-app/gradio/commit/d30432e9c6d4cc1e5cfd989a1a3ae4aba7e21290) - harden CI. Thanks @pngwn! + +### Fixes + +- [#8847](https://github.com/gradio-app/gradio/pull/8847) [`4d8a473`](https://github.com/gradio-app/gradio/commit/4d8a473632e388a312aee5c705b3c1f79853441b) - fix: wrong named param check for js client. Thanks @freddyaboulton! + +## 1.4.0 + +### Features + +- [#8816](https://github.com/gradio-app/gradio/pull/8816) [`9ee6839`](https://github.com/gradio-app/gradio/commit/9ee6839f94d23d685a800ed3a275206e0b0e48f6) - Change optionality of the `data` param in `submit` + `predict`. Thanks @hannahblair! + +### Fixes + +- [#8820](https://github.com/gradio-app/gradio/pull/8820) [`5050b36`](https://github.com/gradio-app/gradio/commit/5050b36221e75a18d8a5d4f74a725e70768a4c4a) - fix: wrong named param check for js client. Thanks @JacobLinCool! + +## 1.3.0 + +### Fixes + +- [#8699](https://github.com/gradio-app/gradio/pull/8699) [`012da05`](https://github.com/gradio-app/gradio/commit/012da05287846d94beb0ecdc28d7fbc48c4248ff) - Ensure JS client `status_callback` functionality works and improve status messages. Thanks @hannahblair! +- [#8505](https://github.com/gradio-app/gradio/pull/8505) [`2943d6d`](https://github.com/gradio-app/gradio/commit/2943d6d68847314885dc6c5c0247083116017ca0) - Add Timer component. Thanks @aliabid94! +- [#8715](https://github.com/gradio-app/gradio/pull/8715) [`a6b3c6c`](https://github.com/gradio-app/gradio/commit/a6b3c6ce4e1d06253860c72740024a9138e3a93a) - Ensure `@gradio/client`'s `submit` iterator releases as expected. Thanks @pngwn! +- [#8716](https://github.com/gradio-app/gradio/pull/8716) [`e834d30`](https://github.com/gradio-app/gradio/commit/e834d302e44f7a54565129bf2c11acf4e882a59b) - ensure `@gradio/client` always returns the correct data. Thanks @pngwn! +- [#8714](https://github.com/gradio-app/gradio/pull/8714) [`1b5b5b0`](https://github.com/gradio-app/gradio/commit/1b5b5b0b43e69ee84f3baad2aae59ffc9c4d995a) - Bind `fetch` and `stream` in JS client. Thanks @hannahblair! +- [#8720](https://github.com/gradio-app/gradio/pull/8720) [`936c713`](https://github.com/gradio-app/gradio/commit/936c7137a99ef59efdf75bae5dd27eea2ac1f577) - Documents auth in the guides, in the view API page, and also types the Blocks.config object. Thanks @abidlabs! + +## 1.2.1 + +### Features + +- [#8649](https://github.com/gradio-app/gradio/pull/8649) [`4b6c8b1`](https://github.com/gradio-app/gradio/commit/4b6c8b1c004cee67345a7f103ba2dc8e90b82e6c) - ensure `File` objects are handled in JS client `handle_file`. Thanks @hannahblair! + +## 1.2.0 + +### Features + +- [#8489](https://github.com/gradio-app/gradio/pull/8489) [`c2a0d05`](https://github.com/gradio-app/gradio/commit/c2a0d056d679d90631d9ccd944dadd67e7e03b7f) - Control Display of Error, Info, Warning. Thanks @freddyaboulton! +- [#8571](https://github.com/gradio-app/gradio/pull/8571) [`a77877f`](https://github.com/gradio-app/gradio/commit/a77877f62df7c610fcfac7b3b00e186a087c8ec6) - First time loading performance optimization. Thanks @baojianting! +- [#8600](https://github.com/gradio-app/gradio/pull/8600) [`7289c4b`](https://github.com/gradio-app/gradio/commit/7289c4b036d8a78c48f8c9e66ba998e6730e80d2) - Add credentials: include and Cookie header to prevent 401 error. Thanks @yinkiu602! +- [#8522](https://github.com/gradio-app/gradio/pull/8522) [`bdaa678`](https://github.com/gradio-app/gradio/commit/bdaa678d0c0a22250b41104f32e9121f98dc7437) - add handle_file docs. Thanks @pngwn! + +### Fixes + +- [#8521](https://github.com/gradio-app/gradio/pull/8521) [`900cf25`](https://github.com/gradio-app/gradio/commit/900cf25256a5b0563860097d69aac28b6afbfd8b) - Ensure frontend functions work when they don't return a value. Thanks @pngwn! +- [#8548](https://github.com/gradio-app/gradio/pull/8548) [`7fc0f51`](https://github.com/gradio-app/gradio/commit/7fc0f5149bb8d31f3d01b4151b478070499751ee) - Fix reload mode by implementing `close` on the client. Thanks @freddyaboulton! + +## 1.1.1 + +### Features + +- [#8499](https://github.com/gradio-app/gradio/pull/8499) [`c5f6e77`](https://github.com/gradio-app/gradio/commit/c5f6e7722a197d4706419ade14276ddecf3196f8) - Cache break themes on change. Thanks @aliabid94! + +## 1.1.0 + +### Features + +- [#8483](https://github.com/gradio-app/gradio/pull/8483) [`e2271e2`](https://github.com/gradio-app/gradio/commit/e2271e207d98074bf39b02ae3c5443b2f097627d) - documentation for @gradio/client. Thanks @pngwn! +- [#8485](https://github.com/gradio-app/gradio/pull/8485) [`f8ebace`](https://github.com/gradio-app/gradio/commit/f8ebaceccef60a112603d290d10072ef4e938a6a) - Ensure all status are reported internally when calling `predict`. Thanks @pngwn! + +## 1.0.0 + +### Highlights + +#### Clients 1.0 Launch! ([#8468](https://github.com/gradio-app/gradio/pull/8468) [`7cc0a0c`](https://github.com/gradio-app/gradio/commit/7cc0a0c1abea585c3f50ffb1ff78d2b08ddbdd92)) + +We're excited to unveil the first major release of the Gradio clients. +We've made it even easier to turn any Gradio application into a production endpoint thanks to the clients' **ergonomic**, **transparent**, and **portable** design. + +#### Ergonomic API 💆 + +**Stream From a Gradio app in 5 lines** + +Use the `submit` method to get a job you can iterate over: + +```python +from gradio_client import Client + +client = Client("gradio/llm_stream") + +for result in client.submit("What's the best UI framework in Python?"): + print(result) +``` + +```ts +import { Client } from "@gradio/client"; + +const client = await Client.connect("gradio/llm_stream") +const job = client.submit("/predict", {"text": "What's the best UI framework in Python?"}) + +for await (const msg of job) console.log(msg.data) +``` + +**Use the same keyword arguments as the app** + + +```python +from gradio_client import Client + +client = Client("http://127.0.0.1:7860/") +result = client.predict( + message="Hello!!", + system_prompt="You are helpful AI.", + tokens=10, + api_name="/chat" +) +print(result) +``` + +```ts +import { Client } from "@gradio/client"; + +const client = await Client.connect("http://127.0.0.1:7860/"); +const result = await client.predict("/chat", { + message: "Hello!!", + system_prompt: "Hello!!", + tokens: 10, +}); + +console.log(result.data); +``` + +**Better Error Messages** + +If something goes wrong in the upstream app, the client will raise the same exception as the app provided that `show_error=True` in the original app's `launch()` function, or it's a `gr.Error` exception. + +#### Transparent Design 🪟 + +Anything you can do in the UI, you can do with the client: +* 🔒 Authentication +* 🛑 Job Cancelling +* ℹ️ Access Queue Position and API +* 📕 View the API information + +Here's an example showing how to display the queue position of a pending job: + +```python +from gradio_client import Client + +client = Client("gradio/diffusion_model") + +job = client.submit("A cute cat") +while not job.done(): + status = job.status() + print(f"Current in position {status.rank} out of {status.queue_size}") +``` + +#### Portable Design ⛺️ + +The client can run from pretty much any python and javascript environment (node, deno, the browser, Service Workers). + +Here's an example using the client from a Flask server using gevent: + +```python +from gevent import monkey +monkey.patch_all() + +from gradio_client import Client +from flask import Flask, send_file +import time + +app = Flask(__name__) + +imageclient = Client("gradio/diffusion_model") + +@app.route("/gen") +def gen(): + result = imageclient.predict( + "A cute cat", + api_name="/predict" + ) + return send_file(result) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) +``` + +#### 1.0 Migration Guide and Breaking Changes + +**Python** +- The `serialize` argument of the `Client` class was removed. Has no effect. +- The `upload_files` argument of the `Client` was removed. +- All filepaths must be wrapped in the `handle_file` method. Example: +```python +from gradio_client import Client, handle_file + +client = Client("gradio/image_captioner") +client.predict(handle_file("cute_cat.jpg")) +``` +- The `output_dir` argument was removed. It is not specified in the `download_files` argument. + + +**Javascript** +The client has been redesigned entirely. It was refactored from a function into a class. An instance can now be constructed by awaiting the `connect` method. + +```js +const app = await Client.connect("gradio/whisper") +``` +The app variable has the same methods as the python class (`submit`, `predict`, `view_api`, `duplicate`). + + + +#### Additional Changes + +- [#8243](https://github.com/gradio-app/gradio/pull/8243) - Set orig_name in python client file uploads. +- [#8264](https://github.com/gradio-app/gradio/pull/8264) - Make exceptions in the Client more specific. +- [#8247](https://github.com/gradio-app/gradio/pull/8247) - Fix api recorder. +- [#8276](https://github.com/gradio-app/gradio/pull/8276) - Fix bug where client could not connect to apps that had self signed certificates. +- [#8245](https://github.com/gradio-app/gradio/pull/8245) - Cancel server progress from the python client. +- [#8200](https://github.com/gradio-app/gradio/pull/8200) - Support custom components in gr.load +- [#8182](https://github.com/gradio-app/gradio/pull/8182) - Convert sse calls in client from async to sync. +- [#7732](https://github.com/gradio-app/gradio/pull/7732) - Adds support for kwargs and default arguments in the python client, and improves how parameter information is displayed in the "view API" page. +- [#7888](https://github.com/gradio-app/gradio/pull/7888) - Cache view_api info in server and python client. +- [#7575](https://github.com/gradio-app/gradio/pull/7575) - Files should now be supplied as `file(...)` in the Client, and some fixes to `gr.load()` as well. +- [#8401](https://github.com/gradio-app/gradio/pull/8401) - Add CDN installation to JS docs. +- [#8299](https://github.com/gradio-app/gradio/pull/8299) - Allow JS Client to work with authenticated spaces 🍪. +- [#8408](https://github.com/gradio-app/gradio/pull/8408) - Connect heartbeat if state created in render. Also fix config cleanup bug #8407. +- [#8258](https://github.com/gradio-app/gradio/pull/8258) - Improve URL handling in JS Client. +- [#8322](https://github.com/gradio-app/gradio/pull/8322) - ensure the client correctly handles all binary data. +- [#8296](https://github.com/gradio-app/gradio/pull/8296) - always create a jwt when connecting to a space if a hf_token is present. +- [#8285](https://github.com/gradio-app/gradio/pull/8285) - use the correct query param to pass the jwt to the heartbeat event. +- [#8272](https://github.com/gradio-app/gradio/pull/8272) - ensure client works for private spaces. +- [#8197](https://github.com/gradio-app/gradio/pull/8197) - Add support for passing keyword args to `data` in JS client. +- [#8252](https://github.com/gradio-app/gradio/pull/8252) - Client node fix. +- [#8209](https://github.com/gradio-app/gradio/pull/8209) - Rename `eventSource_Factory` and `fetch_implementation`. +- [#8109](https://github.com/gradio-app/gradio/pull/8109) - Implement JS Client tests. +- [#8211](https://github.com/gradio-app/gradio/pull/8211) - remove redundant event source logic. +- [#8179](https://github.com/gradio-app/gradio/pull/8179) - rework upload to be a class method + pass client into each component. +- [#8181](https://github.com/gradio-app/gradio/pull/8181) - Ensure connectivity to private HF spaces with SSE protocol. +- [#8169](https://github.com/gradio-app/gradio/pull/8169) - Only connect to heartbeat if needed. +- [#8118](https://github.com/gradio-app/gradio/pull/8118) - Add eventsource polyfill for Node.js and browser environments. +- [#7646](https://github.com/gradio-app/gradio/pull/7646) - Refactor JS Client. +- [#7974](https://github.com/gradio-app/gradio/pull/7974) - Fix heartbeat in the js client to be Lite compatible. +- [#7926](https://github.com/gradio-app/gradio/pull/7926) - Fixes streaming event race condition. + + Thanks @freddyaboulton! + +### Features + +- [#8370](https://github.com/gradio-app/gradio/pull/8370) [`48eeea4`](https://github.com/gradio-app/gradio/commit/48eeea4eaab7e24168688e3c3fbafb30e4e78d51) - Refactor Cancelling Logic To Use /cancel. Thanks @freddyaboulton! + +### Fixes + +- [#8477](https://github.com/gradio-app/gradio/pull/8477) [`d5a9604`](https://github.com/gradio-app/gradio/commit/d5a960493017a4890685af61d78ce7d3b3b12e6b) - Fix js client bundle. Thanks @pngwn! +- [#8451](https://github.com/gradio-app/gradio/pull/8451) [`9d2d605`](https://github.com/gradio-app/gradio/commit/9d2d6051caed5c8749a26a6fa7480a5ae6e6c4f3) - Change client submit API to be an AsyncIterable and support more platforms. Thanks @pngwn! +- [#8462](https://github.com/gradio-app/gradio/pull/8462) [`6447dfa`](https://github.com/gradio-app/gradio/commit/6447dface4d46db1c69460e8325a1928d0476a46) - Improve file handling in JS Client. Thanks @hannahblair! +- [#8439](https://github.com/gradio-app/gradio/pull/8439) [`63d36fb`](https://github.com/gradio-app/gradio/commit/63d36fbbf4bf6dc909be9a0ffc7b6bf6621d83e8) - Handle gradio apps using `state` in the JS Client. Thanks @hannahblair! + +## 0.20.1 + +### Features + +- [#8415](https://github.com/gradio-app/gradio/pull/8415) [`227de35`](https://github.com/gradio-app/gradio/commit/227de352982b3dcdf9384eaa28b7e9cf09afb6e8) - Fix spaces load error. Thanks @aliabid94! + +## 0.20.0 + +### Features + +- [#8401](https://github.com/gradio-app/gradio/pull/8401) [`d078621`](https://github.com/gradio-app/gradio/commit/d078621928136c09ca902d2f37594ed887c67d2e) - Add CDN installation to JS docs. Thanks @hannahblair! +- [#8243](https://github.com/gradio-app/gradio/pull/8243) [`55f664f`](https://github.com/gradio-app/gradio/commit/55f664f2979a49acc29a73cde16c6ebdfcc91db2) - Add event listener support to render blocks. Thanks @aliabid94! +- [#8398](https://github.com/gradio-app/gradio/pull/8398) [`945ac83`](https://github.com/gradio-app/gradio/commit/945ac837e779b120790814ea6f6f81bd2712f5f8) - Improve rendering. Thanks @aliabid94! +- [#8299](https://github.com/gradio-app/gradio/pull/8299) [`ab65360`](https://github.com/gradio-app/gradio/commit/ab653608045ff9462db7ad9fe63e1c60bf20e773) - Allow JS Client to work with authenticated spaces 🍪. Thanks @hannahblair! + +### Fixes + +- [#8408](https://github.com/gradio-app/gradio/pull/8408) [`e86dd01`](https://github.com/gradio-app/gradio/commit/e86dd01b6e8f7bab3d3c25b84f2ad33129138af4) - Connect heartbeat if state created in render. Also fix config cleanup bug #8407. Thanks @freddyaboulton! +- [#8258](https://github.com/gradio-app/gradio/pull/8258) [`1f8e5c4`](https://github.com/gradio-app/gradio/commit/1f8e5c44e054b943052d8f24d044696ddfd01a54) - Improve URL handling in JS Client. Thanks @hannahblair! + +## 0.19.4 + +### Fixes + +- [#8322](https://github.com/gradio-app/gradio/pull/8322) [`47012a0`](https://github.com/gradio-app/gradio/commit/47012a0c4e3e8a80fcae620aaf08b16ceb343cde) - ensure the client correctly handles all binary data. Thanks @Saghen! + +## 0.19.3 + +### Features + +- [#8229](https://github.com/gradio-app/gradio/pull/8229) [`7c81897`](https://github.com/gradio-app/gradio/commit/7c81897076ddcd0bb05e0e4ffec35bb9a986d330) - chore(deps): update dependency esbuild to ^0.21.0. Thanks @renovate! + +### Fixes + +- [#8296](https://github.com/gradio-app/gradio/pull/8296) [`929d216`](https://github.com/gradio-app/gradio/commit/929d216d49aa05614bc83f0761cf7b1cd803d8fe) - always create a jwt when connecting to a space if a hf_token is present. Thanks @pngwn! + +## 0.19.2 + +### Fixes + +- [#8285](https://github.com/gradio-app/gradio/pull/8285) [`7d9d8ea`](https://github.com/gradio-app/gradio/commit/7d9d8eab50d36cbecbb84c6a0f3cc1bca7215604) - use the correct query param to pass the jwt to the heartbeat event. Thanks @pngwn! + +## 0.19.1 + +### Fixes + +- [#8272](https://github.com/gradio-app/gradio/pull/8272) [`fbf4edd`](https://github.com/gradio-app/gradio/commit/fbf4edde7c896cdf4c903463e44c31ed96111b3c) - ensure client works for private spaces. Thanks @pngwn! + +## 0.19.0 + +### Features + +- [#8110](https://github.com/gradio-app/gradio/pull/8110) [`5436031`](https://github.com/gradio-app/gradio/commit/5436031f92c1596282eb64e1e74d555f279e9697) - Render decorator 2. Thanks @aliabid94! +- [#8197](https://github.com/gradio-app/gradio/pull/8197) [`e09b4e8`](https://github.com/gradio-app/gradio/commit/e09b4e8216b970bc1b142a0f08e7d190b954eb35) - Add support for passing keyword args to `data` in JS client. Thanks @hannahblair! + +### Fixes + +- [#8252](https://github.com/gradio-app/gradio/pull/8252) [`22df61a`](https://github.com/gradio-app/gradio/commit/22df61a26adf8023f6dd49c051979990e8d3879a) - Client node fix. Thanks @pngwn! + +## 0.18.0 + +### Features + +- [#8121](https://github.com/gradio-app/gradio/pull/8121) [`f5b710c`](https://github.com/gradio-app/gradio/commit/f5b710c919b0ce604ea955f0d5f4faa91095ca4a) - chore(deps): update dependency eslint to v9. Thanks @renovate! +- [#8209](https://github.com/gradio-app/gradio/pull/8209) [`b9afe93`](https://github.com/gradio-app/gradio/commit/b9afe93915401df5bd6737c89395c2477acfa585) - Rename `eventSource_Factory` and `fetch_implementation`. Thanks @hannahblair! +- [#8109](https://github.com/gradio-app/gradio/pull/8109) [`bed2f82`](https://github.com/gradio-app/gradio/commit/bed2f82e2297b50f7b59423a3de05af0b9910724) - Implement JS Client tests. Thanks @hannahblair! +- [#8211](https://github.com/gradio-app/gradio/pull/8211) [`91b5cd6`](https://github.com/gradio-app/gradio/commit/91b5cd6132fb8903c92f70fce0800324836a1fc3) - remove redundant event source logic. Thanks @hannahblair! + +### Fixes + +- [#8179](https://github.com/gradio-app/gradio/pull/8179) [`6a218b4`](https://github.com/gradio-app/gradio/commit/6a218b4148095aaa0c58d8c20973ba01c8764fc2) - rework upload to be a class method + pass client into each component. Thanks @pngwn! +- [#8181](https://github.com/gradio-app/gradio/pull/8181) [`cf52ca6`](https://github.com/gradio-app/gradio/commit/cf52ca6a51320ece97f009a177792840b5fbc785) - Ensure connectivity to private HF spaces with SSE protocol. Thanks @hannahblair! +- [#8169](https://github.com/gradio-app/gradio/pull/8169) [`3a6f1a5`](https://github.com/gradio-app/gradio/commit/3a6f1a50b263e0a733f609a08019fc4d05480e1a) - Only connect to heartbeat if needed. Thanks @freddyaboulton! +- [#8118](https://github.com/gradio-app/gradio/pull/8118) [`7aca673`](https://github.com/gradio-app/gradio/commit/7aca673b38a087533524b2fd8dd3a03e0e4bacfe) - Add eventsource polyfill for Node.js and browser environments. Thanks @hannahblair! + +## 0.17.0 + +### Highlights + +#### Setting File Upload Limits ([#7909](https://github.com/gradio-app/gradio/pull/7909) [`2afca65`](https://github.com/gradio-app/gradio/commit/2afca6541912b37dc84f447c7ad4af21607d7c72)) + +We have added a `max_file_size` size parameter to `launch()` that limits to size of files uploaded to the server. This limit applies to each individual file. This parameter can be specified as a string or an integer (corresponding to the size in bytes). + +The following code snippet sets a max file size of 5 megabytes. + +```python +import gradio as gr + +demo = gr.Interface(lambda x: x, "image", "image") + +demo.launch(max_file_size="5mb") +# or +demo.launch(max_file_size=5 * gr.FileSize.MB) +``` + +![max_file_size_upload](https://github.com/gradio-app/gradio/assets/41651716/7547330c-a082-4901-a291-3f150a197e45) + + +#### Error states can now be cleared + +When a component encounters an error, the error state shown in the UI can now be cleared by clicking on the `x` icon in the top right of the component. This applies to all types of errors, whether it's raised in the UI or the server. + +![error_modal_calculator](https://github.com/gradio-app/gradio/assets/41651716/16cb071c-accd-45a6-9c18-0dea27d4bd98) + + Thanks @freddyaboulton! + +### Features + +- [#8056](https://github.com/gradio-app/gradio/pull/8056) [`2e469a5`](https://github.com/gradio-app/gradio/commit/2e469a5f99e52a5011a010f46e47dde7bb0c7140) - Using keys to preserve values between reloads. Thanks @aliabid94! +- [#7646](https://github.com/gradio-app/gradio/pull/7646) [`450b8cc`](https://github.com/gradio-app/gradio/commit/450b8cc898f130f15caa3742f65c17b9f7a8f398) - Refactor JS Client. Thanks @hannahblair! +- [#8061](https://github.com/gradio-app/gradio/pull/8061) [`17e83c9`](https://github.com/gradio-app/gradio/commit/17e83c958ebb35b3e122ca486067d1bd5ce33a22) - Docs Reorg and Intro Page. Thanks @aliabd! + +### Fixes + +- [#8066](https://github.com/gradio-app/gradio/pull/8066) [`624f9b9`](https://github.com/gradio-app/gradio/commit/624f9b9477f74a581a6c14119234f9efdfcda398) - make gradio dev tools a local dependency rather than bundling. Thanks @pngwn! + +## 0.16.0 + +### Features + +- [#7845](https://github.com/gradio-app/gradio/pull/7845) [`dbb7373`](https://github.com/gradio-app/gradio/commit/dbb7373dde69d4ed2741942b5a1898f8620cec24) - ensure `ImageEditor` events work as expected. Thanks @pngwn! + +### Fixes + +- [#7974](https://github.com/gradio-app/gradio/pull/7974) [`79e0aa8`](https://github.com/gradio-app/gradio/commit/79e0aa81c94e755faa6e85d76ac5d5a666313e6a) - Fix heartbeat in the js client to be Lite compatible. Thanks @whitphx! + +## 0.15.1 + +### Fixes + +- [#7926](https://github.com/gradio-app/gradio/pull/7926) [`9666854`](https://github.com/gradio-app/gradio/commit/966685479078f59430b3bced7e6068eb8157c003) - Fixes streaming event race condition. Thanks @aliabid94! + +## 0.15.0 + +### Highlights + +#### Automatically delete state after user has disconnected from the webpage ([#7829](https://github.com/gradio-app/gradio/pull/7829) [`6a4bf7a`](https://github.com/gradio-app/gradio/commit/6a4bf7abe29059dbdc6a342e0366fdaa2e4120ee)) + +Gradio now automatically deletes `gr.State` variables stored in the server's RAM when users close their browser tab. +The deletion will happen 60 minutes after the server detected a disconnect from the user's browser. +If the user connects again in that timeframe, their state will not be deleted. + +Additionally, Gradio now includes a `Blocks.unload()` event, allowing you to run arbitrary cleanup functions when users disconnect (this does not have a 60 minute delay). +You can think of the `unload` event as the opposite of the `load` event. + + +```python +with gr.Blocks() as demo: + gr.Markdown( +"""# State Cleanup Demo +🖼️ Images are saved in a user-specific directory and deleted when the users closes the page via demo.unload. +""") + with gr.Row(): + with gr.Column(scale=1): + with gr.Row(): + img = gr.Image(label="Generated Image", height=300, width=300) + with gr.Row(): + gen = gr.Button(value="Generate") + with gr.Row(): + history = gr.Gallery(label="Previous Generations", height=500, columns=10) + state = gr.State(value=[], delete_callback=lambda v: print("STATE DELETED")) + + demo.load(generate_random_img, [state], [img, state, history]) + gen.click(generate_random_img, [state], [img, state, history]) + demo.unload(delete_directory) + + +demo.launch(auth=lambda user,pwd: True, + auth_message="Enter any username and password to continue") +``` + + Thanks @freddyaboulton! + +## 0.14.0 + +### Features + +- [#7691](https://github.com/gradio-app/gradio/pull/7691) [`84f81fe`](https://github.com/gradio-app/gradio/commit/84f81fec9287b041203a141bbf2852720f7d199c) - Closing stream from the backend. Thanks @aliabid94! + +### Fixes + +- [#7564](https://github.com/gradio-app/gradio/pull/7564) [`5d1e8da`](https://github.com/gradio-app/gradio/commit/5d1e8dae5ac23f605c3b5f41dbe18751dff380a0) - batch UI updates on a per frame basis. Thanks @pngwn! + +## 0.13.0 + +### Fixes + +- [#7575](https://github.com/gradio-app/gradio/pull/7575) [`d0688b3`](https://github.com/gradio-app/gradio/commit/d0688b3c25feabb4fc7dfa0ab86086b3af7eb337) - Files should now be supplied as `file(...)` in the Client, and some fixes to `gr.load()` as well. Thanks @abidlabs! + +## 0.12.2 + +### Features + +- [#7528](https://github.com/gradio-app/gradio/pull/7528) [`eda33b3`](https://github.com/gradio-app/gradio/commit/eda33b3763897a542acf298e523fa493dc655aee) - Refactors `get_fetchable_url_or_file()` to remove it from the frontend. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#7340](https://github.com/gradio-app/gradio/pull/7340) [`4b0d589`](https://github.com/gradio-app/gradio/commit/4b0d58933057432758a54169a360eb352903d6b4) - chore(deps): update all non-major dependencies. Thanks [@renovate](https://github.com/apps/renovate)! + +## 0.12.1 + +### Fixes + +- [#7411](https://github.com/gradio-app/gradio/pull/7411) [`32b317f`](https://github.com/gradio-app/gradio/commit/32b317f24e3d43f26684bb9f3964f31efd0ea556) - Set `root` correctly for Gradio apps that are deployed behind reverse proxies. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.12.0 + +### Features + +- [#7183](https://github.com/gradio-app/gradio/pull/7183) [`49d9c48`](https://github.com/gradio-app/gradio/commit/49d9c48537aa706bf72628e3640389470138bdc6) - [WIP] Refactor file normalization to be in the backend and remove it from the frontend of each component. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.11.0 + +### Features + +- [#7102](https://github.com/gradio-app/gradio/pull/7102) [`68a54a7`](https://github.com/gradio-app/gradio/commit/68a54a7a310d8d7072fdae930bf1cfdf12c45a7f) - Improve chatbot streaming performance with diffs. Thanks [@aliabid94](https://github.com/aliabid94)!/n Note that this PR changes the API format for generator functions, which would be a breaking change for any clients reading the EventStream directly + +## 0.10.1 + +### Fixes + +- [#7055](https://github.com/gradio-app/gradio/pull/7055) [`3c3cf86`](https://github.com/gradio-app/gradio/commit/3c3cf8618a8cad1ef66a7f96664923d2c9f5e0e2) - Fix UI freeze on rapid generators. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.10.0 + +### Features + +- [#6931](https://github.com/gradio-app/gradio/pull/6931) [`6c863af`](https://github.com/gradio-app/gradio/commit/6c863af92fa9ceb5c638857eb22cc5ddb718d549) - Fix functional tests. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#6820](https://github.com/gradio-app/gradio/pull/6820) [`649cd4d`](https://github.com/gradio-app/gradio/commit/649cd4d68041d11fcbe31f8efa455345ac49fc74) - Use `EventSource_factory` in `open_stream()` for Wasm. Thanks [@whitphx](https://github.com/whitphx)! + +## 0.9.4 + +### Fixes + +- [#6863](https://github.com/gradio-app/gradio/pull/6863) [`d406855`](https://github.com/gradio-app/gradio/commit/d4068557953746662235d595ec435c42ceb24414) - Fix JS Client when app is running behind a proxy. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.9.3 + +### Features + +- [#6814](https://github.com/gradio-app/gradio/pull/6814) [`828fb9e`](https://github.com/gradio-app/gradio/commit/828fb9e6ce15b6ea08318675a2361117596a1b5d) - Refactor queue so that there are separate queues for each concurrency id. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.9.2 + +### Features + +- [#6798](https://github.com/gradio-app/gradio/pull/6798) [`245d58e`](https://github.com/gradio-app/gradio/commit/245d58eff788e8d44a59d37a2d9b26d0f08a62b4) - Improve how server/js client handle unexpected errors. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.9.1 + +### Fixes + +- [#6693](https://github.com/gradio-app/gradio/pull/6693) [`34f9431`](https://github.com/gradio-app/gradio/commit/34f943101bf7dd6b8a8974a6131c1ed7c4a0dac0) - Python client properly handles hearbeat and log messages. Also handles responses longer than 65k. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.9.0 + +### Features + +- [#6398](https://github.com/gradio-app/gradio/pull/6398) [`67ddd40`](https://github.com/gradio-app/gradio/commit/67ddd40b4b70d3a37cb1637c33620f8d197dbee0) - Lite v4. Thanks [@whitphx](https://github.com/whitphx)! + +### Fixes + +- [#6556](https://github.com/gradio-app/gradio/pull/6556) [`d76bcaa`](https://github.com/gradio-app/gradio/commit/d76bcaaaf0734aaf49a680f94ea9d4d22a602e70) - Fix api event drops. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.8.2 + +### Features + +- [#6511](https://github.com/gradio-app/gradio/pull/6511) [`71f1a1f99`](https://github.com/gradio-app/gradio/commit/71f1a1f9931489d465c2c1302a5c8d768a3cd23a) - Mark `FileData.orig_name` optional on the frontend aligning the type definition on the Python side. Thanks [@whitphx](https://github.com/whitphx)! + +## 0.8.1 + +### Fixes + +- [#6383](https://github.com/gradio-app/gradio/pull/6383) [`324867f63`](https://github.com/gradio-app/gradio/commit/324867f63c920113d89a565892aa596cf8b1e486) - Fix event target. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.8.0 + +### Features + +- [#6307](https://github.com/gradio-app/gradio/pull/6307) [`f1409f95e`](https://github.com/gradio-app/gradio/commit/f1409f95ed39c5565bed6a601e41f94e30196a57) - Provide status updates on file uploads. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.7.2 + +### Fixes + +- [#6327](https://github.com/gradio-app/gradio/pull/6327) [`bca6c2c80`](https://github.com/gradio-app/gradio/commit/bca6c2c80f7e5062427019de45c282238388af95) - Restore query parameters in request. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.7.1 + +### Features + +- [#6137](https://github.com/gradio-app/gradio/pull/6137) [`2ba14b284`](https://github.com/gradio-app/gradio/commit/2ba14b284f908aa13859f4337167a157075a68eb) - JS Param. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! + +## 0.7.0 + +### Features + +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - fix circular dependency with client + upload. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Image v4. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Swap websockets for SSE. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.7.0-beta.1 + +### Features + +- [#6143](https://github.com/gradio-app/gradio/pull/6143) [`e4f7b4b40`](https://github.com/gradio-app/gradio/commit/e4f7b4b409323b01aa01b39e15ce6139e29aa073) - fix circular dependency with client + upload. Thanks [@pngwn](https://github.com/pngwn)! +- [#6094](https://github.com/gradio-app/gradio/pull/6094) [`c476bd5a5`](https://github.com/gradio-app/gradio/commit/c476bd5a5b70836163b9c69bf4bfe068b17fbe13) - Image v4. Thanks [@pngwn](https://github.com/pngwn)! +- [#6069](https://github.com/gradio-app/gradio/pull/6069) [`bf127e124`](https://github.com/gradio-app/gradio/commit/bf127e1241a41401e144874ea468dff8474eb505) - Swap websockets for SSE. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.7.0-beta.0 + +### Features + +- [#6016](https://github.com/gradio-app/gradio/pull/6016) [`83e947676`](https://github.com/gradio-app/gradio/commit/83e947676d327ca2ab6ae2a2d710c78961c771a0) - Format js in v4 branch. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Fixes + +- [#6046](https://github.com/gradio-app/gradio/pull/6046) [`dbb7de5e0`](https://github.com/gradio-app/gradio/commit/dbb7de5e02c53fee05889d696d764d212cb96c74) - fix tests. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.6.0 + +### Features + +- [#5972](https://github.com/gradio-app/gradio/pull/5972) [`11a300791`](https://github.com/gradio-app/gradio/commit/11a3007916071f0791844b0a37f0fb4cec69cea3) - Lite: Support opening the entrypoint HTML page directly in browser via the `file:` protocol. Thanks [@whitphx](https://github.com/whitphx)! + +## 0.5.2 + +### Fixes + +- [#5840](https://github.com/gradio-app/gradio/pull/5840) [`4e62b8493`](https://github.com/gradio-app/gradio/commit/4e62b8493dfce50bafafe49f1a5deb929d822103) - Ensure websocket polyfill doesn't load if there is already a `global.Webocket` property set. Thanks [@Jay2theWhy](https://github.com/Jay2theWhy)! + +## 0.5.1 + +### Fixes + +- [#5816](https://github.com/gradio-app/gradio/pull/5816) [`796145e2c`](https://github.com/gradio-app/gradio/commit/796145e2c48c4087bec17f8ec0be4ceee47170cb) - Fix calls to the component server so that `gr.FileExplorer` works on Spaces. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.5.0 + +### Highlights + +#### new `FileExplorer` component ([#5672](https://github.com/gradio-app/gradio/pull/5672) [`e4a307ed6`](https://github.com/gradio-app/gradio/commit/e4a307ed6cde3bbdf4ff2f17655739addeec941e)) + +Thanks to a new capability that allows components to communicate directly with the server _without_ passing data via the value, we have created a new `FileExplorer` component. + +This component allows you to populate the explorer by passing a glob, but only provides the selected file(s) in your prediction function. + +Users can then navigate the virtual filesystem and select files which will be accessible in your predict function. This component will allow developers to build more complex spaces, with more flexible input options. + +![output](https://github.com/pngwn/MDsveX/assets/12937446/ef108f0b-0e84-4292-9984-9dc66b3e144d) + +For more information check the [`FileExplorer` documentation](https://gradio.app/docs/fileexplorer). + + Thanks [@aliabid94](https://github.com/aliabid94)! + +### Features + +- [#5787](https://github.com/gradio-app/gradio/pull/5787) [`caeee8bf7`](https://github.com/gradio-app/gradio/commit/caeee8bf7821fd5fe2f936ed82483bed00f613ec) - ensure the client does not depend on `window` when running in a node environment. Thanks [@gibiee](https://github.com/gibiee)! + +### Fixes + +- [#5776](https://github.com/gradio-app/gradio/pull/5776) [`c0fef4454`](https://github.com/gradio-app/gradio/commit/c0fef44541bfa61568bdcfcdfc7d7d79869ab1df) - Revert replica proxy logic and instead implement using the `root` variable. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.4.2 + +### Features + +- [#5124](https://github.com/gradio-app/gradio/pull/5124) [`6e56a0d9b`](https://github.com/gradio-app/gradio/commit/6e56a0d9b0c863e76c69e1183d9d40196922b4cd) - Lite: Websocket queueing. Thanks [@whitphx](https://github.com/whitphx)! + +## 0.4.1 + +### Fixes + +- [#5705](https://github.com/gradio-app/gradio/pull/5705) [`78e7cf516`](https://github.com/gradio-app/gradio/commit/78e7cf5163e8d205e8999428fce4c02dbdece25f) - ensure internal data has updated before dispatching `success` or `then` events. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.4.0 + +### Features + +- [#5682](https://github.com/gradio-app/gradio/pull/5682) [`c57f1b75e`](https://github.com/gradio-app/gradio/commit/c57f1b75e272c76b0af4d6bd0c7f44743ff34f26) - Fix functional tests. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5681](https://github.com/gradio-app/gradio/pull/5681) [`40de3d217`](https://github.com/gradio-app/gradio/commit/40de3d2178b61ebe424b6f6228f94c0c6f679bea) - add query parameters to the `gr.Request` object through the `query_params` attribute. Thanks [@DarhkVoyd](https://github.com/DarhkVoyd)! +- [#5653](https://github.com/gradio-app/gradio/pull/5653) [`ea0e00b20`](https://github.com/gradio-app/gradio/commit/ea0e00b207b4b90a10e9d054c4202d4e705a29ba) - Prevent Clients from accessing API endpoints that set `api_name=False`. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.3.1 + +### Fixes + +- [#5412](https://github.com/gradio-app/gradio/pull/5412) [`26fef8c7`](https://github.com/gradio-app/gradio/commit/26fef8c7f85a006c7e25cdbed1792df19c512d02) - Skip view_api request in js client when auth enabled. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.3.0 + +### Features + +- [#5267](https://github.com/gradio-app/gradio/pull/5267) [`119c8343`](https://github.com/gradio-app/gradio/commit/119c834331bfae60d4742c8f20e9cdecdd67e8c2) - Faster reload mode. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.2.1 + +### Features + +- [#5173](https://github.com/gradio-app/gradio/pull/5173) [`730f0c1d`](https://github.com/gradio-app/gradio/commit/730f0c1d54792eb11359e40c9f2326e8a6e39203) - Ensure gradio client works as expected for functions that return nothing. Thanks [@raymondtri](https://github.com/raymondtri)! + +## 0.2.0 + +### Features + +- [#5133](https://github.com/gradio-app/gradio/pull/5133) [`61129052`](https://github.com/gradio-app/gradio/commit/61129052ed1391a75c825c891d57fa0ad6c09fc8) - Update dependency esbuild to ^0.19.0. Thanks [@renovate](https://github.com/apps/renovate)! +- [#5035](https://github.com/gradio-app/gradio/pull/5035) [`8b4eb8ca`](https://github.com/gradio-app/gradio/commit/8b4eb8cac9ea07bde31b44e2006ca2b7b5f4de36) - JS Client: Fixes cannot read properties of null (reading 'is_file'). Thanks [@raymondtri](https://github.com/raymondtri)! + +### Fixes + +- [#5075](https://github.com/gradio-app/gradio/pull/5075) [`67265a58`](https://github.com/gradio-app/gradio/commit/67265a58027ef1f9e4c0eb849a532f72eaebde48) - Allow supporting >1000 files in `gr.File()` and `gr.UploadButton()`. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.1.4 + +### Patch Changes + +- [#4717](https://github.com/gradio-app/gradio/pull/4717) [`ab5d1ea0`](https://github.com/gradio-app/gradio/commit/ab5d1ea0de87ed888779b66fd2a705583bd29e02) Thanks [@whitphx](https://github.com/whitphx)! - Fix the package description + +## 0.1.3 + +### Patch Changes + +- [#4357](https://github.com/gradio-app/gradio/pull/4357) [`0dbd8f7f`](https://github.com/gradio-app/gradio/commit/0dbd8f7fee4b4877f783fa7bc493f98bbfc3d01d) Thanks [@pngwn](https://github.com/pngwn)! - Various internal refactors and cleanups. + +## 0.1.2 + +### Patch Changes + +- [#4273](https://github.com/gradio-app/gradio/pull/4273) [`1d0f0a9d`](https://github.com/gradio-app/gradio/commit/1d0f0a9db096552e67eb2197c932342587e9e61e) Thanks [@pngwn](https://github.com/pngwn)! - Ensure websocket error messages are correctly handled. + +- [#4315](https://github.com/gradio-app/gradio/pull/4315) [`b525b122`](https://github.com/gradio-app/gradio/commit/b525b122dd8569bbaf7e06db5b90d622d2e9073d) Thanks [@whitphx](https://github.com/whitphx)! - Refacor types. + +- [#4271](https://github.com/gradio-app/gradio/pull/4271) [`1151c525`](https://github.com/gradio-app/gradio/commit/1151c5253554cb87ebd4a44a8a470ac215ff782b) Thanks [@pngwn](https://github.com/pngwn)! - Ensure the full root path is always respected when making requests to a gradio app server. + +## 0.1.1 + +### Patch Changes + +- [#4201](https://github.com/gradio-app/gradio/pull/4201) [`da5b4ee1`](https://github.com/gradio-app/gradio/commit/da5b4ee11721175858ded96e5710225369097f74) Thanks [@pngwn](https://github.com/pngwn)! - Ensure semiver is bundled so CDN links work correctly. + +- [#4202](https://github.com/gradio-app/gradio/pull/4202) [`a26e9afd`](https://github.com/gradio-app/gradio/commit/a26e9afde319382993e6ddc77cc4e56337a31248) Thanks [@pngwn](https://github.com/pngwn)! - Ensure all URLs returned by the client are complete URLs with the correct host instead of an absolute path relative to a server. + +## 0.1.0 + +### Minor Changes + +- [#4185](https://github.com/gradio-app/gradio/pull/4185) [`67239ca9`](https://github.com/gradio-app/gradio/commit/67239ca9b2fe3796853fbf7bf865c9e4b383200d) Thanks [@pngwn](https://github.com/pngwn)! - Update client for initial release + +### Patch Changes + +- [#3692](https://github.com/gradio-app/gradio/pull/3692) [`48e8b113`](https://github.com/gradio-app/gradio/commit/48e8b113f4b55e461d9da4f153bf72aeb4adf0f1) Thanks [@pngwn](https://github.com/pngwn)! - Ensure client works in node, create ESM bundle and generate typescript declaration files. + +- [#3605](https://github.com/gradio-app/gradio/pull/3605) [`ae4277a9`](https://github.com/gradio-app/gradio/commit/ae4277a9a83d49bdadfe523b0739ba988128e73b) Thanks [@pngwn](https://github.com/pngwn)! - Update readme. \ No newline at end of file diff --git a/client/js/README.md b/client/js/README.md new file mode 100644 index 0000000..4f79bf0 --- /dev/null +++ b/client/js/README.md @@ -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 + +``` + +## 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, +}); +``` \ No newline at end of file diff --git a/client/js/index.html b/client/js/index.html new file mode 100644 index 0000000..1afd437 --- /dev/null +++ b/client/js/index.html @@ -0,0 +1,39 @@ + + + + + + + Client + + + +
+ + diff --git a/client/js/package-lock.json b/client/js/package-lock.json new file mode 100644 index 0000000..7aa85d7 --- /dev/null +++ b/client/js/package-lock.json @@ -0,0 +1,2093 @@ +{ + "name": "@gradio/client", + "version": "1.19.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@gradio/client", + "version": "1.19.1", + "license": "ISC", + "dependencies": { + "@types/eventsource": "^1.1.15", + "bufferutil": "^4.0.7", + "eventsource": "^2.0.2", + "fetch-event-stream": "^0.1.5", + "msw": "^2.2.1", + "semiver": "^1.1.0", + "textlinestream": "^1.1.1", + "typescript": "^5.0.0", + "vi": "^1.0.0", + "vite": "^6.4.1", + "ws": "^8.13.0" + }, + "devDependencies": { + "@types/ws": "^8.5.10", + "esbuild": "^0.21.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@inquirer/ansi/-/ansi-1.0.1.tgz", + "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.19", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@inquirer/confirm/-/confirm-5.1.19.tgz", + "integrity": "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@inquirer/core/-/core-10.3.0.tgz", + "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.1", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.14", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@inquirer/figures/-/figures-1.0.14.tgz", + "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.9", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@inquirer/type/-/type-3.0.9.tgz", + "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.40.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@mswjs/interceptors/-/interceptors-0.40.0.tgz", + "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/eventsource": { + "version": "1.1.15", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@types/eventsource/-/eventsource-1.1.15.tgz", + "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.8.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/bufferutil": { + "version": "4.0.9", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/bufferutil/-/bufferutil-4.0.9.tgz", + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-event-stream": { + "version": "0.1.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/fetch-event-stream/-/fetch-event-stream-0.1.5.tgz", + "integrity": "sha512-V1PWovkspxQfssq/NnxoEyQo1DV+MRK/laPuPblIZmSjMN8P5u46OhlFQznSr9p/t0Sp8Uc6SbM3yCMfr0KU8g==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.11.6", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/msw/-/msw-2.11.6.tgz", + "integrity": "sha512-MCYMykvmiYScyUm7I6y0VCxpNq1rgd5v7kG8ks5dKtvmxRUUPjribX6mUoUNBbM5/3PhUyoelEWiKXGOz84c+w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.40.0", + "@open-draft/deferred-promise": "^2.2.0", + "@types/statuses": "^2.0.4", + "cookie": "^1.0.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.7.0", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.0", + "type-fest": "^4.26.1", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rettime": { + "version": "0.7.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/rettime/-/rettime-0.7.0.tgz", + "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/semiver": { + "version": "1.1.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/semiver/-/semiver-1.1.0.tgz", + "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/textlinestream": { + "version": "1.1.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/textlinestream/-/textlinestream-1.1.1.tgz", + "integrity": "sha512-iBHbi7BQxrFmwZUQJsT0SjNzlLLsXhvW/kg7EyOMVMBIrlnj/qYofwo1LVLZi+3GbUEo96Iu2eqToI2+lZoAEQ==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, + "node_modules/vi": { + "version": "1.0.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/vi/-/vi-1.0.0.tgz", + "integrity": "sha512-fLpBeQzBddayFzoNMfCzvkcomwRYHx05qRo8lVCjMmR8ggs36Auw5A6/yH/UU8e2UAsGvh0qmXlNoR1DIdfWUw==", + "license": "ISC" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.11", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://repos.verz.local/artifactory/api/npm/npm/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/client/js/package.json b/client/js/package.json new file mode 100644 index 0000000..0d67308 --- /dev/null +++ b/client/js/package.json @@ -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" + } +} diff --git a/client/js/src/client.ts b/client/js/src/client.ts new file mode 100644 index 0000000..047b22c --- /dev/null +++ b/client/js/src/client.ts @@ -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 | undefined; + api_map: Record = {}; + session_hash: string = Math.random().toString(36).substring(2); + jwt: string | false = false; + last_status: Record = {}; + + private cookies: string | null = null; + + // streaming + stream_status = { open: false }; + closed = false; + pending_stream_messages: Record = {}; + pending_diff_streams: Record = {}; + event_callbacks: Record Promise> = {}; + unclosed_events: Set = 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 { + 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>; + upload_files: ( + root_url: string, + files: (Blob | File)[], + upload_id?: string + ) => Promise; + 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 + ) => Promise; + post_data: ( + url: string, + body: unknown, + additional_headers?: any + ) => Promise; + submit: ( + endpoint: string | number, + data: unknown[] | Record | undefined, + event_data?: unknown, + trigger_id?: number | null, + all_events?: boolean + ) => SubmitIterable; + predict: ( + endpoint: string | number, + data: unknown[] | Record | undefined, + event_data?: unknown + ) => Promise>; + open_stream: () => Promise; + private resolve_config: (endpoint: string) => Promise; + private resolve_cookies: () => Promise; + 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 { + 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 { + 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 { + 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 { + return duplicate(app_reference, options); + } + + private async _resolve_config(): Promise { + 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 { + 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 { + 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 } + ): Promise { + 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 }; + 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} A promise that resolves to a `Client` instance. + */ +export async function client( + app_reference: string, + options: ClientOptions = { + events: ["data"] + } +): Promise { + 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} A promise that resolves to a `Client` instance. + */ +export async function duplicate_space( + app_reference: string, + options: DuplicateOptions +): Promise { + return await Client.duplicate(app_reference, options); +} + +export type ClientInstance = Client; diff --git a/client/js/src/constants.ts b/client/js/src/constants.ts new file mode 100644 index 0000000..ba0e83b --- /dev/null +++ b/client/js/src/constants.ts @@ -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"; diff --git a/client/js/src/globals.d.ts b/client/js/src/globals.d.ts new file mode 100644 index 0000000..1ba2e1c --- /dev/null +++ b/client/js/src/globals.d.ts @@ -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 | { api: ApiInfo }; + __is_colab__: boolean; + __gradio_space__: string | null; + supports_zerogpu_headers?: boolean; + BUILD_MODE?: "dev" | "production"; + } +} diff --git a/client/js/src/helpers/api_info.ts b/client/js/src/helpers/api_info.ts new file mode 100644 index 0000000..8da9410 --- /dev/null +++ b/client/js/src/helpers/api_info.ts @@ -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, + config: Config, + api_map: Record +): ApiInfo { + const transformed_info: ApiInfo = { + 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} 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 = [], + endpoint_info: EndpointInfo +): 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; +}; diff --git a/client/js/src/helpers/data.ts b/client/js/src/helpers/data.ts new file mode 100644 index 0000000..42aad8d --- /dev/null +++ b/client/js/src/helpers/data.ts @@ -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 | undefined = undefined +): Promise { + 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( + message: any, + origin: string +): Promise { + return new Promise((res, _rej) => { + const channel = new MessageChannel(); + channel.port1.onmessage = (({ data }) => { + channel.port1.close(); + res(data); + }) as (ev: MessageEvent) => 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; +} diff --git a/client/js/src/helpers/init_helpers.ts b/client/js/src/helpers/init_helpers.ts new file mode 100644 index 0000000..94b1e28 --- /dev/null +++ b/client/js/src/helpers/init_helpers.ts @@ -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 { + 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 { + let apis: Record = {}; + + fns.forEach(({ api_name, id }) => { + if (api_name) apis[api_name] = id; + }); + return apis; +} + +export async function resolve_config( + this: Client, + endpoint: string +): Promise { + const headers: Record = 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 { + 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 { + 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 { + 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; +}; diff --git a/client/js/src/helpers/spaces.ts b/client/js/src/helpers/spaces.ts new file mode 100644 index 0000000..81875f4 --- /dev/null +++ b/client/js/src/helpers/spaces.ts @@ -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 { + 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 => { + 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 { + 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 { + 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; diff --git a/client/js/src/helpers/zerogpu.ts b/client/js/src/helpers/zerogpu.ts new file mode 100644 index 0000000..54b56af --- /dev/null +++ b/client/js/src/helpers/zerogpu.ts @@ -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); + } +} diff --git a/client/js/src/index.ts b/client/js/src/index.ts new file mode 100644 index 0000000..6ea916a --- /dev/null +++ b/client/js/src/index.ts @@ -0,0 +1,26 @@ +export { Client } from "./client"; + +export { predict } from "./utils/predict"; +export { submit } from "./utils/submit"; +export { upload_files } from "./utils/upload_files"; +export { FileData, upload, prepare_files } from "./upload"; +export { handle_file } from "./helpers/data"; + +export type { + SpaceStatus, + StatusMessage, + Status, + client_return, + UploadResponse, + RenderMessage, + LogMessage, + Payload, + Config, + ValidationError +} from "./types"; + +export { MISSING_CREDENTIALS_MSG } from "./constants"; + +// todo: remove in @gradio/client v1.0 +export { client } from "./client"; +export { duplicate_space as duplicate } from "./client"; diff --git a/client/js/src/test/api_info.test.ts b/client/js/src/test/api_info.test.ts new file mode 100644 index 0000000..2bafff8 --- /dev/null +++ b/client/js/src/test/api_info.test.ts @@ -0,0 +1,701 @@ +import { + INVALID_URL_MSG, + QUEUE_FULL_MSG, + SPACE_METADATA_ERROR_MSG +} from "../constants"; +import { beforeAll, afterEach, afterAll, it, expect, describe } from "vitest"; +import { + handle_message, + get_description, + get_type, + process_endpoint, + join_urls, + map_data_to_params +} from "../helpers/api_info"; +import { initialise_server } from "./server"; +import { transformed_api_info } from "./test_data"; + +let server: Awaited>; + +beforeAll(async () => { + server = await initialise_server(); + await server.start({ quiet: true }); +}); +afterEach(() => server.resetHandlers()); +afterAll(() => server.stop()); + +describe("handle_message", () => { + it("should return type 'data' when msg is 'send_data'", () => { + const data = { msg: "send_data" }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ type: "data" }); + }); + + it("should return type 'hash' when msg is 'send_hash'", () => { + const data = { msg: "send_hash" }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ type: "hash" }); + }); + + it("should return type 'update' with queue full message when msg is 'queue_full'", () => { + const data = { msg: "queue_full", code: 500, success: false }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ + type: "update", + status: { + queue: true, + message: QUEUE_FULL_MSG, + stage: "error", + code: 500, + success: false + } + }); + }); + + it("should return type 'heartbeat' when msg is 'heartbeat'", () => { + const data = { msg: "heartbeat" }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ type: "heartbeat" }); + }); + + it("should return type 'unexpected_error' with error message when msg is 'unexpected_error'", () => { + const data = { msg: "unexpected_error", message: "Something went wrong" }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ + type: "unexpected_error", + status: { + queue: true, + message: "Something went wrong", + stage: "error", + success: false + } + }); + }); + + it("should return type 'update' with estimation status when msg is 'estimation'", () => { + const data = { + msg: "estimation", + code: 200, + queue_size: 10, + rank: 5, + rank_eta: 60, + success: true + }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ + type: "update", + status: { + queue: true, + stage: "pending", + code: 200, + size: 10, + position: 5, + eta: 60, + success: true + } + }); + }); + + it("should return type 'update' with progress status when msg is 'progress'", () => { + const data = { + msg: "progress", + code: 200, + progress_data: { current: 50, total: 100 }, + success: true + }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ + type: "update", + status: { + queue: true, + stage: "pending", + code: 200, + progress_data: { current: 50, total: 100 }, + success: true + } + }); + }); + + it("should return type 'log' with the provided data when msg is 'log'", () => { + const data = { msg: "log", log_data: "Some log message" }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ + type: "log", + data: { msg: "log", log_data: "Some log message" } + }); + }); + + it("should return type 'generating' with generating status when msg is 'process_generating' and success is true", () => { + const data = { + msg: "process_generating", + success: true, + code: 200, + progress_data: { current: 50, total: 100 }, + average_duration: 120, + output: { result: "Some result" } + }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ + type: "generating", + status: { + queue: true, + message: null, + stage: "generating", + code: 200, + progress_data: { current: 50, total: 100 }, + eta: 120 + }, + data: { result: "Some result" } + }); + }); + + it("should return type 'update' with error status when msg is 'process_generating' and success is false", () => { + const data = { + msg: "process_generating", + success: false, + code: 500, + progress_data: { current: 50, total: 100 }, + average_duration: 120, + output: { error: "Error" } + }; + const last_status = "pending"; + const result = handle_message(data, last_status); + + expect(result).toEqual({ + type: "generating", + data: null, + status: { + eta: 120, + queue: true, + message: "Error", + stage: "error", + code: 500, + progress_data: { current: 50, total: 100 } + } + }); + }); + + it("should carry changed_state_ids in the streaming status when msg is 'process_streaming'", () => { + const data = { + msg: "process_streaming", + success: true, + code: 200, + time_limit: 30, + eta: 5, + progress_data: { current: 50, total: 100 }, + output: { data: [1], changed_state_ids: [3] } + }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ + type: "streaming", + status: { + queue: true, + message: undefined, + stage: "streaming", + time_limit: 30, + code: 200, + progress_data: { current: 50, total: 100 }, + changed_state_ids: [3], + eta: 5 + }, + data: { data: [1], changed_state_ids: [3] } + }); + }); + + it("should return type 'complete' with success status when msg is 'process_completed' and success is true", () => { + const data = { + msg: "process_completed", + success: true, + code: 200, + progress_data: { current: 100, total: 100 }, + output: { result: "Some result" } + }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ + type: "complete", + status: { + queue: true, + message: undefined, + stage: "complete", + code: 200, + progress_data: { current: 100, total: 100 } + }, + data: { result: "Some result" } + }); + }); + + it("should return type 'update' with error status when msg is 'process_completed' and success is false", () => { + const data = { + msg: "process_completed", + success: false, + code: 500, + duration: undefined, + progress_data: { current: 100, total: 100 }, + output: { error: "Some error message" }, + title: "Error", + visible: undefined + }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ + type: "update", + status: { + queue: true, + message: "Some error message", + stage: "error", + code: 500, + success: false, + duration: undefined, + title: "Error", + visible: undefined + } + }); + }); + + it("should return type 'update' with pending status when msg is 'process_starts'", () => { + const data = { + msg: "process_starts", + code: 200, + rank: 5, + success: true, + eta: 60 + }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ + type: "update", + original_msg: "process_starts", + status: { + queue: true, + stage: "pending", + code: 200, + size: 5, + position: 0, + success: true, + eta: 60 + } + }); + }); + + it("should return type 'none' with error status when msg is unknown", () => { + const data = { msg: "unknown" }; + const last_status = "pending"; + const result = handle_message(data, last_status); + expect(result).toEqual({ + type: "none", + status: { stage: "error", queue: true } + }); + }); +}); + +describe("get_description", () => { + it("should return 'array of [file, label] tuples' when serializer is 'GallerySerializable'", () => { + const type = { type: "string", description: "param description" }; + const serializer = "GallerySerializable"; + const result = get_description(type, serializer); + expect(result).toEqual("array of [file, label] tuples"); + }); + + it("should return 'array of strings' when serializer is 'ListStringSerializable'", () => { + const type = { type: "string", description: "param description" }; + const serializer = "ListStringSerializable"; + const result = get_description(type, serializer); + expect(result).toEqual("array of strings"); + }); + + it("should return 'array of files or single file' when serializer is 'FileSerializable'", () => { + const type = { type: "string", description: "param description" }; + const serializer = "FileSerializable"; + const result = get_description(type, serializer); + expect(result).toEqual("array of files or single file"); + }); + + it("should return the type's description when serializer is not 'GallerySerializable', 'ListStringSerializable', or 'FileSerializable'", () => { + const type = { type: "string", description: "param description" }; + const serializer = "SomeOtherSerializer"; + const result = get_description(type, serializer); + expect(result).toEqual(type.description); + }); +}); + +describe("get_type", () => { + it("should return 'string' when type is 'string'", () => { + const type = { type: "string", description: "param description" }; + const component = "Component"; + const serializer = "Serializer"; + const signature_type = "parameter"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual("string"); + }); + + it("should return 'boolean' when type is 'boolean'", () => { + const type = { type: "boolean", description: "param description" }; + const component = "Component"; + const serializer = "Serializer"; + const signature_type = "parameter"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual("boolean"); + }); + + it("should return 'number' when type is 'number'", () => { + const type = { type: "number", description: "param description" }; + const component = "Component"; + const serializer = "Serializer"; + const signature_type = "parameter"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual("number"); + }); + + it("should return 'any' when serializer is 'JSONSerializable'", () => { + const type = { type: "any", description: "param description" }; + const component = "Component"; + const serializer = "JSONSerializable"; + const signature_type = "parameter"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual("any"); + }); + + it("should return 'any' when serializer is 'StringSerializable'", () => { + const type = { type: "any", description: "param description" }; + const component = "Component"; + const serializer = "StringSerializable"; + const signature_type = "parameter"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual("any"); + }); + + it("should return 'string[]' when serializer is 'ListStringSerializable'", () => { + const type = { type: "any", description: "param description" }; + const component = "Component"; + const serializer = "ListStringSerializable"; + const signature_type = "parameter"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual("string[]"); + }); + + it("should return 'Blob | File | Buffer' when component is 'Image' and signature_type is 'parameter'", () => { + const type = { type: "any", description: "param description" }; + const component = "Image"; + const serializer = "Serializer"; + const signature_type = "parameter"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual("Blob | File | Buffer"); + }); + + it("should return 'string' when component is 'Image' and signature_type is 'return'", () => { + const type = { type: "string", description: "param description" }; + const component = "Image"; + const serializer = "Serializer"; + const signature_type = "return"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual("string"); + }); + + it("should return '(Blob | File | Buffer)[]' when serializer is 'FileSerializable' and type is an array and signature_type is 'parameter'", () => { + const type = { type: "array", description: "param description" }; + const component = "Component"; + const serializer = "FileSerializable"; + const signature_type = "parameter"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual("(Blob | File | Buffer)[]"); + }); + + it("should return 'Blob | File | Buffer' when serializer is 'FileSerializable' and type is not an array and signature_type is 'return'", () => { + const type = { type: "any", description: "param description" }; + const component = "Component"; + const serializer = "FileSerializable"; + const signature_type = "return"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual( + "{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}" + ); + }); + + it("should return a FileData object when serializer is 'FileSerializable' and type is not an array and signature_type is 'return'", () => { + const type = { type: "any", description: "param description" }; + const component = "Component"; + const serializer = "FileSerializable"; + const signature_type = "return"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual( + "{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}" + ); + }); + + it("should return '[(Blob | File | Buffer), (string | null)][]' when serializer is 'GallerySerializable' and signature_type is 'parameter'", () => { + const type = { type: "any", description: "param description" }; + const component = "Component"; + const serializer = "GallerySerializable"; + const signature_type = "parameter"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual("[(Blob | File | Buffer), (string | null)][]"); + }); + + it("should return a FileData object when serializer is 'GallerySerializable' and signature_type is 'return'", () => { + const type = { type: "any", description: "param description" }; + const component = "Component"; + const serializer = "GallerySerializable"; + const signature_type = "return"; + const result = get_type(type, component, serializer, signature_type); + expect(result).toEqual( + "[{ name: string; data: string; size?: number; is_file?: boolean; orig_name?: string}, (string | null))][]" + ); + }); +}); + +describe("process_endpoint", () => { + it("should return space_id, host, ws_protocol, and http_protocol when app_reference is a valid space name", async () => { + const app_reference = "hmb/hello_world"; + const host = "hmb-hello-world.hf.space"; + + const token = "hf_token"; + const expected = { + space_id: app_reference, + host, + ws_protocol: "wss", + http_protocol: "https:" + }; + + const result = await process_endpoint(app_reference, token); + expect(result).toEqual(expected); + }); + + it("should throw an error when fetching space metadata fails", async () => { + const app_reference = "hmb/bye_world"; + const token = "hf_token"; + + try { + await process_endpoint(app_reference, token); + } catch (error) { + if (error instanceof Error) { + expect(error.message).toEqual(SPACE_METADATA_ERROR_MSG); + } else { + expect.fail("Error should not be unknown."); + } + } + }); + + it("should return the correct data when app_reference is a valid space domain", async () => { + const app_reference = "hmb/hello_world"; + const host = "hmb-hello-world.hf.space"; + + const expected = { + space_id: app_reference, + host, + ws_protocol: "wss", + http_protocol: "https:" + }; + + const result = await process_endpoint("hmb/hello_world"); + expect(result).toEqual(expected); + }); + + it("processes local server URLs correctly", async () => { + const local_url = "http://localhost:7860/gradio"; + const response_local_url = await process_endpoint(local_url); + expect(response_local_url.space_id).toBe(false); + expect(response_local_url.host).toBe("localhost:7860/gradio"); + + const local_url_2 = "http://localhost:7860/gradio/"; + const response_local_url_2 = await process_endpoint(local_url_2); + expect(response_local_url_2.space_id).toBe(false); + expect(response_local_url_2.host).toBe("localhost:7860/gradio"); + }); + + it("handles hugging face space references", async () => { + const space_id = "hmb/hello_world"; + + const response = await process_endpoint(space_id); + expect(response.space_id).toBe(space_id); + expect(response.host).toContain("hf.space"); + }); + + it("handles hugging face domain URLs", async () => { + const app_reference = "https://hmb-hello-world.hf.space/"; + const response = await process_endpoint(app_reference); + expect(response.space_id).toBe("hmb-hello-world"); + expect(response.host).toBe("hmb-hello-world.hf.space"); + }); + + it("handles huggingface subpath urls", async () => { + const app_reference = + "https://pngwn-pr-demos-test.hf.space/demo/audio_debugger/"; + const response = await process_endpoint(app_reference); + expect(response.space_id).toBe("pngwn-pr-demos-test"); + expect(response.host).toBe( + "pngwn-pr-demos-test.hf.space/demo/audio_debugger" + ); + + expect(response.http_protocol).toBe("https:"); + }); +}); + +describe("join_urls", () => { + it("joins URLs correctly", () => { + expect(join_urls("http://localhost:7860", "/gradio")).toBe( + "http://localhost:7860/gradio" + ); + expect(join_urls("http://localhost:7860/", "/gradio")).toBe( + "http://localhost:7860/gradio" + ); + expect(join_urls("http://localhost:7860", "app/", "/gradio")).toBe( + "http://localhost:7860/app/gradio" + ); + expect(join_urls("http://localhost:7860/", "/app/", "/gradio/")).toBe( + "http://localhost:7860/app/gradio/" + ); + + expect(join_urls("http://127.0.0.1:8000/app", "/config")).toBe( + "http://127.0.0.1:8000/app/config" + ); + + expect(join_urls("http://127.0.0.1:8000/app/gradio", "/config")).toBe( + "http://127.0.0.1:8000/app/gradio/config" + ); + }); + it("throws an error when the URLs are not valid", () => { + expect(() => join_urls("localhost:7860", "/gradio")).toThrowError( + INVALID_URL_MSG + ); + + expect(() => join_urls("localhost:7860", "/gradio", "app")).toThrowError( + INVALID_URL_MSG + ); + }); +}); + +describe("map_data_params", () => { + let test_data = transformed_api_info; + + test_data.named_endpoints["/predict"].parameters = [ + { + parameter_name: "param1", + parameter_has_default: false, + label: "", + component: "", + serializer: "", + python_type: { + type: "", + description: "" + }, + type: { + type: "", + description: "" + } + }, + { + parameter_name: "param2", + parameter_has_default: false, + label: "", + type: { + type: "", + description: "" + }, + component: "", + serializer: "", + python_type: { + type: "", + description: "" + } + }, + { + parameter_name: "param3", + parameter_has_default: true, + parameter_default: 3, + label: "", + type: { + type: "", + description: "" + }, + component: "", + serializer: "", + python_type: { + type: "", + description: "" + } + } + ]; + + let endpoint_info = test_data.named_endpoints["/predict"]; + + it("should return an array of data when data is an array", () => { + const data = [1, 2]; + + const result = map_data_to_params(data, endpoint_info); + expect(result).toEqual(data); + }); + + it("should return an empty array when data is an empty array", () => { + const data: unknown[] = []; + + const result = map_data_to_params(data, endpoint_info); + expect(result).toEqual(data); + }); + + it("should return an empty array when data is not defined", () => { + const data = undefined; + + const result = map_data_to_params(data, endpoint_info); + expect(result).toEqual([]); + }); + + it("should return the data when too many arguments are provided for the endpoint", () => { + const data = [1, 2, 3, 4]; + + const result = map_data_to_params(data, endpoint_info); + expect(result).toEqual(data); + }); + + it("should return an array of resolved data when data is an object", () => { + const data = { + param1: 1, + param2: 2, + param3: 3 + }; + + const result = map_data_to_params(data, endpoint_info); + expect(result).toEqual([1, 2, 3]); + }); + + it("should use the default value when a keyword argument is not provided and has a default value", () => { + const data = { + param1: 1, + param2: 2 + }; + + const result = map_data_to_params(data, endpoint_info); + expect(result).toEqual([1, 2, 3]); + }); + + it("should throw an error when an invalid keyword argument is provided", () => { + const data = { + param1: 1, + param2: 2, + param3: 3, + param4: 4 + }; + + expect(() => map_data_to_params(data, endpoint_info)).toThrowError( + "Parameter `param4` is not a valid keyword argument. Please refer to the API for usage." + ); + }); + + it("should throw an error when no value is provided for a required parameter", () => { + const data = {}; + + expect(() => map_data_to_params(data, endpoint_info)).toThrowError( + "No value provided for required parameter: param1" + ); + }); +}); diff --git a/client/js/src/test/apply_diff.test.ts b/client/js/src/test/apply_diff.test.ts new file mode 100644 index 0000000..7d0d65f --- /dev/null +++ b/client/js/src/test/apply_diff.test.ts @@ -0,0 +1,29 @@ +import { apply_diff } from "../utils/stream"; +import { it, expect, describe } from "vitest"; + +describe("apply_diff", () => { + it("delete_operation_works", () => { + const data = [ + { content: "Hi", role: "user" }, + { content: "How can I assist you?", role: "assistant" } + ]; + + const diff: any = [ + ["delete", [0], null], + ["delete", [0], null] + ]; + const result = apply_diff(data, diff); + expect(result).toEqual([]); + }); + + it("delete_operation_works with multiple deletes", () => { + const data = ["a", "b", "c"]; + const diff: any = [ + ["replace", [0], "d"], + ["delete", [1], null], + ["delete", [1], null] + ]; + const result = apply_diff(data, diff); + expect(result).toEqual(["d"]); + }); +}); diff --git a/client/js/src/test/data.test.ts b/client/js/src/test/data.test.ts new file mode 100644 index 0000000..f1dd739 --- /dev/null +++ b/client/js/src/test/data.test.ts @@ -0,0 +1,525 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { + update_object, + walk_and_store_blobs, + skip_queue, + post_message, + handle_file, + handle_payload +} from "../helpers/data"; +import { config_response, endpoint_info } from "./test_data"; +import { BlobRef, Command } from "../types"; +import { FileData } from "../upload"; + +const IS_NODE = + typeof process !== "undefined" && process.env.TEST_MODE === "node"; + +class FakeBuffer { + static from(data: string) { + return new Blob([data]); + } + + static isBuffer() { + return false; + } + + static isEncoding() { + return false; + } + + static byteLength() { + return 0; + } + + static concat() { + return new Blob([]); + } + + static compare() { + return 0; + } + + static alloc() { + return new Blob([]); + } + + static allocUnsafe() { + return new Blob([]); + } + + static allocUnsafeSlow() { + return new Blob([]); + } + + static poolSize = 0; + + static of() { + return new Blob([]); + } + + equals(other: Blob) { + return this.toString() === other.toString(); + } +} + +if (!IS_NODE) { + globalThis.Buffer = FakeBuffer as unknown as BufferConstructor; +} + +describe("walk_and_store_blobs", () => { + it.skipIf(!IS_NODE)("should convert a Buffer to a Blob", async () => { + const buffer = globalThis.Buffer.from("test data"); + const parts = await walk_and_store_blobs(buffer, "text"); + + expect(parts).toHaveLength(1); + expect(parts[0].blob).toBeInstanceOf(Blob); + }); + + it("should return a Blob when passed a Blob", async () => { + const blob = new Blob(["test data"]); + const parts = await walk_and_store_blobs( + blob, + undefined, + [], + true, + endpoint_info + ); + + expect(parts[0].blob).toBeInstanceOf(Blob); + }); + + it("should handle arrays", async () => { + const image = new Blob([]); + const parts = await walk_and_store_blobs([image]); + + expect(parts).toHaveLength(1); + expect(parts[0].blob).toBeInstanceOf(Blob); + expect(parts[0].path).toEqual(["0"]); + }); + + it("should handle deep structures", async () => { + const image = new Blob([]); + const parts = await walk_and_store_blobs({ a: { b: { data: { image } } } }); + + expect(parts).toHaveLength(1); + expect(parts[0].blob).toBeInstanceOf(Blob); + expect(parts[0].path).toEqual(["a", "b", "data", "image"]); + }); + + it("should handle deep structures with arrays", async () => { + const image = new Blob([]); + const parts = await walk_and_store_blobs({ + a: [ + { + b: [ + { + data: [ + { + image + } + ] + } + ] + } + ] + }); + + expect(parts[0].blob).toBeInstanceOf(Blob); + }); + + it.skipIf(!IS_NODE)( + "should handle deep structures with arrays (with equality check)", + async () => { + const image = new Blob([]); + + const obj = { + a: [ + { + b: [ + { + data: [[image], image, [image, [image]]] + } + ] + } + ] + }; + const parts = await walk_and_store_blobs(obj); + + async function map_path(obj: Record, parts: BlobRef[]) { + const { path, blob } = parts[parts.length - 1]; + let ref = obj; + path.forEach((p) => (ref = ref[p])); + + // since ref is a Blob and blob is a Blob, we deep equal check the two buffers instead + if (ref instanceof Blob && blob instanceof Blob) { + const refBuffer = Buffer.from(await ref.arrayBuffer()); + const blobBuffer = Buffer.from(await blob.arrayBuffer()); + return refBuffer.equals(blobBuffer); + } + + return ref === blob; + } + + expect(parts[0].blob).toBeInstanceOf(Blob); + expect(map_path(obj, parts)).toBeTruthy(); + } + ); + + it.skipIf(!IS_NODE)( + "should handle buffer instances and return a BlobRef", + async () => { + const buffer = Buffer.from("test"); + const parts = await walk_and_store_blobs(buffer, undefined, ["blob"]); + + expect(parts).toHaveLength(1); + expect(parts[0].blob).toBeInstanceOf(Blob); + expect(parts[0].path).toEqual(["blob"]); + } + ); + + it.skipIf(!IS_NODE)( + "should handle buffer instances with a path and return a BlobRef with the path", + async () => { + const buffer = Buffer.from("test data"); + const parts = await walk_and_store_blobs(buffer); + + expect(parts).toHaveLength(1); + expect(parts[0].path).toEqual([]); + expect(parts[0].blob).toBeInstanceOf(Blob); + } + ); + + it.skipIf(!IS_NODE)( + "should convert an object with deep structures to BlobRefs", + async () => { + const param = { + a: { + b: { + data: { + image: Buffer.from("test image") + } + } + } + }; + const parts = await walk_and_store_blobs(param); + + expect(parts).toHaveLength(1); + expect(parts[0].path).toEqual(["a", "b", "data", "image"]); + expect(parts[0].blob).toBeInstanceOf(Blob); + } + ); +}); +describe("update_object", () => { + it("should update the value of a nested property", () => { + const obj = { + a: { + b: { + c: "old value" + } + } + }; + + const stack = ["a", "b", "c"]; + const new_val = "new value"; + + update_object(obj, new_val, stack); + + expect(obj.a.b.c).toBe(new_val); + }); + + it("should throw an error for invalid key type", () => { + const obj = { + a: { + b: { + c: "value" + } + } + }; + + const stack = ["a", "b", true]; + const newValue = "new value"; + + expect(() => { + // @ts-ignore + update_object(obj, newValue, stack); + }).toThrowError("Invalid key type"); + }); +}); + +describe("skip_queue", () => { + const id = 0; + const config = config_response; + + it("should not skip queue when global and dependency queue is enabled", () => { + config.enable_queue = true; + config.dependencies.find((dep) => dep.id === id)!.queue = true; + + const result = skip_queue(id, config_response); + + expect(result).toBe(false); + }); + + it("should not skip queue when global queue is disabled and dependency queue is enabled", () => { + config.enable_queue = false; + config.dependencies.find((dep) => dep.id === id)!.queue = true; + + const result = skip_queue(id, config_response); + + expect(result).toBe(false); + }); + + it("should should skip queue when global queue and dependency queue is disabled", () => { + config.enable_queue = false; + config.dependencies.find((dep) => dep.id === id)!.queue = false; + + const result = skip_queue(id, config_response); + + expect(result).toBe(true); + }); + + it("should should skip queue when global queue is enabled and dependency queue is disabled", () => { + config.enable_queue = true; + config.dependencies.find((dep) => dep.id === id)!.queue = false; + + const result = skip_queue(id, config_response); + + expect(result).toBe(true); + }); +}); + +describe("post_message", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it.skipIf(IS_NODE)( + "should send a message to the parent window and resolve with received data", + async () => { + const test_data = { key: "value" }; + const test_origin = "https://huggingface.co"; + + // Create a mock for window.parent.postMessage that we'll spy on + const post_message_spy = vi + .spyOn(window.parent, "postMessage") + .mockImplementation(() => {}); + + // Mock MessageChannel + const original_message_channel = globalThis.MessageChannel; + const mock_port1 = { + onmessage: null as unknown as (event: { data: any }) => void, + close: vi.fn() + }; + const mock_port2 = {}; + + class MockMessageChannel { + port1 = mock_port1; + port2 = mock_port2; + } + + // Replace MessageChannel with our mock version + globalThis.MessageChannel = MockMessageChannel as any; + + const promise = post_message(test_data, test_origin); + + // Simulate receiving a message back + if (mock_port1.onmessage) { + mock_port1.onmessage({ data: test_data } as any); + } + + await expect(promise).resolves.toEqual(test_data); + expect(post_message_spy).toHaveBeenCalledWith(test_data, test_origin, [ + mock_port2 + ]); + + // Restore original MessageChannel + globalThis.MessageChannel = original_message_channel; + post_message_spy.mockRestore(); + } + ); +}); + +describe("handle_file", () => { + it("should handle a Blob object and return the blob", () => { + const blob = new Blob(["test data"], { type: "image/png" }); + const result = handle_file(blob) as FileData; + + expect(result).toBe(blob); + }); + + it.skipIf(!IS_NODE)( + "should handle a Buffer object and return it as a blob", + () => { + const buffer = Buffer.from("test data"); + const result = handle_file(buffer) as FileData; + expect(result).toBeInstanceOf(Blob); + } + ); + + it.skipIf(!IS_NODE)( + "should handle a local file path and return a Command object", + () => { + const file_path = "./owl.png"; + const result = handle_file(file_path) as Command; + expect(result).toBeInstanceOf(Command); + expect(result).toEqual({ + type: "command", + command: "upload_file", + meta: { path: "./owl.png", name: "./owl.png", orig_path: "./owl.png" }, + fileData: undefined + }); + } + ); + + it.skipIf(IS_NODE)( + "should handle a File object and return it as FileData", + () => { + const file = new File(["test image"], "test.png", { type: "image/png" }); + const result = handle_file(file) as FileData; + expect(result).toBeInstanceOf(Blob); + } + ); + + it("should throw an error for invalid input", () => { + const invalid_input = 123; + + expect(() => { + // @ts-ignore + handle_file(invalid_input); + }).toThrowError( + "Invalid input: must be a URL, File, Blob, or Buffer object." + ); + }); +}); + +describe("handle_payload", () => { + it("should return an input payload with null in place of `state` when with_null_state is true", () => { + const resolved_payload = [2]; + const dependency = { + inputs: [1, 2] + }; + const components = [ + { id: 1, type: "number" }, + { id: 2, type: "state" } + ]; + const with_null_state = true; + const result = handle_payload( + resolved_payload, + // @ts-ignore + dependency, + components, + "input", + with_null_state + ); + expect(result).toEqual([2, null]); + }); + it("should return an input payload with null in place of two `state` components when with_null_state is true", () => { + const resolved_payload = ["hello", "goodbye"]; + const dependency = { + inputs: [1, 2, 3, 4] + }; + const components = [ + { id: 1, type: "textbox" }, + { id: 2, type: "state" }, + { id: 3, type: "textbox" }, + { id: 4, type: "state" } + ]; + const with_null_state = true; + const result = handle_payload( + resolved_payload, + // @ts-ignore + dependency, + components, + "input", + with_null_state + ); + expect(result).toEqual(["hello", null, "goodbye", null]); + }); + + it("should return an output payload without the state component value when with_null_state is false", () => { + const resolved_payload = ["hello", null]; + const dependency = { + outputs: [2, 3] + }; + const components = [ + { id: 2, type: "textbox" }, + { id: 3, type: "state" } + ]; + const with_null_state = false; + const result = handle_payload( + resolved_payload, + // @ts-ignore + dependency, + components, + "output", + with_null_state + ); + expect(result).toEqual(["hello"]); + }); + + it("should return an ouput payload without the two state component values when with_null_state is false", () => { + const resolved_payload = ["hello", null, "world", null]; + const dependency = { + outputs: [2, 3, 4, 5] + }; + const components = [ + { id: 2, type: "textbox" }, + { id: 3, type: "state" }, + { id: 4, type: "textbox" }, + { id: 5, type: "state" } + ]; + const with_null_state = false; + const result = handle_payload( + resolved_payload, + // @ts-ignore + dependency, + components, + "output", + with_null_state + ); + expect(result).toEqual(["hello", "world"]); + }); + + it("should return an ouput payload with the two state component values when with_null_state is true", () => { + const resolved_payload = ["hello", null, "world", null]; + const dependency = { + outputs: [2, 3, 4, 5] + }; + const components = [ + { id: 2, type: "textbox" }, + { id: 3, type: "state" }, + { id: 4, type: "textbox" }, + { id: 5, type: "state" } + ]; + const with_null_state = true; + const result = handle_payload( + resolved_payload, + // @ts-ignore + dependency, + components, + "output", + with_null_state + ); + expect(result).toEqual(["hello", null, "world", null]); + }); + + it("should return the same payload where no state components are defined", () => { + const resolved_payload = ["hello", "world"]; + const dependency = { + inputs: [2, 3] + }; + const components = [ + { id: 2, type: "textbox" }, + { id: 3, type: "textbox" } + ]; + const with_null_state = true; + const result = handle_payload( + resolved_payload, + // @ts-ignore + dependency, + components, + "input", + with_null_state + ); + expect(result).toEqual(["hello", "world"]); + }); +}); diff --git a/client/js/src/test/filesize.test.ts b/client/js/src/test/filesize.test.ts new file mode 100644 index 0000000..8dc47fb --- /dev/null +++ b/client/js/src/test/filesize.test.ts @@ -0,0 +1,143 @@ +import { describe, it, expect } from "vitest"; +import { filesize, SPECS } from "../utils/filesize"; + +describe("filesize", () => { + describe("basic functionality", () => { + it("should format bytes correctly with default spec (jedec)", () => { + expect(filesize(0)).toBe("0.0 b"); + expect(filesize(512)).toBe("512.0 b"); + expect(filesize(1024)).toBe("1.0 Kb"); + expect(filesize(1536)).toBe("1.5 Kb"); // 1024 + 512 + expect(filesize(1048576)).toBe("1.0 Mb"); // 1024^2 + expect(filesize(1073741824)).toBe("1.0 Gb"); // 1024^3 + }); + + it("should handle different fixed decimal places", () => { + expect(filesize(1536, 0)).toBe("2 Kb"); + expect(filesize(1536, 1)).toBe("1.5 Kb"); + expect(filesize(1536, 2)).toBe("1.50 Kb"); + expect(filesize(1536, 3)).toBe("1.500 Kb"); + }); + + it("should handle negative numbers by taking absolute value", () => { + expect(filesize(-1024)).toBe("1.0 Kb"); + expect(filesize(-1536)).toBe("1.5 Kb"); + expect(filesize(-0)).toBe("0.0 b"); + }); + }); + + describe("SI specification", () => { + it("should use base 1000 for SI spec", () => { + expect(filesize(1000, 1, "si")).toBe("1.0 kb"); + expect(filesize(1500, 1, "si")).toBe("1.5 kb"); + expect(filesize(1000000, 1, "si")).toBe("1.0 Mb"); + expect(filesize(1000000000, 1, "si")).toBe("1.0 Gb"); + }); + + it("should handle large numbers with SI spec", () => { + expect(filesize(1e12, 1, "si")).toBe("1.0 Tb"); + expect(filesize(1e15, 1, "si")).toBe("1.0 Pb"); + expect(filesize(1e18, 1, "si")).toBe("1.0 Eb"); + }); + }); + + describe("IEC specification", () => { + it("should use base 1024 with binary units for IEC spec", () => { + expect(filesize(1024, 1, "iec")).toBe("1.0 Kib"); + expect(filesize(1536, 1, "iec")).toBe("1.5 Kib"); + expect(filesize(1048576, 1, "iec")).toBe("1.0 Mib"); + expect(filesize(1073741824, 1, "iec")).toBe("1.0 Gib"); + }); + + it("should handle large numbers with IEC spec", () => { + expect(filesize(Math.pow(1024, 4), 1, "iec")).toBe("1.0 Tib"); + expect(filesize(Math.pow(1024, 5), 1, "iec")).toBe("1.0 Pib"); + }); + }); + + describe("JEDEC specification", () => { + it("should use base 1024 with decimal-style units for JEDEC spec", () => { + expect(filesize(1024, 1, "jedec")).toBe("1.0 Kb"); + expect(filesize(1536, 1, "jedec")).toBe("1.5 Kb"); + expect(filesize(1048576, 1, "jedec")).toBe("1.0 Mb"); + expect(filesize(1073741824, 1, "jedec")).toBe("1.0 Gb"); + }); + }); + + describe("edge cases", () => { + it("should handle very large numbers", () => { + const veryLarge = Math.pow(1024, 8); // Yottabytes + expect(filesize(veryLarge, 1, "jedec")).toBe("1.0 Yb"); + }); + + it("should handle decimal input", () => { + expect(filesize(1024.5, 1)).toBe("1.0 Kb"); + expect(filesize(1500.7, 2)).toBe("1.47 Kb"); + }); + + it("should handle invalid spec by falling back to jedec", () => { + expect(filesize(1024, 1, "invalid")).toBe("1.0 Kb"); + expect(filesize(1024, 1, "")).toBe("1.0 Kb"); + expect(filesize(1024, 1, undefined)).toBe("1.0 Kb"); + }); + }); + + describe("precision and rounding", () => { + it("should round correctly with different precision levels", () => { + const testBytes = 1536; // 1.5 KB + expect(filesize(testBytes, 0)).toBe("2 Kb"); + expect(filesize(testBytes, 1)).toBe("1.5 Kb"); + expect(filesize(testBytes, 2)).toBe("1.50 Kb"); + }); + + it("should handle rounding edge cases", () => { + const almostTwoKb = 2047; // almost 2KB + expect(filesize(almostTwoKb, 0)).toBe("2 Kb"); + expect(filesize(almostTwoKb, 3)).toBe("1.999 Kb"); + }); + }); + + describe("SPECS constant", () => { + it("should have correct radix values", () => { + expect(SPECS.si.radix).toBe(1000); + expect(SPECS.iec.radix).toBe(1024); + expect(SPECS.jedec.radix).toBe(1024); + }); + + it("should have correct unit arrays", () => { + expect(SPECS.si.unit).toEqual([ + "b", + "kb", + "Mb", + "Gb", + "Tb", + "Pb", + "Eb", + "Zb", + "Yb" + ]); + expect(SPECS.iec.unit).toEqual([ + "b", + "Kib", + "Mib", + "Gib", + "Tib", + "Pib", + "Eib", + "Zib", + "Yib" + ]); + expect(SPECS.jedec.unit).toEqual([ + "b", + "Kb", + "Mb", + "Gb", + "Tb", + "Pb", + "Eb", + "Zb", + "Yb" + ]); + }); + }); +}); diff --git a/client/js/src/test/handlers.ts b/client/js/src/test/handlers.ts new file mode 100644 index 0000000..1956303 --- /dev/null +++ b/client/js/src/test/handlers.ts @@ -0,0 +1,687 @@ +import { HttpResponse, http, RequestHandler } from "msw"; +import { + HOST_URL, + API_INFO_URL, + CONFIG_URL, + RUNTIME_URL, + SLEEPTIME_URL, + UPLOAD_URL, + BROKEN_CONNECTION_MSG, + LOGIN_URL +} from "../constants"; +import { + response_api_info, + config_response, + whoami_response, + duplicate_response, + hardware_sleeptime_response, + discussions_response, + runtime_response +} from "./test_data"; + +const root_url = "https://huggingface.co"; + +export const direct_space_url = "https://hmb-hello-world.hf.space"; +const private_space_url = "https://hmb-secret-world.hf.space"; +const private_auth_space_url = "https://hmb-private-auth-space.hf.space"; + +const server_error_space_url = "https://hmb-server-error.hf.space"; +const upload_server_test_space_url = "https://hmb-server-test.hf.space"; +const auth_app_space_url = "https://hmb-auth-space.hf.space"; +const unauth_app_space_url = "https://hmb-unauth-space.hf.space"; +const invalid_auth_space_url = "https://hmb-invalid-auth-space.hf.space"; + +const server_error_reference = "hmb/server_error"; +const app_reference = "hmb/hello_world"; +const broken_app_reference = "hmb/bye_world"; +const duplicate_app_reference = "gradio/hello_world"; +const private_app_reference = "hmb/secret_world"; +const server_test_app_reference = "hmb/server_test"; +const auth_app_reference = "hmb/auth_space"; +const unauth_app_reference = "hmb/unauth_space"; +const invalid_auth_app_reference = "hmb/invalid_auth_space"; +const private_auth_app_reference = "hmb/private_auth_space"; + +export const handlers: RequestHandler[] = [ + // /host requests + http.get(`${root_url}/api/spaces/${app_reference}/${HOST_URL}`, () => { + return new HttpResponse( + JSON.stringify({ + subdomain: "hmb-hello-world", + host: "https://hmb-hello-world.hf.space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + http.get(`${root_url}/api/spaces/${broken_app_reference}/${HOST_URL}`, () => { + return new HttpResponse(null, { + status: 404, + headers: { + "Content-Type": "application/json", + hf_token: "hf_123" + } + }); + }), + http.get( + `${root_url}/api/spaces/${private_auth_app_reference}/${HOST_URL}`, + () => { + return new HttpResponse( + JSON.stringify({ + subdomain: "hmb-private-auth-space", + host: "https://hmb-private-auth-space.hf.space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + } + ), + http.get( + `${root_url}/api/spaces/${private_app_reference}/${HOST_URL}`, + ({ request }) => { + const token = request.headers.get("authorization")?.substring(7); + + if (!token || token !== "hf_123") { + return new HttpResponse(null, { + status: 401, + headers: { + "Content-Type": "application/json" + } + }); + } + + return new HttpResponse( + JSON.stringify({ + subdomain: private_app_reference, + host: private_space_url + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + } + ), + http.get( + `${root_url}/api/spaces/${server_error_reference}/${HOST_URL}`, + () => { + return new HttpResponse( + JSON.stringify({ + subdomain: "hmb-server-test", + host: "https://hmb-server-test.hf.space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + } + ), + http.get( + `${root_url}/api/spaces/${server_test_app_reference}/${HOST_URL}`, + () => { + return new HttpResponse( + JSON.stringify({ + subdomain: "hmb-server-test", + host: "https://hmb-server-test.hf.space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + } + ), + http.get(`${root_url}/api/spaces/${auth_app_reference}/${HOST_URL}`, () => { + return new HttpResponse( + JSON.stringify({ + subdomain: "hmb-auth-space", + host: "https://hmb-auth-space.hf.space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + http.get( + `${root_url}/api/spaces/${invalid_auth_app_reference}/${HOST_URL}`, + () => { + return new HttpResponse( + JSON.stringify({ + subdomain: "hmb-invalid-auth-space", + host: "https://hmb-invalid-auth-space.hf.space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + } + ), + http.get( + `${root_url}/api/spaces/${duplicate_app_reference}/${HOST_URL}`, + () => { + return new HttpResponse( + JSON.stringify({ + subdomain: "gradio-hello-world", + host: "https://gradio-hello-world.hf.space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + } + ), + http.get(`${root_url}/api/spaces/${unauth_app_reference}/${HOST_URL}`, () => { + return new HttpResponse( + JSON.stringify({ + subdomain: "hmb-unath-space", + host: "https://hmb-unauth-space.hf.space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + // /info requests + http.get(`${direct_space_url}/${API_INFO_URL}`, () => { + return new HttpResponse(JSON.stringify(response_api_info), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.get(`${upload_server_test_space_url}/${API_INFO_URL}`, () => { + return new HttpResponse(JSON.stringify(response_api_info), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.get(`${private_space_url}/${API_INFO_URL}`, () => { + return new HttpResponse(JSON.stringify(response_api_info), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.get(`${server_error_space_url}/${API_INFO_URL}`, () => { + return new HttpResponse(JSON.stringify(response_api_info), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.get(`${auth_app_space_url}/${API_INFO_URL}`, async () => { + return new HttpResponse(JSON.stringify(response_api_info), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.get(`${private_auth_space_url}/${API_INFO_URL}`, async () => { + return new HttpResponse(JSON.stringify(response_api_info), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + // /config requests + http.get(`${direct_space_url}/${CONFIG_URL}`, () => { + return new HttpResponse(JSON.stringify(config_response), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.get(`${private_space_url}/${CONFIG_URL}`, () => { + return new HttpResponse( + JSON.stringify({ + ...config_response, + root: "https://hmb-secret-world.hf.space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + http.get(`${upload_server_test_space_url}/${CONFIG_URL}`, () => { + return new HttpResponse( + JSON.stringify({ + ...config_response, + root: "https://hmb-server-test.hf.space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + http.get(`${private_auth_space_url}/${CONFIG_URL}`, () => { + return new HttpResponse( + JSON.stringify({ + ...config_response, + root: "https://hmb-private-auth-space.hf.space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + http.get(`${direct_space_url}/${CONFIG_URL}`, () => { + return new HttpResponse(JSON.stringify(config_response), { + status: 500, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.get(`${server_error_space_url}/${CONFIG_URL}`, () => { + return new HttpResponse(JSON.stringify(config_response), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.get(`${invalid_auth_space_url}/${CONFIG_URL}`, () => { + return new HttpResponse(JSON.stringify({ detail: "Unauthorized" }), { + status: 401, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.get(`${auth_app_space_url}/${CONFIG_URL}`, ({ request }) => { + return new HttpResponse( + JSON.stringify({ + ...config_response, + root: "https://hmb-auth-space.hf.space", + space_id: "hmb/auth_space" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + http.get(`${unauth_app_space_url}/${CONFIG_URL}`, () => { + return new HttpResponse( + JSON.stringify({ + detail: "Unauthorized" + }), + { + status: 401, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + // /whoami requests + http.get(`${root_url}/api/whoami-v2`, () => { + return new HttpResponse(JSON.stringify(whoami_response), { + status: 200, + headers: { + "Content-Type": "application/json", + "hf-token": "hf_123" + } + }); + }), + // /duplicate requests + http.post( + `${root_url}/api/spaces/${duplicate_app_reference}/duplicate`, + ({ request }) => { + if (request.headers.get("authorization")?.substring(7) !== "hf_123") { + throw new HttpResponse(null, { + status: 401, + headers: { + "Content-Type": "application/json" + } + }); + } + return new HttpResponse(JSON.stringify(duplicate_response), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + } + ), + // /sleeptime requests + http.post(`${root_url}/api/spaces/${app_reference}/${SLEEPTIME_URL}`, () => { + return new HttpResponse(JSON.stringify(hardware_sleeptime_response), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.post( + `${root_url}/api/spaces/${server_test_app_reference}/${SLEEPTIME_URL}`, + () => { + throw new HttpResponse(null, { + status: 500, + headers: { + "Content-Type": "application/json" + } + }); + } + ), + // /runtime requests + http.get( + `${root_url}/api/spaces/${broken_app_reference}/${RUNTIME_URL}`, + () => { + return new HttpResponse(null, { + status: 404, + headers: { + "Content-Type": "application/json" + } + }); + } + ), + http.get(`${root_url}/api/spaces/${app_reference}/${RUNTIME_URL}`, () => { + return new HttpResponse(JSON.stringify(hardware_sleeptime_response), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + // queue requests + http.get(`${direct_space_url}/queue/data`, () => { + return new HttpResponse(JSON.stringify({ event_id: "123" }), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.post(`${direct_space_url}/queue/join`, () => { + return new HttpResponse(JSON.stringify({ event_id: "123" }), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + // upload requests + http.post(`${direct_space_url}/${UPLOAD_URL}`, () => { + return new HttpResponse(JSON.stringify(["lion.jpg"]), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.post(`${upload_server_test_space_url}/${UPLOAD_URL}`, () => { + throw new HttpResponse(JSON.parse("Internal Server Error"), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + // discussions requests + http.head(`${root_url}/api/spaces/${app_reference}/discussions`, () => { + return new HttpResponse(JSON.stringify(discussions_response), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.head( + `${root_url}/api/spaces/${broken_app_reference}/discussions`, + () => { + throw new HttpResponse( + JSON.parse("Discussions are disabled for this repo"), + { + status: 403, + headers: { + "Content-Type": "application/json" + } + } + ); + } + ), + // space requests + http.get(`${root_url}/api/spaces/${app_reference}`, () => { + return new HttpResponse( + JSON.stringify({ id: app_reference, runtime: runtime_response }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + http.get(`${root_url}/api/spaces/hmb/paused_space`, () => { + return new HttpResponse( + JSON.stringify({ + id: app_reference, + runtime: { ...runtime_response, stage: "PAUSED" } + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + http.get(`${root_url}/api/spaces/hmb/building_space`, () => { + return new HttpResponse( + JSON.stringify({ + id: app_reference, + runtime: { ...runtime_response, stage: "BUILDING" } + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + http.get(`${root_url}/api/spaces/hmb/stopped_space`, () => { + return new HttpResponse( + JSON.stringify({ + id: app_reference, + runtime: { ...runtime_response, stage: "STOPPED" } + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + http.get(`${root_url}/api/spaces/hmb/failed_space`, () => { + throw new HttpResponse(null, { + status: 500, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.get(`${root_url}/api/spaces/${unauth_app_reference}`, () => { + return new HttpResponse( + JSON.stringify({ + id: unauth_app_reference, + runtime: { ...runtime_response } + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + // jwt requests + http.get(`${root_url}/api/spaces/${app_reference}/jwt`, () => { + return new HttpResponse( + JSON.stringify({ + token: "jwt_123" + }), + { + status: 200, + headers: { + "Content-Type": "application/json" + } + } + ); + }), + http.get(`${root_url}/api/spaces/${broken_app_reference}/jwt`, () => { + return new HttpResponse(null, { + status: 500, + headers: { + "Content-Type": "application/json" + } + }); + }), + // post_data requests + http.post(`${direct_space_url}`, () => { + return new HttpResponse(JSON.stringify({}), { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.post(`${private_space_url}`, () => { + return new HttpResponse(JSON.stringify(BROKEN_CONNECTION_MSG), { + status: 500, + headers: { + "Content-Type": "application/json" + } + }); + }), + // heartbeat requests + http.get(`*/heartbeat/*`, () => { + return new HttpResponse(null, { + status: 200, + headers: { + "Content-Type": "application/json" + } + }); + }), + // login requests + http.post(`${auth_app_space_url}/${LOGIN_URL}`, async ({ request }) => { + let username; + let password; + + await request.formData().then((data) => { + username = data.get("username"); + password = data.get("password"); + }); + + if (username === "admin" && password === "pass1234") { + return new HttpResponse( + JSON.stringify({ + success: true + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + "Set-Cookie": + "access-token-unsecure-123=abc; HttpOnly; Path=/; SameSite=none; Secure" + } + } + ); + } + + return new HttpResponse(null, { + status: 401, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.post(`${invalid_auth_space_url}/${LOGIN_URL}`, async () => { + return new HttpResponse(null, { + status: 401, + headers: { + "Content-Type": "application/json" + } + }); + }), + http.post(`${private_auth_space_url}/${LOGIN_URL}`, async ({ request }) => { + let username; + let password; + + await request.formData().then((data) => { + username = data.get("username"); + password = data.get("password"); + }); + + if (username === "admin" && password === "pass1234") { + return new HttpResponse( + JSON.stringify({ + success: true + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + "Set-Cookie": + "access-token-unsecure-123=abc; HttpOnly; Path=/; SameSite=none; Secure" + } + } + ); + } + + return new HttpResponse(null, { + status: 401, + headers: { + "Content-Type": "application/json" + } + }); + }) +]; diff --git a/client/js/src/test/init.test.ts b/client/js/src/test/init.test.ts new file mode 100644 index 0000000..8ef7d4d --- /dev/null +++ b/client/js/src/test/init.test.ts @@ -0,0 +1,165 @@ +import { + describe, + beforeAll, + afterEach, + afterAll, + test, + expect, + vi +} from "vitest"; + +import { Client, client, duplicate } from ".."; +import { + transformed_api_info, + config_response, + response_api_info +} from "./test_data"; +import { initialise_server } from "./server"; +import { SPACE_METADATA_ERROR_MSG } from "../constants"; + +const app_reference = "hmb/hello_world"; +const broken_app_reference = "hmb/bye_world"; +const direct_app_reference = "https://hmb-hello-world.hf.space"; +const secret_direct_app_reference = "https://hmb-secret-world.hf.space"; + +let server: Awaited>; + +beforeAll(async () => { + server = await initialise_server(); + await server.start({ quiet: true }); +}); +afterEach(() => server.resetHandlers()); +afterAll(() => server.stop()); + +describe("Client class", () => { + describe("initialisation", () => { + test("fetch is bound to the Client instance", async () => { + const test = await Client.connect("hmb/hello_world"); + const fetch_method = test.fetch; + const res = await fetch_method(direct_app_reference + "/info"); + + await expect(res.json()).resolves.toEqual(response_api_info); + }); + + test("stream is bound to the Client instance", async () => { + const test = await Client.connect("hmb/hello_world"); + const stream_method = test.stream; + const url = new URL(`${direct_app_reference}/queue/data`); + const stream = stream_method(url); + + expect(stream).toBeDefined(); + expect(stream.onmessage).toBeDefined(); + }); + + test("backwards compatibility of client using deprecated syntax", async () => { + const app = await client(app_reference); + expect(app.config).toEqual(config_response); + }); + test("connecting to a running app with a space reference", async () => { + const app = await Client.connect(app_reference); + expect(app.config).toEqual(config_response); + }); + + test("connecting to a running app with a direct app URL", async () => { + const app = await Client.connect(direct_app_reference); + expect(app.config).toEqual(config_response); + }); + + test("connecting successfully to a private running app with a space reference", async () => { + const app = await Client.connect("hmb/secret_world", { + token: "hf_123" + }); + + expect(app.config).toEqual({ + ...config_response, + root: "https://hmb-secret-world.hf.space" + }); + }); + + test("connecting successfully to a private running app with a direct app URL ", async () => { + const app = await Client.connect(secret_direct_app_reference, { + token: "hf_123" + }); + + expect(app.config).toEqual({ + ...config_response, + root: "https://hmb-secret-world.hf.space" + }); + }); + + test("unsuccessfully attempting to connect to a private running app", async () => { + await expect( + Client.connect("hmb/secret_world", { + token: "hf_bad_token" + }) + ).rejects.toThrowError(SPACE_METADATA_ERROR_MSG); + }); + + test("viewing the api info of a running app", async () => { + const app = await Client.connect(app_reference); + expect(await app.view_api()).toEqual(transformed_api_info); + }); + + test("viewing the api info of a non-existent app", async () => { + const app = Client.connect(broken_app_reference); + await expect(app).rejects.toThrowError(); + }); + }); + + describe("duplicate", () => { + test("backwards compatibility of duplicate using deprecated syntax", async () => { + const app = await duplicate("gradio/hello_world", { + token: "hf_123", + private: true, + hardware: "cpu-basic" + }); + + expect(app.config).toEqual(config_response); + }); + + test("creating a duplicate of a running app", async () => { + const duplicate = await Client.duplicate("gradio/hello_world", { + token: "hf_123", + private: true, + hardware: "cpu-basic" + }); + + expect(duplicate.config).toEqual(config_response); + }); + + test("creating a duplicate of a running app without a token", async () => { + const duplicate = Client.duplicate("gradio/hello_world", { + private: true, + hardware: "cpu-basic" + }); + + await expect(duplicate).rejects.toThrow("Error: Unauthorized"); + }); + + test("creating a duplicate of a broken app", async () => { + const duplicate = Client.duplicate(broken_app_reference); + + await expect(duplicate).rejects.toThrow(SPACE_METADATA_ERROR_MSG); + }); + }); + + describe("overriding the Client class", () => { + // TODO: broken test since https://github.com/gradio-app/gradio/pull/10890 + test.skip("overriding methods on the Client class", async () => { + const mocked_fetch = vi.fn( + (input: RequestInfo | URL, init?: RequestInit): Promise => { + return Promise.resolve( + new Response(JSON.stringify({ data: "test" })) + ); + } + ); + + class CustomClient extends Client { + fetch = mocked_fetch; + } + + await CustomClient.connect("hmb/hello_world"); + expect(mocked_fetch).toHaveBeenCalled(); + }); + }); +}); diff --git a/client/js/src/test/init_helpers.test.ts b/client/js/src/test/init_helpers.test.ts new file mode 100644 index 0000000..045bf01 --- /dev/null +++ b/client/js/src/test/init_helpers.test.ts @@ -0,0 +1,156 @@ +import { + resolve_root, + get_jwt, + determine_protocol, + parse_and_set_cookies, + resolve_config +} from "../helpers/init_helpers"; +import { initialise_server } from "./server"; +import { beforeAll, afterEach, afterAll, it, expect, describe } from "vitest"; +import { Client } from "../client"; +import { INVALID_CREDENTIALS_MSG, MISSING_CREDENTIALS_MSG } from "../constants"; +import { config_response } from "./test_data"; + +let server: Awaited>; + +beforeAll(async () => { + server = await initialise_server(); + await server.start({ quiet: true }); +}); +afterEach(() => server.resetHandlers()); +afterAll(() => server.stop()); + +describe("resolve_config", () => { + it("requests /config without a Content-Type header and with same-origin credentials, so the cross-origin embed fetch is not blocked by CORS", async () => { + let captured_init: RequestInit | undefined; + const fake_client = { + options: {}, + deep_link: null, + fetch: (_url: string, init: RequestInit) => { + captured_init = init; + return Promise.resolve( + new Response(JSON.stringify(config_response), { status: 200 }) + ); + } + } as unknown as Client; + + await resolve_config.call(fake_client, "https://hmb-hello-world.hf.space"); + + expect(captured_init).toBeDefined(); + const header_names = Object.keys( + captured_init?.headers as Record + ).map((h) => h.toLowerCase()); + expect(header_names).not.toContain("content-type"); + expect(captured_init?.credentials).toBe("same-origin"); + }); +}); + +describe("resolve_root", () => { + it('should return the base URL if the root path starts with "http://"', () => { + const base_url = "https://huggingface.co"; + const root_path = "https://hmb-hello-world.hf.space"; + const prioritize_base = true; + const result = resolve_root(base_url, root_path, prioritize_base); + expect(result).toBe(base_url); + }); + + it('should return the base URL if the root path starts with "https://"', () => { + const base_url = "https://huggingface.co"; + const root_path = "https://hmb-hello-world.hf.space"; + const prioritize_base = true; + const result = resolve_root(base_url, root_path, prioritize_base); + expect(result).toBe(base_url); + }); +}); + +describe("get_jwt", () => { + it("should return a valid JWT token when the API call is successful", async () => { + const space = "hmb/hello_world"; + const token = "hf_123"; + const expected_jwt = "jwt_123"; + + const result = await get_jwt(space, token); + + expect(result).toBe(expected_jwt); + }); + + it("should return false when the API call fails", async () => { + const space = "hmb/bye_world"; + const token = "hf_123"; + + const result = await get_jwt(space, token); + + expect(result).toBe(false); + }); +}); + +describe("determine_protocol", () => { + it('should return the correct protocols and host when the endpoint starts with "http"', () => { + const endpoint = "http://huggingface.co"; + const result = determine_protocol(endpoint); + expect(result).toEqual({ + ws_protocol: "ws", + http_protocol: "http:", + host: "huggingface.co" + }); + }); + + it('should return the correct protocols and host when the endpoint starts with "https"', () => { + const endpoint = "https://huggingface.co"; + const result = determine_protocol(endpoint); + expect(result).toEqual({ + ws_protocol: "wss", + http_protocol: "https:", + host: "huggingface.co" + }); + }); +}); + +describe("parse_and_set_cookies", () => { + it("should return an empty array when the cookie header is empty", () => { + const cookie_header = ""; + const result = parse_and_set_cookies(cookie_header); + expect(result).toEqual([]); + }); + + it("should parse the cookie header and return an array of cookies", () => { + const cookie_header = "access-token-123=abc;access-token-unsecured-456=def"; + const result = parse_and_set_cookies(cookie_header); + expect(result).toEqual(["access-token-123=abc"]); + }); +}); + +describe("resolve_cookies", () => { + it("should set the cookies when correct auth credentials are provided", async () => { + const client = await Client.connect("hmb/auth_space", { + auth: ["admin", "pass1234"] + }); + + const api = client.view_api(); + expect((await api).named_endpoints["/predict"]).toBeDefined(); + }); + + it("should connect to a private and authenticated space", async () => { + const client = await Client.connect("hmb/private_auth_space", { + token: "hf_123", + auth: ["admin", "pass1234"] + }); + + const api = client.view_api(); + expect((await api).named_endpoints["/predict"]).toBeDefined(); + }); + + it("should not set the cookies when auth credentials are invalid", async () => { + await expect( + Client.connect("hmb/invalid_auth_space", { + auth: ["admin", "wrong_password"] + }) + ).rejects.toThrowError(INVALID_CREDENTIALS_MSG); + }); + + it("should not set the cookies when auth option is not provided in an auth space", async () => { + await expect(Client.connect("hmb/unauth_space")).rejects.toThrowError( + MISSING_CREDENTIALS_MSG + ); + }); +}); diff --git a/client/js/src/test/mock_eventsource.ts b/client/js/src/test/mock_eventsource.ts new file mode 100644 index 0000000..db92054 --- /dev/null +++ b/client/js/src/test/mock_eventsource.ts @@ -0,0 +1,13 @@ +import { vi } from "vitest"; + +if (import.meta.env.TEST_MODE !== "node") { + Object.defineProperty(window, "EventSource", { + writable: true, + value: vi.fn().mockImplementation(() => ({ + close: vi.fn(() => {}), + addEventListener: vi.fn(), + onmessage: vi.fn((_event: MessageEvent) => {}), + onerror: vi.fn((_event: Event) => {}) + })) + }); +} diff --git a/client/js/src/test/post_data.test.ts b/client/js/src/test/post_data.test.ts new file mode 100644 index 0000000..518fb13 --- /dev/null +++ b/client/js/src/test/post_data.test.ts @@ -0,0 +1,70 @@ +import { Client } from "../client"; +import { post_data } from "../utils/post_data"; + +import { initialise_server } from "./server"; +import { BROKEN_CONNECTION_MSG } from "../constants"; +import { beforeAll, afterEach, afterAll, it, expect, describe } from "vitest"; + +let server: Awaited>; + +beforeAll(async () => { + server = await initialise_server(); + await server.start({ quiet: true }); +}); +afterEach(() => server.resetHandlers()); +afterAll(() => server.stop()); + +describe("post_data", () => { + it("should send a POST request with the correct headers and body", async () => { + const app = await Client.connect("hmb/hello_world"); + const config = app.config; + const url = config?.root; + const body = { data: "test" }; + + if (!url) { + throw new Error("No URL provided"); + } + + const [response, status] = await app.post_data(url, body); + + expect(response).toEqual({}); + expect(status).toBe(200); + }); + + it("should handle network errors", async () => { + const app = await Client.connect("hmb/secret_world", { + token: "hf_123" + }); + + const url = "https://hmb-secret-world.hf.space"; + + if (!url) { + throw new Error("No URL provided"); + } + + const [response, status] = await app.post_data(url, {}); + + expect(response).toEqual(BROKEN_CONNECTION_MSG); + expect(status).toBe(500); + }); + + it("honors the credentials client option, so authenticated cross-origin deployments can opt back into cookies", async () => { + let captured_init: RequestInit | undefined; + const fake_client = { + options: { credentials: "include" }, + fetch: (_url: string, init: RequestInit) => { + captured_init = init; + return Promise.resolve(new Response("{}", { status: 200 })); + } + } as unknown as Client; + + const [, status] = await post_data.call( + fake_client, + "https://hmb-hello-world.hf.space/gradio_api/queue/join", + { data: "test" } + ); + + expect(status).toBe(200); + expect(captured_init?.credentials).toBe("include"); + }); +}); diff --git a/client/js/src/test/server.ts b/client/js/src/test/server.ts new file mode 100644 index 0000000..fd82f07 --- /dev/null +++ b/client/js/src/test/server.ts @@ -0,0 +1,31 @@ +import { handlers } from "./handlers"; +// import type { StartOptions } from 'msw'; +import type { SetupWorker, StartOptions } from "msw/browser"; + +const IS_NODE = + typeof process !== "undefined" && process.env.TEST_MODE === "node"; + +interface MockServer { + start: (opts: StartOptions) => void | ReturnType; + stop: () => void | Promise; + resetHandlers: (...handlers: any[]) => void; +} + +export async function initialise_server(): Promise { + if (IS_NODE) { + const { setupServer } = await import("msw/node"); + const server = setupServer(...handlers); + return { + start: (opts: StartOptions) => server.listen(opts), + stop: () => server.close(), + resetHandlers: (...h) => server.resetHandlers(...h) + }; + } + const { setupWorker } = await import("msw/browser"); + const worker = setupWorker(...handlers); + return { + start: (opts: StartOptions) => worker.start(opts), + stop: () => worker.stop(), + resetHandlers: (...h) => worker.resetHandlers(...h) + }; +} diff --git a/client/js/src/test/spaces.test.ts b/client/js/src/test/spaces.test.ts new file mode 100644 index 0000000..1cc58b0 --- /dev/null +++ b/client/js/src/test/spaces.test.ts @@ -0,0 +1,146 @@ +import { SPACE_STATUS_ERROR_MSG } from "../constants"; +import { + discussions_enabled, + get_space_hardware, + set_space_timeout, + check_space_status +} from "../helpers/spaces"; +import { beforeAll, afterEach, afterAll, it, expect, describe } from "vitest"; + +import { initialise_server } from "./server"; +import { hardware_sleeptime_response } from "./test_data"; +import { vi } from "vitest"; + +let server: Awaited>; + +beforeAll(async () => { + server = await initialise_server(); + await server.start({ quiet: true }); +}); +afterEach(() => server.resetHandlers()); +afterAll(() => server.stop()); + +describe("set_space_timeout", () => { + it("should set the sleep timeout for a space", async () => { + const space_id = "hmb/hello_world"; + const timeout = 60; + const token = "hf_123"; + + const response = await set_space_timeout(space_id, timeout, token); + + expect(response).toEqual(hardware_sleeptime_response); + }); + + it("should throw an error if the fetch call fails", async () => { + const space_id = "hmb/server_test"; + const timeout = 60; + const token = "hf_123"; + + await expect(set_space_timeout(space_id, timeout, token)).rejects.toThrow( + "Could not set sleep timeout on duplicated Space. Please visit *ADD HF LINK TO SETTINGS* to set a timeout manually to reduce billing charges." + ); + }); +}); + +describe("get_space_hardware", () => { + it("should return the current hardware for a space", async () => { + const space_id = "hmb/hello_world"; + const token = "hf_123"; + + const hardware = await get_space_hardware(space_id, token); + expect(hardware).toEqual(hardware_sleeptime_response.hardware.current); + }); + + it("should throw an error if the fetch call fails", async () => { + const space_id = "hmb/bye_world"; + + await expect(get_space_hardware(space_id)).rejects.toThrow( + "Space hardware could not be obtained." + ); + }); +}); + +describe("discussions_enabled", () => { + it("should return true if discussions are enabled for the space", async () => { + const space_id = "hmb/hello_world"; + const result = await discussions_enabled(space_id); + expect(result).toBe(true); + }); + + it("should return false if discussions are disabled for the space", async () => { + const space_id = "hmb/bye_world"; + const result = await discussions_enabled(space_id); + expect(result).toBe(false); + }); +}); + +describe("check_space_status", () => { + const status_callback = vi.fn(); + + it("should handle a successful response with RUNNING stage", async () => { + const id = "hmb/hello_world"; + const type = "space_name"; + + await check_space_status(id, type, status_callback); + expect(status_callback).toHaveBeenCalledWith({ + status: "running", + load_status: "complete", + message: "Space is running.", + detail: "RUNNING" + }); + }); + + it("should handle a successful response with PAUSED stage", async () => { + const id = "hmb/paused_space"; + const type = "space_name"; + + await check_space_status(id, type, status_callback); + expect(status_callback).toHaveBeenCalledWith({ + 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: "PAUSED", + discussions_enabled: true + }); + }); + + it("should handle a successful response with BUILDING stage", async () => { + const id = "hmb/building_space"; + const type = "space_name"; + + await check_space_status(id, type, status_callback); + expect(status_callback).toHaveBeenCalledWith({ + status: "building", + load_status: "pending", + message: "Space is building...", + detail: "BUILDING" + }); + }); + + it("should handle a successful response with STOPPED stage", async () => { + const id = "hmb/stopped_space"; + const type = "space_name"; + + await check_space_status(id, type, status_callback); + expect(status_callback).toHaveBeenCalledWith({ + status: "sleeping", + load_status: "pending", + message: "Space is asleep. Waking it up...", + detail: "STOPPED" + }); + }); + + it("should handle a failed response", async () => { + const id = "hmb/failed_space"; + const type = "space_name"; + + await check_space_status(id, type, status_callback); + expect(status_callback).toHaveBeenCalledWith({ + status: "error", + load_status: "error", + message: SPACE_STATUS_ERROR_MSG, + detail: "NOT_FOUND" + }); + }); +}); diff --git a/client/js/src/test/stream.test.ts b/client/js/src/test/stream.test.ts new file mode 100644 index 0000000..649052c --- /dev/null +++ b/client/js/src/test/stream.test.ts @@ -0,0 +1,87 @@ +import { vi, type Mock } from "vitest"; +import { Client } from "../client"; +import { readable_stream } from "../utils/stream"; +import { initialise_server } from "./server"; +import { direct_space_url } from "./handlers.ts"; + +import { + describe, + it, + expect, + afterEach, + beforeAll, + afterAll, + beforeEach +} from "vitest"; + +let server: Awaited>; + +beforeAll(async () => { + server = await initialise_server(); + await server.start({ quiet: true }); +}); +afterEach(() => server.resetHandlers()); +afterAll(() => server.stop()); + +describe("open_stream", () => { + let app: Client; + + beforeEach(async () => { + app = await Client.connect("hmb/hello_world"); + app.stream = vi.fn().mockImplementation(() => { + app.stream_instance = readable_stream( + new URL(`${direct_space_url}/queue/data`) + ); + return app.stream_instance; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should throw an error if config is not defined", async () => { + app.config = undefined; + + await expect(async () => { + await app.open_stream(); + }).rejects.toThrow("Could not resolve app config"); + }); + + it("should connect to the SSE endpoint and handle messages", async () => { + await app.open_stream(); + + const eventsource_mock_call = (app.stream as Mock).mock.calls[0][0]; + + expect(eventsource_mock_call.href).toMatch( + /https:\/\/hmb-hello-world\.hf\.space\/queue\/data\?session_hash/ + ); + + expect(app.stream).toHaveBeenCalledWith(eventsource_mock_call); + + if (!app.stream_instance?.onmessage || !app.stream_instance?.onerror) { + throw new Error("stream instance is not defined"); + } + + const message = { msg: "hello jerry" }; + + app.stream_instance.onmessage({ + data: JSON.stringify(message) + } as MessageEvent); + expect(app.stream_status.open).toBe(true); + + expect(app.event_callbacks).toEqual({}); + expect(app.pending_stream_messages).toEqual({}); + + const close_stream_message = { msg: "close_stream" }; + app.stream_instance.onmessage({ + data: JSON.stringify(close_stream_message) + } as MessageEvent); + expect(app.stream_status.open).toBe(false); + + app.stream_instance.onerror({ + data: JSON.stringify("404") + } as MessageEvent); + expect(app.stream_status.open).toBe(false); + }); +}); diff --git a/client/js/src/test/submit.test.ts b/client/js/src/test/submit.test.ts new file mode 100644 index 0000000..fcf88f7 --- /dev/null +++ b/client/js/src/test/submit.test.ts @@ -0,0 +1,169 @@ +import { describe, beforeAll, afterEach, afterAll, test, expect } from "vitest"; + +import { Client } from "../client"; +import { initialise_server } from "./server"; + +let server: Awaited>; + +beforeAll(async () => { + server = await initialise_server(); + await server.start({ quiet: true }); +}); +afterEach(() => server.resetHandlers()); +afterAll(() => server.stop()); + +async function race_with_timeout( + promise: Promise, + ms: number, + message: string +): Promise { + let timer: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(message)), ms); + }); + try { + return await Promise.race([promise, timeout]); + } finally { + if (timer !== undefined) clearTimeout(timer); + } +} + +describe("submit iterator", () => { + test("next() after the iterator is closed resolves to {done: true}", async () => { + const app = await Client.connect("hmb/hello_world"); + // Avoid opening a real SSE stream — the test does not need one. + app.stream_status.open = true; + + const iterator = app.submit("/predict", ["hi"]); + await iterator.return(); + + const result = await race_with_timeout( + iterator.next(), + 1000, + "next() did not resolve after the iterator was closed" + ); + expect(result).toEqual({ value: undefined, done: true }); + }); + + test("for-await loop terminates when data and complete arrive in the same SSE callback", async () => { + const app = await Client.connect("hmb/hello_world", { + events: ["data", "status"] + }); + app.stream_status.open = true; + + const iterator = app.submit("/predict", ["hi"]); + const event_id = await iterator.wait_for_id(); + expect(event_id).toBeTruthy(); + + const callback = app.event_callbacks[event_id as string]; + expect(callback).toBeDefined(); + + const events: { type: string }[] = []; + const consumer = (async () => { + for await (const event of iterator) { + events.push(event as { type: string }); + } + })(); + + // Let the consumer drain the pending status event pushed by submit() + // and suspend on a fresh next() call, so a resolver is queued before + // the SSE callback fires. + await new Promise((r) => setTimeout(r, 0)); + + // process_completed in the same tick fires a data event (resolving the + // pending resolver), a status complete event (queued to values because + // no resolver is registered at that instant), and then close(). + await callback({ + msg: "process_completed", + output: { data: ["done"] }, + success: true + }); + + await race_with_timeout( + consumer, + 1000, + "submit iterator did not terminate after process_completed" + ); + + const types = events.map((e) => e.type); + expect(types).toContain("data"); + expect(types).toContain("status"); + }); + + test("for-await loop terminates when the server raises an error mid-stream", async () => { + const app = await Client.connect("hmb/hello_world", { + events: ["data", "status"] + }); + app.stream_status.open = true; + + const iterator = app.submit("/predict", ["hi"]); + const event_id = await iterator.wait_for_id(); + expect(event_id).toBeTruthy(); + + const callback = app.event_callbacks[event_id as string]; + expect(callback).toBeDefined(); + + const events: { type: string; stage?: string }[] = []; + const consumer = (async () => { + for await (const event of iterator) { + events.push(event as { type: string; stage?: string }); + } + })(); + + await new Promise((r) => setTimeout(r, 0)); + + // Stream a partial result first, so the error below genuinely arrives + // mid-stream rather than as the very first message. + await callback({ + msg: "process_generating", + output: { data: ["partial"] }, + success: true + }); + + // A server-side exception then arrives as process_completed with + // success:false and an `error` field in the output. This must fire an + // "error" status and terminate the iterator rather than hang the + // consumer. handle_message() reads title/visible/duration from + // `output`, so mirror a real payload's shape here. + await callback({ + msg: "process_completed", + output: { + error: "boom mid-stream", + title: "Error", + visible: true, + duration: 0 + }, + success: false + }); + + await race_with_timeout( + consumer, + 1000, + "submit iterator did not terminate after a server-side error" + ); + + // The partial result should have been delivered before the error. + expect(events.some((e) => e.type === "data")).toBe(true); + + const error_event = events.find( + (e) => e.type === "status" && e.stage === "error" + ); + expect(error_event).toBeDefined(); + }); +}); + +describe("predict error handling", () => { + test("predict() rejects its returned promise when the endpoint does not exist, so the error is catchable", async () => { + const app = await Client.connect("hmb/hello_world"); + + await expect( + race_with_timeout( + app.predict("nonexistent_endpoint", ["hi"]), + 1000, + "predict() never settled for an unknown endpoint" + ) + ).rejects.toThrow( + "There is no endpoint matching that name of fn_index matching that number." + ); + }); +}); diff --git a/client/js/src/test/test_data.ts b/client/js/src/test/test_data.ts new file mode 100644 index 0000000..331dcc9 --- /dev/null +++ b/client/js/src/test/test_data.ts @@ -0,0 +1,562 @@ +// @ts-nocheck +import { ApiData, ApiInfo, Config, EndpointInfo } from "../types"; + +export const runtime_response = { + stage: "RUNNING", + hardware: { + current: "cpu-basic", + requested: "cpu-basic" + }, + storage: { + current: null, + requested: null + }, + gcTimeout: 86400, + replicas: { + current: 1, + requested: 1 + }, + devMode: false, + domains: [ + { + domain: "hmb-hello-world.hf.space", + isCustom: false, + stage: "READY" + } + ] +}; + +export const transformed_api_info: ApiInfo = { + named_endpoints: { + "/predict": { + parameters: [ + { + label: "name", + type: "string", + python_type: { type: "str", description: "" }, + component: "Textbox", + example_input: "Hello!!" + } + ], + returns: [ + { + label: "output", + type: "string", + python_type: { type: "str", description: "" }, + component: "Textbox" + } + ], + type: { generator: false, cancel: false } + } + }, + unnamed_endpoints: { + "0": { + parameters: [ + { + label: "name", + type: "string", + python_type: { type: "str", description: "" }, + component: "Textbox", + example_input: "Hello!!" + } + ], + returns: [ + { + label: "output", + type: "string", + python_type: { type: "str", description: "" }, + component: "Textbox" + } + ], + type: { generator: false, cancel: false } + } + } +}; + +export const response_api_info: ApiInfo = { + named_endpoints: { + "/predict": { + parameters: [ + { + label: "name", + type: { + type: "string" + }, + python_type: { + type: "str", + description: "" + }, + component: "Textbox", + example_input: "Hello!!" + } + ], + returns: [ + { + label: "output", + type: { + type: "string" + }, + python_type: { + type: "str", + description: "" + }, + component: "Textbox" + } + ] + } + }, + unnamed_endpoints: {} +}; + +export const config_response: Config = { + version: "4.27.0", + mode: "interface", + app_id: 123, + dev_mode: false, + analytics_enabled: true, + components: [ + { + id: 3, + type: "row", + props: { + variant: "default", + visible: true, + equal_height: false, + name: "row" + }, + skip_api: true, + component_class_id: "" + }, + { + id: 4, + type: "column", + props: { + scale: 1, + min_width: 320, + variant: "panel", + visible: true, + name: "column" + }, + skip_api: true, + component_class_id: "" + }, + { + id: 5, + type: "column", + props: { + scale: 1, + min_width: 320, + variant: "default", + visible: true, + name: "column" + }, + skip_api: true, + component_class_id: "" + }, + { + id: 1, + type: "textbox", + props: { + lines: 1, + max_lines: 20, + label: "name", + show_label: true, + container: true, + min_width: 160, + visible: true, + autofocus: false, + autoscroll: true, + elem_classes: [], + type: "text", + rtl: false, + show_copy_button: false, + name: "textbox", + _selectable: false + }, + skip_api: false, + component_class_id: "", + api_info: { + type: "string" + }, + example_inputs: "Hello!!" + }, + { + id: 6, + type: "form", + props: { + scale: 0, + min_width: 0, + name: "form" + }, + skip_api: true, + component_class_id: "" + }, + { + id: 7, + type: "row", + props: { + variant: "default", + visible: true, + equal_height: true, + name: "row" + }, + skip_api: true, + component_class_id: "" + }, + { + id: 8, + type: "button", + props: { + value: "Clear", + variant: "secondary", + visible: true, + interactive: true, + elem_classes: [], + api_visibility: "undocumented", + name: "button", + _selectable: false + }, + skip_api: true, + component_class_id: "" + }, + { + id: 9, + type: "button", + props: { + value: "Submit", + variant: "primary", + visible: true, + interactive: true, + elem_classes: [], + name: "button", + _selectable: false + }, + skip_api: true, + component_class_id: "" + }, + { + id: 10, + type: "column", + props: { + scale: 1, + min_width: 320, + variant: "panel", + visible: true, + name: "column" + }, + skip_api: true, + component_class_id: "" + }, + { + id: 2, + type: "textbox", + props: { + lines: 1, + max_lines: 20, + label: "output", + show_label: true, + container: true, + min_width: 160, + interactive: false, + visible: true, + autofocus: false, + autoscroll: true, + elem_classes: [], + type: "text", + rtl: false, + show_copy_button: false, + name: "textbox", + _selectable: false + }, + skip_api: false, + component_class_id: "", + api_info: { + type: "string" + }, + example_inputs: "Hello!!" + }, + { + id: 11, + type: "row", + props: { + variant: "default", + visible: true, + equal_height: true, + name: "row" + }, + skip_api: true, + component_class_id: "" + }, + { + id: 12, + type: "form", + props: { + scale: 0, + min_width: 0, + name: "form" + }, + skip_api: true, + component_class_id: "" + } + ], + css: null, + js: null, + head: null, + title: "Gradio", + space_id: "hmb/hello_world", + enable_queue: true, + show_error: false, + footer_links: ["api", "gradio", "settings"], + is_colab: false, + stylesheets: [], + theme: "default", + protocol: "sse_v3", + body_css: { + body_background_fill: "white", + body_text_color: "#1f2937", + body_background_fill_dark: "#0b0f19", + body_text_color_dark: "#f3f4f6" + }, + fill_height: false, + layout: { + id: 0, + children: [ + { + id: 3, + children: [ + { + id: 4, + children: [ + { + id: 5, + children: [ + { + id: 6, + children: [ + { + id: 1 + } + ] + } + ] + }, + { + id: 7, + children: [ + { + id: 8 + }, + { + id: 9 + } + ] + } + ] + }, + { + id: 10, + children: [ + { + id: 12, + children: [ + { + id: 2 + } + ] + }, + { + id: 11, + children: [] + } + ] + } + ] + } + ] + }, + dependencies: [ + { + id: 0, + targets: [ + [9, "click"], + [1, "submit"] + ], + inputs: [1], + outputs: [2], + backend_fn: true, + js: null, + queue: null, + api_name: "predict", + scroll_to_output: false, + show_progress: "full", + every: null, + batch: false, + max_batch_size: 4, + cancels: [], + types: { + generator: false, + cancel: false + }, + collects_event_data: false, + trigger_after: null, + trigger_only_on_success: false, + trigger_only_on_failure: false, + trigger_mode: "once", + api_visibility: "public", + zerogpu: false + }, + { + id: 1, + targets: [[8, "click"]], + inputs: [], + outputs: [1, 2], + backend_fn: false, + js: "() => [null, null]", + queue: false, + api_name: "js_fn", + scroll_to_output: false, + show_progress: "full", + every: null, + batch: false, + max_batch_size: 4, + cancels: [], + types: { + generator: false, + cancel: false + }, + collects_event_data: false, + trigger_after: null, + trigger_only_on_success: false, + trigger_only_on_failure: false, + trigger_mode: "once", + api_visibility: "private", + zerogpu: false + }, + { + id: 2, + targets: [[8, "click"]], + inputs: [], + outputs: [5], + backend_fn: false, + js: '() => [{"variant": null, "visible": true, "__type__": "update"}]\n ', + queue: false, + api_name: "js_fn_1", + scroll_to_output: false, + show_progress: "full", + every: null, + batch: false, + max_batch_size: 4, + cancels: [], + types: { + generator: false, + cancel: false + }, + collects_event_data: false, + trigger_after: null, + trigger_only_on_success: false, + trigger_only_on_failure: false, + trigger_mode: "once", + api_visibility: "private", + zerogpu: false + } + ], + root: "https://hmb-hello-world.hf.space", + path: "" +}; + +export const whoami_response = { + type: "user", + id: "123", + name: "hmb", + fullname: "jerry", + email: "jerry@gradio.com", + emailVerified: true, + canPay: true, + periodEnd: 123, + isPro: false, + avatarUrl: "", + orgs: [], + auth: { + type: "access_token", + accessToken: { + displayName: "Gradio Client", + role: "write" + } + } +}; + +export const duplicate_response = { + url: "https://huggingface.co/spaces/hmb/hello_world" +}; + +export const hardware_sleeptime_response = { + stage: "RUNNING", + hardware: { + current: "cpu-basic", + requested: "cpu-upgrade" + }, + storage: null, + gcTimeout: 300, + replicas: { + current: 1, + requested: 1 + }, + devMode: false, + domains: [ + { + domain: "hmb-hello-world.hf.space", + isCustom: false, + stage: "READY" + } + ] +}; + +export const endpoint_info: EndpointInfo = { + parameters: [ + { + label: "parameter_2", + parameter_name: "im", + parameter_has_default: false, + parameter_default: null, + type: "", + python_type: { + type: "Dict(background: filepath | None, layers: List[filepath], composite: filepath | None, id: str | None)", + description: "" + }, + component: "Imageeditor", + example_input: { + background: { + path: "", + meta: { + _type: "gradio.FileData" + }, + orig_name: "bus.png", + url: "" + }, + layers: [], + composite: null + } + } + ], + returns: [ + { + label: "value_3", + type: "string", + python_type: { + type: "filepath", + description: "" + }, + component: "Image" + } + ], + type: { + generator: false + } +}; + +export const discussions_response = { + discussions: [], + count: 0, + start: 0, + numClosedDiscussions: 0 +}; diff --git a/client/js/src/test/typing.test.ts b/client/js/src/test/typing.test.ts new file mode 100644 index 0000000..6956609 --- /dev/null +++ b/client/js/src/test/typing.test.ts @@ -0,0 +1,65 @@ +import { describe, test, expect } from "vitest"; +import type { PredictReturn, PredictFunction } from "../types"; + +describe("PredictReturn generic types", () => { + test("PredictReturn with default unknown type", () => { + const result: PredictReturn = { + type: "data", + time: new Date(), + data: ["any", "data"], + endpoint: "/predict", + fn_index: 0 + }; + expect(result.data).toBeDefined(); + }); + + test("PredictReturn with custom type", () => { + type MyData = { text: string; confidence: number }; + const result: PredictReturn = { + type: "data", + time: new Date(), + data: { text: "hello", confidence: 0.95 }, + endpoint: "/predict", + fn_index: 0 + }; + expect(result.data.text).toBe("hello"); + expect(result.data.confidence).toBe(0.95); + }); + + test("PredictReturn with array type", () => { + type ArrayData = string[]; + const result: PredictReturn = { + type: "data", + time: new Date(), + data: ["hello", "world"], + endpoint: "/predict", + fn_index: 0 + }; + expect(result.data[0]).toBe("hello"); + expect(result.data.length).toBe(2); + }); + + test("PredictFunction type signature allows generic parameter", () => { + // This test verifies that PredictFunction accepts a generic type parameter + type MyResponse = { output: string }; + + // Mock function that matches PredictFunction signature + const mockPredict: PredictFunction = async ( + endpoint: string | number, + data?: unknown[] | Record, + event_data?: unknown + ): Promise> => { + return { + type: "data", + time: new Date(), + data: { output: "test" } as T, + endpoint: String(endpoint), + fn_index: 0 + }; + }; + + // Type check: calling with generic parameter + const typedResult = mockPredict("/predict", ["input"]); + expect(typedResult).toBeDefined(); + }); +}); diff --git a/client/js/src/test/upload_files.test.ts b/client/js/src/test/upload_files.test.ts new file mode 100644 index 0000000..508e7e5 --- /dev/null +++ b/client/js/src/test/upload_files.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, afterEach, beforeAll, afterAll } from "vitest"; + +import { Client } from ".."; +import { initialise_server } from "./server"; + +let server: Awaited>; + +beforeAll(async () => { + server = await initialise_server(); + await server.start({ quiet: true }); +}); +afterEach(() => server.resetHandlers()); +afterAll(() => server.stop()); + +describe("upload_files", () => { + it("should upload files successfully", async () => { + const root_url = "https://hmb-hello-world.hf.space"; + + const client = await Client.connect("hmb/hello_world", { + token: "hf_token" + }); + + const files = [new Blob([], { type: "image/jpeg" })]; + + const response = await client.upload_files(root_url, files); + + if (!response.files) { + throw new Error("No files returned"); + } + + expect(response.files).toHaveLength(1); + expect(response.files[0]).toBe("lion.jpg"); + }); + + it.skip("should handle a server error when connected to a running app and uploading files", async () => { + const client = await Client.connect("hmb/server_test"); + + const root_url = "https://hmb-server-test.hf.space"; + const files = [new Blob([""], { type: "text/plain" })]; + + await expect(client.upload_files(root_url, files)).rejects.toThrow( + "Connection errored out. Failed to fetch" + ); + }); +}); diff --git a/client/js/src/test/view_api.test.ts b/client/js/src/test/view_api.test.ts new file mode 100644 index 0000000..ebc822a --- /dev/null +++ b/client/js/src/test/view_api.test.ts @@ -0,0 +1,56 @@ +import { describe, beforeAll, afterEach, afterAll, test, expect } from "vitest"; + +import { Client, client, duplicate } from ".."; +import { transformed_api_info, config_response } from "./test_data"; +import { initialise_server } from "./server"; + +const app_reference = "hmb/hello_world"; +const secret_app_reference = "hmb/secret_world"; +const secret_direct_app_reference = "https://hmb-secret-world.hf.space"; + +let server: Awaited>; + +beforeAll(async () => { + server = await initialise_server(); + await server.start({ quiet: true }); +}); +afterEach(() => server.resetHandlers()); +afterAll(() => server.stop()); + +describe("view_api", () => { + test("viewing the api of a running, public app", async () => { + const app = await Client.connect(app_reference); + + expect(await app.view_api()).toEqual(transformed_api_info); + }); + + test("viewing the api of a running, private app", async () => { + const app = await Client.connect(secret_app_reference, { + token: "hf_123" + }); + + expect(app.config).toEqual({ + ...config_response, + root: secret_direct_app_reference + }); + + expect(await app.view_api()).toEqual({ + ...transformed_api_info + }); + }); + + test("viewing the api of a running, private app with a direct app URL", async () => { + const app = await Client.connect(secret_direct_app_reference, { + token: "hf_123" + }); + + expect(app.config).toEqual({ + ...config_response, + root: secret_direct_app_reference + }); + + expect(await app.view_api()).toEqual({ + ...transformed_api_info + }); + }); +}); diff --git a/client/js/src/types.ts b/client/js/src/types.ts new file mode 100644 index 0000000..c037134 --- /dev/null +++ b/client/js/src/types.ts @@ -0,0 +1,436 @@ +// API Data Types + +import { hardware_types } from "./helpers/spaces"; +import type { SvelteComponent } from "svelte"; +import type { ComponentType } from "svelte"; + +export interface ApiData { + label: string; + parameter_name: string; + parameter_default?: any; + parameter_has_default?: boolean; + type: { + type: any; + description: string; + }; + component: string; + example_input?: any; + python_type: { type: string; description: string }; + serializer: string; +} + +export interface JsApiData { + label: string; + parameter_name: string; + parameter_default?: any; + parameter_has_default?: boolean; + type: string; + description: string; + component: string; + example_input?: any; + serializer: string; + python_type: { type: string; description: string }; +} + +export interface EndpointInfo { + parameters: T[]; + returns: T[]; + type?: DependencyTypes; +} + +export interface ApiInfo { + named_endpoints: Record>; + unnamed_endpoints: Record>; +} + +export interface BlobRef { + path: string[]; + type: string | undefined; + blob: Blob | File | false; +} + +export type DataType = string | Buffer | Record | any[]; + +// custom class used for uploading local files +export class Command { + type: string; + command: string; + meta: { + path: string; + name: string; + orig_path: string; + }; + fileData?: FileData; + + constructor( + command: string, + meta: { path: string; name: string; orig_path: string } + ) { + this.type = "command"; + this.command = command; + this.meta = meta; + } +} + +// Function Signature Types + +export type SubmitFunction = ( + endpoint: string | number, + data?: unknown[] | Record, + event_data?: unknown, + trigger_id?: number | null, + all_events?: boolean, + additional_headers?: Record +) => SubmitIterable; + +export type PredictFunction = ( + endpoint: string | number, + data?: unknown[] | Record, + event_data?: unknown +) => Promise>; + +export type client_return = { + config: Config | undefined; + predict: PredictFunction; + submit: SubmitFunction; + component_server: ( + component_id: number, + fn_name: string, + data: unknown[] + ) => any; + view_api: (_fetch: typeof fetch) => Promise>; +}; + +export interface SubmitIterable extends AsyncIterable { + [Symbol.asyncIterator](): SubmitIterable; + next: () => Promise>; + throw: (value: unknown) => Promise>; + return: () => Promise>; + cancel: () => Promise; + event_id: () => string; + send_chunk: (payload: Record) => void; + wait_for_id: () => Promise; + close_stream: () => void; +} + +export type PredictReturn = { + type: EventType; + time: Date; + data: T; + endpoint: string; + fn_index: number; +}; + +// Space Status Types + +export type SpaceStatus = SpaceStatusNormal | SpaceStatusError; + +export interface SpaceStatusNormal { + status: + | "sleeping" + | "running" + | "building" + | "error" + | "stopped" + | "starting"; + detail: + | "SLEEPING" + | "RUNNING" + | "RUNNING_BUILDING" + | "BUILDING" + | "APP_STARTING" + | "NOT_FOUND"; + load_status: "pending" | "error" | "complete" | "generating"; + message: string; +} + +export interface SpaceStatusError { + status: "space_error" | "paused"; + detail: + | "NO_APP_FILE" + | "CONFIG_ERROR" + | "BUILD_ERROR" + | "RUNTIME_ERROR" + | "PAUSED"; + load_status: "error"; + message: string; + discussions_enabled: boolean; +} + +export type SpaceStatusCallback = (a: SpaceStatus) => void; + +// Configuration and Response Types +// -------------------------------- +export interface Config { + deep_link_state?: "none" | "valid" | "invalid"; + auth_required?: true; + app_id?: string; + analytics_enabled: boolean; + connect_heartbeat: boolean; + dev_mode: boolean; + vibe_mode: boolean; + auth_message: string; + components: ComponentMeta[]; + css: string | null; + js: string | null; + head: string | null; + dependencies: Dependency[]; + enable_queue: boolean; + show_error: boolean; + layout: any; + mode: "blocks" | "interface" | "chat_interface"; + root: string; + root_url?: string; + theme: string; + title: string; + version: string; + space_id: string | null; + is_space: boolean; + is_colab: boolean; + footer_links: string[]; + stylesheets: string[]; + current_page: string; + page: Record< + string, + { + components: number[]; + dependencies: number[]; + layout: any; + } + >; + pages: [string, string, boolean][]; + protocol: "sse_v3" | "sse_v2.1" | "sse_v2" | "sse_v1" | "sse" | "ws"; + max_file_size?: number; + theme_hash?: number; + username: string | null; + api_prefix?: string; + fill_height?: boolean; + fill_width?: boolean; + pwa?: boolean; + i18n_translations?: Record> | null; + mcp_server?: boolean; +} + +// todo: DRY up types +export interface ComponentMeta { + type: string; + id: number; + has_modes: boolean; + props: SharedProps; + instance: SvelteComponent; + component: ComponentType; + documentation?: Documentation; + children?: ComponentMeta[]; + parent?: ComponentMeta; + value?: any; + component_class_id: string; + key: string | number | null; + rendered_in?: number; +} + +interface SharedProps { + elem_id?: string; + elem_classes?: string[]; + components?: string[]; + server_fns?: string[]; + interactive: boolean; + visible: boolean | "hidden"; + [key: string]: unknown; + root_url?: string; +} + +export interface Documentation { + type?: TypeDescription; + description?: TypeDescription; + example_data?: string; +} + +interface TypeDescription { + input_payload?: string; + response_object?: string; + payload?: string; +} + +export interface Dependency { + id: number; + targets: [number, string][]; + inputs: number[]; + outputs: number[]; + backend_fn: boolean; + js: string | null; + scroll_to_output: boolean; + trigger: "click" | "load" | string; + max_batch_size: number; + show_progress: "full" | "minimal" | "hidden"; + show_progress_on: number[] | null; + frontend_fn: ((...args: unknown[]) => Promise) | null; + status?: string; + queue: boolean | null; + every: number | null; + batch: boolean; + api_name: string | null; + cancels: number[]; + types: DependencyTypes; + collects_event_data: boolean; + pending_request?: boolean; + trigger_after?: number; + trigger_only_on_success?: boolean; + trigger_only_on_failure?: boolean; + trigger_mode: "once" | "multiple" | "always_last"; + final_event: Payload | null; + api_visibility: "public" | "private" | "undocumented"; + rendered_in: number | null; + render_id: number | null; + connection: "stream" | "sse"; + time_limit: number; + stream_every: number; + like_user_message: boolean; + event_specific_args: string[]; + component_prop_inputs: number[]; + js_implementation: string | null; +} + +export interface DependencyTypes { + generator: boolean; + cancel: boolean; +} + +export interface Payload { + fn_index: number; + data: unknown[]; + time?: Date; + event_data?: unknown; + trigger_id?: number | null; +} + +export interface PostResponse { + error?: string; + [x: string]: any; +} + +export interface UploadResponse { + error?: string; + files?: string[]; +} + +// Client and File Handling Types + +export interface DuplicateOptions extends ClientOptions { + private?: boolean; + hardware?: (typeof hardware_types)[number]; + timeout?: number; +} + +export interface ClientOptions { + token?: `hf_${string}`; + status_callback?: SpaceStatusCallback | null; + auth?: [string, string] | null; + with_null_state?: boolean; + events?: EventType[]; + headers?: Record | Headers; + query_params?: Record; + session_hash?: string; + cookies?: string; + credentials?: RequestCredentials; +} + +export interface FileData { + name: string; + orig_name?: string; + size?: number; + data: string; + blob?: File; + is_file?: boolean; + mime_type?: string; + alt_text?: string; +} + +// Event and Listener Types + +export type EventType = "data" | "status" | "log" | "render"; + +export interface EventMap { + data: PayloadMessage; + status: StatusMessage; + log: LogMessage; + render: RenderMessage; +} + +export type GradioEvent = { + [P in EventType]: EventMap[P]; +}[EventType]; + +export interface Log { + log: string; + title: string; + level: "warning" | "info" | "success" | "error"; +} +export interface Render { + data: { + components: any[]; + layout: any; + dependencies: Dependency[]; + render_id: number; + }; +} + +export interface ValidationError { + is_valid: boolean; + message: string; +} + +export interface Status { + queue: boolean; + code?: string; + success?: boolean; + stage: "pending" | "error" | "complete" | "generating" | "streaming"; + duration?: number; + visible?: boolean; + broken?: boolean; + size?: number; + position?: number; + eta?: number; + title?: string; + message?: string | ValidationError[]; + progress_data?: { + progress: number | null; + index: number | null; + length: number | null; + unit: string | null; + desc: string | null; + }[]; + time?: Date; + changed_state_ids?: number[]; + time_limit?: number; + session_not_found?: boolean; + used_cache?: "full" | "partial" | null; + cache_duration?: number; + avg_time?: number; +} + +export interface StatusMessage extends Status { + type: "status"; + endpoint: string; + fn_index: number; + original_msg?: string; +} + +export interface PayloadMessage extends Payload { + type: "data"; + endpoint: string; + fn_index: number; +} + +export interface LogMessage extends Log { + type: "log"; + endpoint: string; + fn_index: number; + duration: number | null; + visible: boolean; +} + +export interface RenderMessage extends Render { + type: "render"; + endpoint: string; + fn_index: number; +} diff --git a/client/js/src/upload.ts b/client/js/src/upload.ts new file mode 100644 index 0000000..ecbe277 --- /dev/null +++ b/client/js/src/upload.ts @@ -0,0 +1,109 @@ +import type { Client } from "./client"; +import { filesize } from "./utils/filesize"; +export async function upload( + this: Client, + file_data: FileData[], + root_url: string, + upload_id?: string, + max_file_size?: number +): Promise<(FileData | null)[] | null> { + let files = (Array.isArray(file_data) ? file_data : [file_data]).map( + (file_data) => file_data.blob! + ); + + const oversized_files = files.filter( + (f) => f.size > (max_file_size ?? Infinity) + ); + if (oversized_files.length) { + throw new Error( + `File(s) exceed the maximum allowed size of ${filesize(max_file_size || Infinity)}: ${oversized_files + .map((f) => `"${f.name}"`) + .join(", ")}` + ); + } + + return await Promise.all( + await this.upload_files(root_url, files, upload_id).then( + async (response: { files?: string[]; error?: string }) => { + if (response.error) { + throw new Error(response.error); + } else { + if (response.files) { + return response.files.map((f, i) => { + const file = new FileData({ + ...file_data[i], + path: f, + url: `${root_url}${this.api_prefix}/file=${f}` + }); + return file; + }); + } + + return []; + } + } + ) + ); +} + +export async function prepare_files( + files: File[], + is_stream?: boolean +): Promise { + return files.map( + (f) => + new FileData({ + path: f.name, + orig_name: f.name, + blob: f, + size: f.size, + mime_type: f.type, + is_stream + }) + ); +} + +export class FileData { + path: string; + url?: string; + orig_name?: string; + size?: number; + blob?: File; + is_stream?: boolean; + mime_type?: string; + alt_text?: string; + b64?: string; + readonly meta = { _type: "gradio.FileData" }; + + constructor({ + path, + url, + orig_name, + size, + blob, + is_stream, + mime_type, + alt_text, + b64 + }: { + path: string; + url?: string; + orig_name?: string; + size?: number; + blob?: File; + is_stream?: boolean; + mime_type?: string; + alt_text?: string; + b64?: string; + }) { + this.path = path; + this.url = url; + this.orig_name = orig_name; + this.size = size; + this.blob = url ? undefined : blob; + this.is_stream = is_stream; + this.mime_type = mime_type; + this.alt_text = alt_text; + this.b64 = b64; + } +} diff --git a/client/js/src/utils/duplicate.ts b/client/js/src/utils/duplicate.ts new file mode 100644 index 0000000..11d763d --- /dev/null +++ b/client/js/src/utils/duplicate.ts @@ -0,0 +1,128 @@ +import { + get_space_hardware, + hardware_types, + set_space_timeout +} from "../helpers/spaces"; +import type { DuplicateOptions } from "../types"; +import { Client } from "../client"; +import { SPACE_METADATA_ERROR_MSG } from "../constants"; +import { + get_cookie_header, + parse_and_set_cookies +} from "../helpers/init_helpers"; +import { process_endpoint } from "../helpers/api_info"; + +export async function duplicate( + app_reference: string, + options: DuplicateOptions +): Promise { + const { token, private: _private, hardware, timeout, auth } = options; + + if (hardware && !hardware_types.includes(hardware)) { + throw new Error( + `Invalid hardware type provided. Valid types are: ${hardware_types + .map((v) => `"${v}"`) + .join(",")}.` + ); + } + + const { http_protocol, host } = await process_endpoint(app_reference, token); + + let cookies: string[] | null = null; + + if (auth) { + const cookie_header = await get_cookie_header( + http_protocol, + host, + auth, + fetch, + undefined, + options.credentials + ); + + if (cookie_header) cookies = parse_and_set_cookies(cookie_header); + } + + const headers = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + ...(cookies ? { Cookie: cookies.join("; ") } : {}) + }; + + const user = ( + await ( + await fetch(`https://huggingface.co/api/whoami-v2`, { + headers + }) + ).json() + ).name; + + const space_name = app_reference.split("/")[1]; + const body: { + repository: string; + private?: boolean; + hardware?: string; + } = { + repository: `${user}/${space_name}` + }; + + if (_private) { + body.private = true; + } + + let original_hardware; + + try { + if (!hardware) { + original_hardware = await get_space_hardware(app_reference, token); + } + } catch (e) { + throw Error(SPACE_METADATA_ERROR_MSG + (e as Error).message); + } + + const requested_hardware = hardware || original_hardware || "cpu-basic"; + + body.hardware = requested_hardware; + + try { + const response = await fetch( + `https://huggingface.co/api/spaces/${app_reference}/duplicate`, + { + method: "POST", + headers, + body: JSON.stringify(body) + } + ); + + if (response.status === 409) { + try { + const client = await Client.connect(`${user}/${space_name}`, options); + return client; + } catch (error) { + console.error("Failed to connect Client instance:", error); + throw error; + } + } else if (response.status !== 200) { + throw new Error(response.statusText); + } + + const duplicated_space = await response.json(); + + await set_space_timeout(`${user}/${space_name}`, timeout || 300, token); + + return await Client.connect( + get_space_reference(duplicated_space.url), + options + ); + } catch (e: any) { + throw new Error(e); + } +} + +function get_space_reference(url: string): any { + const regex = /https:\/\/huggingface.co\/spaces\/([^/]+\/[^/]+)/; + const match = url.match(regex); + if (match) { + return match[1]; + } +} diff --git a/client/js/src/utils/filesize.ts b/client/js/src/utils/filesize.ts new file mode 100644 index 0000000..3e28e69 --- /dev/null +++ b/client/js/src/utils/filesize.ts @@ -0,0 +1,41 @@ +type SPEC = { + readonly radix: number; + readonly unit: string[]; +}; + +const si = { + radix: 1e3, + unit: ["b", "kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb"] +}; +const iec = { + radix: 1024, + unit: ["b", "Kib", "Mib", "Gib", "Tib", "Pib", "Eib", "Zib", "Yib"] +}; +const jedec = { + radix: 1024, + unit: ["b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb"] +}; + +export const SPECS: Record = { + si, + iec, + jedec +}; + +/** + * file size from https://github.com/hustcc/filesize.js + * @param bytes - The number of bytes to convert to human-readable format + * @param fixed - Number of decimal places to display (default: 1) + * @param spec - Size specification to use: "si", "iec", or "jedec" (default: "jedec") + * @returns Human-readable file size string + */ +export function filesize(bytes: number, fixed = 1, spec = "jedec"): string { + bytes = Math.abs(bytes); + const { radix, unit } = SPECS[spec] || SPECS.jedec; + let loop = 0; + while (bytes >= radix) { + bytes /= radix; + ++loop; + } + return `${bytes.toFixed(fixed)} ${unit[loop]}`; +} diff --git a/client/js/src/utils/handle_blob.ts b/client/js/src/utils/handle_blob.ts new file mode 100644 index 0000000..815a965 --- /dev/null +++ b/client/js/src/utils/handle_blob.ts @@ -0,0 +1,142 @@ +import { update_object, walk_and_store_blobs } from "../helpers/data"; +import { + Command, + type ApiData, + type EndpointInfo, + type JsApiData +} from "../types"; +import { FileData } from "../upload"; +import type { Client } from ".."; +import { + FILE_PROCESSING_ERROR_MSG, + NODEJS_FS_ERROR_MSG, + ROOT_URL_ERROR_MSG +} from "../constants"; + +export async function handle_blob( + this: Client, + endpoint: string, + data: unknown[], + api_info: EndpointInfo +): Promise { + const self = this; + + await process_local_file_commands(self, data); + + const blobRefs = await walk_and_store_blobs( + data, + undefined, + [], + true, + api_info + ); + + const results = await Promise.all( + blobRefs.map(async ({ path, blob, type }) => { + if (!blob) return { path, type }; + + const response = await self.upload_files(endpoint, [blob]); + const file_url = response.files && response.files[0]; + return { + path, + file_url, + type, + name: + typeof File !== "undefined" && blob instanceof File + ? blob?.name + : undefined + }; + }) + ); + + results.forEach(({ path, file_url, type, name }) => { + if (type === "Gallery") { + update_object(data, file_url, path); + } else if (file_url) { + const file = new FileData({ path: file_url, orig_name: name }); + update_object(data, file, path); + } + }); + + return data; +} + +export async function process_local_file_commands( + client: Client, + data: unknown[] +): Promise { + const root = client.config?.root || client.config?.root_url; + + if (!root) { + throw new Error(ROOT_URL_ERROR_MSG); + } + + await recursively_process_commands(client, data); +} + +async function recursively_process_commands( + client: Client, + data: any, + path: string[] = [] +): Promise { + for (const key in data) { + if (data[key] instanceof Command) { + await process_single_command(client, data, key); + } else if (typeof data[key] === "object" && data[key] !== null) { + await recursively_process_commands(client, data[key], [...path, key]); + } + } +} + +async function process_single_command( + client: Client, + data: any, + key: string +): Promise { + let cmd_item = data[key] as Command; + const root = client.config?.root || client.config?.root_url; + + if (!root) { + throw new Error(ROOT_URL_ERROR_MSG); + } + + try { + let fileBuffer: Buffer; + let fullPath: string; + + // check if running in a Node.js environment + if ( + typeof process !== "undefined" && + process.versions && + process.versions.node + ) { + const fs = await import("fs/promises"); + const path = await import("path"); + + fullPath = path.resolve(process.cwd(), cmd_item.meta.path); + fileBuffer = await fs.readFile(fullPath); // Read file from disk + } else { + throw new Error(NODEJS_FS_ERROR_MSG); + } + + const file = new Blob([fileBuffer as any], { + type: "application/octet-stream" + }); + + const response = await client.upload_files(root, [file]); + + const file_url = response.files && response.files[0]; + + if (file_url) { + const fileData = new FileData({ + path: file_url, + orig_name: cmd_item.meta.name || "" + }); + + // replace the command object with the fileData object + data[key] = fileData; + } + } catch (error) { + console.error(FILE_PROCESSING_ERROR_MSG, error); + } +} diff --git a/client/js/src/utils/post_data.ts b/client/js/src/utils/post_data.ts new file mode 100644 index 0000000..44ae427 --- /dev/null +++ b/client/js/src/utils/post_data.ts @@ -0,0 +1,38 @@ +import { BROKEN_CONNECTION_MSG } from "../constants"; +import type { PostResponse } from "../types"; +import { Client } from ".."; + +export async function post_data( + this: Client, + url: string, + body: unknown, + additional_headers?: any +): Promise<[PostResponse, number]> { + const headers: { + Authorization?: string; + "Content-Type": "application/json"; + } = { "Content-Type": "application/json" }; + if (this.options.token) { + headers.Authorization = `Bearer ${this.options.token}`; + } + try { + var response = await this.fetch(url, { + method: "POST", + body: JSON.stringify(body), + headers: { ...headers, ...additional_headers }, + credentials: this.options.credentials ?? "same-origin" + }); + } catch (e) { + return [{ error: BROKEN_CONNECTION_MSG }, 500]; + } + let output: PostResponse; + let status: number; + try { + output = await response.json(); + status = response.status; + } catch (e) { + output = { error: `Could not parse server response: ${e}` }; + status = 500; + } + return [output, status]; +} diff --git a/client/js/src/utils/predict.ts b/client/js/src/utils/predict.ts new file mode 100644 index 0000000..3a850f4 --- /dev/null +++ b/client/js/src/utils/predict.ts @@ -0,0 +1,53 @@ +import { Client } from "../client"; +import type { Dependency, PredictReturn } from "../types"; + +export async function predict( + this: Client, + endpoint: string | number, + data: unknown[] | Record = {} +): Promise> { + let data_returned = false; + let status_complete = false; + let dependency: Dependency; + + if (!this.config) { + throw new Error("Could not resolve app config"); + } + + if (typeof endpoint === "number") { + dependency = this.config.dependencies.find((dep) => dep.id == endpoint)!; + } else { + const trimmed_endpoint = endpoint.replace(/^\//, ""); + dependency = this.config.dependencies.find( + (dep) => dep.id == this.api_map[trimmed_endpoint] + )!; + } + + const app = this.submit(endpoint, data, null, null, true); + let result: unknown; + + for await (const message of app) { + if (message.type === "data") { + data_returned = true; + result = message; + if (status_complete) { + return result as PredictReturn; + } + } + + if (message.type === "status") { + if (message.stage === "error") { + throw message; + } + if (message.stage === "complete") { + status_complete = true; + // if complete message comes after data, resolve here + if (data_returned) { + return result as PredictReturn; + } + } + } + } + + return result as PredictReturn; +} diff --git a/client/js/src/utils/stream.ts b/client/js/src/utils/stream.ts new file mode 100644 index 0000000..d5f2446 --- /dev/null +++ b/client/js/src/utils/stream.ts @@ -0,0 +1,229 @@ +import { BROKEN_CONNECTION_MSG, SSE_URL } from "../constants"; +import type { Client } from "../client"; +import { stream } from "fetch-event-stream"; + +export async function open_stream(this: Client): Promise { + let { + event_callbacks, + unclosed_events, + pending_stream_messages, + stream_status, + config, + jwt + } = this; + + const that = this; + + if (!config) { + throw new Error("Could not resolve app config"); + } + + stream_status.open = true; + + let stream: EventSource | null = null; + let params = new URLSearchParams({ + session_hash: this.session_hash + }).toString(); + + let url = new URL(`${config.root}${this.api_prefix}/${SSE_URL}?${params}`); + + if (jwt) { + url.searchParams.set("__sign", jwt); + } + + stream = this.stream(url); + + if (!stream) { + console.warn("Cannot connect to SSE endpoint: " + url.toString()); + return; + } + + stream.onmessage = async function (event: MessageEvent) { + let _data = JSON.parse(event.data); + if (_data.msg === "close_stream") { + close_stream(stream_status, that.abort_controller); + return; + } + const event_id = _data.event_id; + if (!event_id) { + await Promise.all( + Object.keys(event_callbacks).map((event_id) => + event_callbacks[event_id](_data) + ) + ); + } else if (event_callbacks[event_id] && config) { + if ( + _data.msg === "process_completed" && + ["sse", "sse_v1", "sse_v2", "sse_v2.1", "sse_v3"].includes( + config.protocol + ) + ) { + unclosed_events.delete(event_id); + } + let fn: (data: any) => void = event_callbacks[event_id]; + + if (typeof window !== "undefined" && typeof document !== "undefined") { + // fn(_data); // need to do this to put the event on the end of the event loop, so the browser can refresh between callbacks and not freeze in case of quick generations. See + setTimeout(fn, 0, _data); // need to do this to put the event on the end of the event loop, so the browser can refresh between callbacks and not freeze in case of quick generations. See https://github.com/gradio-app/gradio/pull/7055 + } else { + fn(_data); + } + } else { + if (!pending_stream_messages[event_id]) { + pending_stream_messages[event_id] = []; + } + pending_stream_messages[event_id].push(_data); + } + }; + stream.onerror = async function (e) { + console.error(e); + await Promise.all( + Object.keys(event_callbacks).map((event_id) => + event_callbacks[event_id]({ + msg: "broken_connection", + message: BROKEN_CONNECTION_MSG + }) + ) + ); + }; +} + +export function close_stream( + stream_status: { open: boolean }, + abort_controller: AbortController | null +): void { + if (stream_status) { + stream_status.open = false; + abort_controller?.abort(); + } +} + +export function apply_diff_stream( + pending_diff_streams: Record, + event_id: string, + data: any +): void { + let is_first_generation = !pending_diff_streams[event_id]; + if (is_first_generation) { + pending_diff_streams[event_id] = []; + data.data.forEach((value: any, i: number) => { + pending_diff_streams[event_id][i] = value; + }); + } else { + data.data.forEach((value: any, i: number) => { + let new_data = apply_diff(pending_diff_streams[event_id][i], value); + pending_diff_streams[event_id][i] = new_data; + data.data[i] = new_data; + }); + } +} + +export function apply_diff( + obj: any, + diff: [string, (number | string)[], any][] +): any { + diff.forEach(([action, path, value]) => { + obj = apply_edit(obj, path, action, value); + }); + + return obj; +} + +function apply_edit( + target: any, + path: (number | string)[], + action: string, + value: any +): any { + if (path.length === 0) { + if (action === "replace") { + return value; + } else if (action === "append") { + return target + value; + } + throw new Error(`Unsupported action: ${action}`); + } + + let current = target; + for (let i = 0; i < path.length - 1; i++) { + current = current[path[i]]; + } + + const last_path = path[path.length - 1]; + switch (action) { + case "replace": + current[last_path] = value; + break; + case "append": + current[last_path] += value; + break; + case "add": + if (Array.isArray(current)) { + current.splice(Number(last_path), 0, value); + } else { + current[last_path] = value; + } + break; + case "delete": + if (Array.isArray(current)) { + current.splice(Number(last_path), 1); + } else { + delete current[last_path]; + } + break; + default: + throw new Error(`Unknown action: ${action}`); + } + return target; +} + +export function readable_stream( + input: RequestInfo | URL, + init: RequestInit = {} +): EventSource { + const instance: EventSource & { readyState: number } = { + close: () => { + console.warn("Method not implemented."); + }, + onerror: null, + onmessage: null, + onopen: null, + readyState: 0, + url: input.toString(), + withCredentials: false, + CONNECTING: 0, + OPEN: 1, + CLOSED: 2, + addEventListener: () => { + throw new Error("Method not implemented."); + }, + dispatchEvent: () => { + throw new Error("Method not implemented."); + }, + removeEventListener: () => { + throw new Error("Method not implemented."); + } + }; + + stream(input, init) + .then(async (res) => { + instance.readyState = instance.OPEN; + try { + for await (const chunk of res) { + //@ts-ignore + instance.onmessage && instance.onmessage(chunk); + } + instance.readyState = instance.CLOSED; + } catch (e) { + instance.onerror && instance.onerror(e as Event); + instance.readyState = instance.CLOSED; + } + }) + .catch((e) => { + console.error(e); + instance.onerror && instance.onerror(e as Event); + instance.readyState = instance.CLOSED; + }); + + return instance as EventSource; +} diff --git a/client/js/src/utils/submit.ts b/client/js/src/utils/submit.ts new file mode 100644 index 0000000..bc982a4 --- /dev/null +++ b/client/js/src/utils/submit.ts @@ -0,0 +1,760 @@ +import type { + Status, + Payload, + GradioEvent, + JsApiData, + EndpointInfo, + ApiInfo, + Config, + Dependency, + SubmitIterable +} from "../types"; + +import { skip_queue, post_message, handle_payload } from "../helpers/data"; +import { get_zerogpu_origin } from "../helpers/zerogpu"; +import { + handle_message, + map_data_to_params, + process_endpoint +} from "../helpers/api_info"; +import { + BROKEN_CONNECTION_MSG, + QUEUE_FULL_MSG, + SSE_URL, + SSE_DATA_URL, + RESET_URL, + CANCEL_URL +} from "../constants"; +import { apply_diff_stream, close_stream } from "./stream"; +import { Client } from "../client"; + +export function submit( + this: Client, + endpoint: string | number, + data: unknown[] | Record = {}, + event_data?: unknown, + trigger_id?: number | null, + all_events?: boolean, + additional_headers?: Record +): SubmitIterable { + try { + const { token } = this.options; + const { + fetch, + app_reference, + config, + session_hash, + api_info, + api_map, + stream_status, + pending_stream_messages, + pending_diff_streams, + event_callbacks, + unclosed_events, + post_data, + options, + api_prefix + } = this; + + const addt_headers = additional_headers || { "x-gradio-user": "api" }; + + const that = this; + + if (!api_info) throw new Error("No API found"); + if (!config) throw new Error("Could not resolve app config"); + + let { fn_index, endpoint_info, dependency } = get_endpoint_info( + api_info, + endpoint, + api_map, + config + ); + + let resolved_data = map_data_to_params(data, endpoint_info); + + let stream: EventSource | null; + let protocol = config.protocol ?? "ws"; + if (protocol === "ws") { + throw new Error("WebSocket protocol is not supported in this version"); + } + let event_id_final = ""; + let event_id_cb: () => string = () => event_id_final; + + const _endpoint = typeof endpoint === "number" ? "/predict" : endpoint; + let payload: Payload; + let event_id: string | null = null; + let complete: Status | undefined | false = false; + let last_status: Record = {}; + let url_params = + typeof window !== "undefined" && typeof document !== "undefined" + ? new URLSearchParams(window.location.search).toString() + : ""; + + const events_to_publish = + options?.events?.reduce( + (acc, event) => { + acc[event] = true; + return acc; + }, + {} as Record + ) || {}; + + // event subscription methods + function fire_event(event: GradioEvent): void { + if (all_events || events_to_publish[event.type]) { + push_event(event); + } + } + + async function cancel(): Promise { + let reset_request = {}; + let cancel_request = {}; + reset_request = { event_id }; + cancel_request = { event_id, session_hash, fn_index }; + + try { + if (!config) { + throw new Error("Could not resolve app config"); + } + + if ("event_id" in cancel_request) { + await fetch(`${config.root}${api_prefix}/${CANCEL_URL}`, { + headers: { "Content-Type": "application/json" }, + method: "POST", + body: JSON.stringify(cancel_request) + }); + } + + await fetch(`${config.root}${api_prefix}/${RESET_URL}`, { + headers: { "Content-Type": "application/json" }, + method: "POST", + body: JSON.stringify(reset_request) + }); + } catch (e) { + console.warn( + "The `/reset` endpoint could not be called. Subsequent endpoint results may be unreliable." + ); + } + } + + const resolve_heartbeat = async (config: Config): Promise => { + await this._resolve_heartbeat(config); + }; + + async function handle_render_config(render_config: any): Promise { + if (!config) return; + let render_id: number = render_config.render_id; + config.components = [ + ...config.components.filter((c) => c.props.rendered_in !== render_id), + ...render_config.components + ]; + config.dependencies = [ + ...config.dependencies.filter((d) => d.rendered_in !== render_id), + ...render_config.dependencies + ]; + const any_state = config.components.some((c) => c.type === "state"); + const any_unload = config.dependencies.some((d) => + d.targets.some((t) => t[1] === "unload") + ); + config.connect_heartbeat = any_state || any_unload; + await resolve_heartbeat(config); + fire_event({ + type: "render", + data: render_config, + endpoint: _endpoint, + fn_index + }); + } + + const job = this.handle_blob( + config.root, + resolved_data, + endpoint_info + ).then(async (_payload) => { + let input_data = handle_payload( + _payload, + dependency, + config.components, + "input", + true + ); + payload = { + data: input_data || [], + event_data, + fn_index, + trigger_id + }; + if (skip_queue(fn_index, config)) { + fire_event({ + type: "status", + endpoint: _endpoint, + stage: "pending", + queue: false, + fn_index, + time: new Date() + }); + + post_data( + `${config.root}${api_prefix}/run${ + _endpoint.startsWith("/") ? _endpoint : `/${_endpoint}` + }${url_params ? "?" + url_params : ""}`, + { + ...payload, + session_hash + }, + addt_headers + ) + .then(async ([output, status_code]: any) => { + const data = output.data; + + if (status_code == 200) { + fire_event({ + type: "data", + endpoint: _endpoint, + fn_index, + data: handle_payload( + data, + dependency, + config.components, + "output", + options.with_null_state + ), + time: new Date(), + event_data, + trigger_id + }); + if (output.render_config) { + await handle_render_config(output.render_config); + } + + fire_event({ + type: "status", + endpoint: _endpoint, + fn_index, + stage: "complete", + eta: output.average_duration, + queue: false, + time: new Date() + }); + } else { + const is_connection_error = + output?.error === BROKEN_CONNECTION_MSG; + fire_event({ + type: "status", + stage: "error", + endpoint: _endpoint, + fn_index, + message: output.error, + broken: is_connection_error, + queue: false, + time: new Date() + }); + } + }) + .catch((e) => { + fire_event({ + type: "status", + stage: "error", + message: e.message, + endpoint: _endpoint, + fn_index, + queue: false, + time: new Date() + }); + }); + } else if (protocol == "sse") { + fire_event({ + type: "status", + stage: "pending", + queue: true, + endpoint: _endpoint, + fn_index, + time: new Date() + }); + var params = new URLSearchParams({ + fn_index: fn_index.toString(), + session_hash: session_hash + }).toString(); + let url = new URL( + `${config.root}${api_prefix}/${SSE_URL}?${ + url_params ? url_params + "&" : "" + }${params}` + ); + + if (this.jwt) { + url.searchParams.set("__sign", this.jwt); + } + + stream = this.stream(url); + + if (!stream) { + return Promise.reject( + new Error("Cannot connect to SSE endpoint: " + url.toString()) + ); + } + + stream.onmessage = async function (event: MessageEvent) { + const _data = JSON.parse(event.data); + const { type, status, data } = handle_message( + _data, + last_status[fn_index] + ); + + if (type === "update" && status && !complete) { + // call 'status' listeners + fire_event({ + type: "status", + endpoint: _endpoint, + fn_index, + time: new Date(), + ...status + }); + if (status.stage === "error") { + stream?.close(); + close(); + } + } else if (type === "data") { + let [_, status] = await post_data( + `${config.root}${api_prefix}/queue/data`, + { + ...payload, + session_hash, + event_id + } + ); + if (status !== 200) { + fire_event({ + type: "status", + stage: "error", + message: BROKEN_CONNECTION_MSG, + queue: true, + endpoint: _endpoint, + fn_index, + time: new Date() + }); + stream?.close(); + close(); + } + } else if (type === "complete") { + complete = status; + } else if (type === "log") { + fire_event({ + type: "log", + title: data.title, + log: data.log, + level: data.level, + endpoint: _endpoint, + duration: data.duration, + visible: data.visible, + fn_index + }); + } else if (type === "generating" || type === "streaming") { + fire_event({ + type: "status", + time: new Date(), + ...status, + stage: status?.stage!, + queue: true, + endpoint: _endpoint, + fn_index + }); + } + if (data) { + fire_event({ + type: "data", + time: new Date(), + data: handle_payload( + data.data, + dependency, + config.components, + "output", + options.with_null_state + ), + endpoint: _endpoint, + fn_index, + event_data, + trigger_id + }); + + if (complete) { + fire_event({ + type: "status", + time: new Date(), + ...complete, + stage: status?.stage!, + queue: true, + endpoint: _endpoint, + fn_index + }); + stream?.close(); + close(); + } + } + }; + } else if ( + protocol == "sse_v1" || + protocol == "sse_v2" || + protocol == "sse_v2.1" || + protocol == "sse_v3" + ) { + // latest API format. v2 introduces sending diffs for intermediate outputs in generative functions, which makes payloads lighter. + // v3 only closes the stream when the backend sends the close stream message. + fire_event({ + type: "status", + stage: "pending", + queue: true, + endpoint: _endpoint, + fn_index, + time: new Date() + }); + let hostname = ""; + if (typeof window !== "undefined" && typeof document !== "undefined") { + hostname = window?.location?.hostname; + } + + const origin = get_zerogpu_origin(hostname); + + const is_zerogpu_iframe = + typeof window !== "undefined" && + typeof document !== "undefined" && + window.parent != window && + !!origin && + window.supports_zerogpu_headers; + const zerogpu_auth_promise = is_zerogpu_iframe + ? post_message>("zerogpu-headers", origin) + : Promise.resolve(null); + const post_data_promise = zerogpu_auth_promise.then((headers) => { + const combined_headers = { ...addt_headers, ...(headers || {}) }; + return post_data( + `${config.root}${api_prefix}/${SSE_DATA_URL}?${url_params}`, + { + ...payload, + session_hash + }, + combined_headers + ); + }); + + return post_data_promise.then(async ([response, status]: any) => { + if (response.event_id) { + event_id_final = response.event_id as string; + } + + if (status === 503) { + fire_event({ + type: "status", + stage: "error", + message: QUEUE_FULL_MSG, + queue: true, + endpoint: _endpoint, + fn_index, + time: new Date(), + visible: true + }); + close(); + } else if (status === 422) { + fire_event({ + type: "status", + stage: "error", + message: response.detail, + queue: true, + endpoint: _endpoint, + fn_index, + code: "validation_error", + time: new Date(), + visible: true + }); + close(); + } else if (status !== 200) { + const is_connection_error = + response?.error === BROKEN_CONNECTION_MSG; + fire_event({ + type: "status", + stage: "error", + broken: is_connection_error, + message: is_connection_error + ? BROKEN_CONNECTION_MSG + : response.detail || response.error, + queue: true, + endpoint: _endpoint, + fn_index, + time: new Date(), + visible: true + }); + close(); + } else { + event_id = response.event_id as string; + event_id_final = event_id; + let callback = async function (_data: object): Promise { + try { + const { type, status, data, original_msg } = handle_message( + _data, + last_status[fn_index] + ); + + if (type == "heartbeat") { + return; + } + + if (type === "update" && status && !complete) { + // call 'status' listeners + fire_event({ + type: "status", + endpoint: _endpoint, + fn_index, + time: new Date(), + original_msg: original_msg, + ...status + }); + } else if (type === "complete") { + complete = status; + } else if ( + type == "unexpected_error" || + type == "broken_connection" + ) { + console.error("Unexpected error", status?.message); + const broken = type === "broken_connection"; + fire_event({ + type: "status", + stage: "error", + message: status?.message || "An Unexpected Error Occurred!", + queue: true, + endpoint: _endpoint, + broken, + session_not_found: status?.session_not_found, + fn_index, + time: new Date() + }); + } else if (type === "log") { + fire_event({ + type: "log", + title: data.title, + log: data.log, + level: data.level, + endpoint: _endpoint, + duration: data.duration, + visible: data.visible, + fn_index + }); + return; + } else if (type === "generating" || type === "streaming") { + fire_event({ + type: "status", + time: new Date(), + ...status, + stage: status?.stage!, + queue: true, + endpoint: _endpoint, + fn_index + }); + if ( + data && + dependency.connection !== "stream" && + ["sse_v2", "sse_v2.1", "sse_v3"].includes(protocol) + ) { + apply_diff_stream(pending_diff_streams, event_id!, data); + } + } + if (data) { + fire_event({ + type: "data", + time: new Date(), + data: handle_payload( + data.data, + dependency, + config.components, + "output", + options.with_null_state + ), + endpoint: _endpoint, + fn_index + }); + if (data.render_config) { + await handle_render_config(data.render_config); + } + + if (complete) { + fire_event({ + type: "status", + time: new Date(), + ...complete, + stage: status?.stage!, + queue: true, + endpoint: _endpoint, + fn_index + }); + close(); + } + } + + if (status?.stage === "complete" || status?.stage === "error") { + if (event_callbacks[event_id!]) { + delete event_callbacks[event_id!]; + } + if (event_id! in pending_diff_streams) { + delete pending_diff_streams[event_id!]; + } + close(); + } + } catch (e) { + console.error("Unexpected client exception", e); + fire_event({ + type: "status", + stage: "error", + message: "An Unexpected Error Occurred!", + queue: true, + endpoint: _endpoint, + fn_index, + time: new Date() + }); + if (["sse_v2", "sse_v2.1", "sse_v3"].includes(protocol)) { + close_stream(stream_status, that.abort_controller); + stream_status.open = false; + close(); + } + } + }; + + if (event_id in pending_stream_messages) { + pending_stream_messages[event_id].forEach((msg) => callback(msg)); + delete pending_stream_messages[event_id]; + } + // @ts-ignore + event_callbacks[event_id] = callback; + unclosed_events.add(event_id); + if (!stream_status.open) { + await this.open_stream(); + } + } + }); + } + }); + + let done = false; + const values: (IteratorResult | PromiseLike)[] = []; + const resolvers: (( + value: IteratorResult | PromiseLike + ) => void)[] = []; + + function close(): void { + done = true; + while (resolvers.length > 0) + (resolvers.shift() as (typeof resolvers)[0])({ + value: undefined, + done: true + }); + } + + function push( + data: { value: GradioEvent; done: boolean } | PromiseLike + ): void { + if (resolvers.length > 0) { + (resolvers.shift() as (typeof resolvers)[0])(data); + } else { + values.push(data); + } + } + + function push_error(error: unknown): void { + push(thenable_reject(error)); + close(); + } + + function push_event(event: GradioEvent): void { + push({ value: event, done: false }); + } + + function next(): Promise> { + if (values.length > 0) { + return Promise.resolve(values.shift() as (typeof values)[0]); + } + if (done) { + return Promise.resolve({ value: undefined, done: true }); + } + return new Promise((resolve) => resolvers.push(resolve)); + } + + const iterator: SubmitIterable = { + [Symbol.asyncIterator]: () => iterator, + next, + throw: async (value: unknown) => { + push_error(value); + return next(); + }, + return: async () => { + close(); + return { value: undefined, done: true as const }; + }, + cancel, + send_chunk: (payload: Record) => { + this.post_data(`${config.root}${api_prefix}/stream/${event_id_final}`, { + ...payload, + session_hash: this.session_hash + }); + }, + close_stream: () => { + this.post_data( + `${config.root}${api_prefix}/stream/${event_id_final}/close`, + {} + ); + + close(); + }, + event_id: () => event_id_final, + wait_for_id: async () => { + await job; + return event_id; + } + }; + + return iterator; + } catch (error) { + console.error("Submit function encountered an error:", error); + throw error; + } +} + +function thenable_reject(error: T): PromiseLike { + return { + then: ( + resolve: (value: never) => PromiseLike, + reject: (error: T) => PromiseLike + ) => reject(error) + }; +} + +function get_endpoint_info( + api_info: ApiInfo, + endpoint: string | number, + api_map: Record, + config: Config +): { + fn_index: number; + endpoint_info: EndpointInfo; + dependency: Dependency; +} { + let fn_index: number; + let endpoint_info: EndpointInfo; + let dependency: Dependency; + + if (typeof endpoint === "number") { + fn_index = endpoint; + endpoint_info = api_info.unnamed_endpoints[fn_index]; + dependency = config.dependencies.find((dep) => dep.id == endpoint)!; + } else { + const trimmed_endpoint = endpoint.replace(/^\//, ""); + + fn_index = api_map[trimmed_endpoint]; + endpoint_info = api_info.named_endpoints[endpoint.trim()]; + dependency = config.dependencies.find( + (dep) => dep.id == api_map[trimmed_endpoint] + )!; + } + + if (typeof fn_index !== "number") { + throw new Error( + "There is no endpoint matching that name of fn_index matching that number." + ); + } + return { fn_index, endpoint_info, dependency }; +} diff --git a/client/js/src/utils/upload_files.ts b/client/js/src/utils/upload_files.ts new file mode 100644 index 0000000..a87ee73 --- /dev/null +++ b/client/js/src/utils/upload_files.ts @@ -0,0 +1,52 @@ +import type { Client } from ".."; +import { BROKEN_CONNECTION_MSG, UPLOAD_URL } from "../constants"; +import type { UploadResponse } from "../types"; + +export async function upload_files( + this: Client, + root_url: string, + files: (Blob | File)[], + upload_id?: string +): Promise { + const headers: { + Authorization?: string; + } = {}; + if (this?.options?.token) { + headers.Authorization = `Bearer ${this.options.token}`; + } + + const chunkSize = 1000; + const uploadResponses = []; + let response: Response; + + for (let i = 0; i < files.length; i += chunkSize) { + const chunk = files.slice(i, i + chunkSize); + const formData = new FormData(); + chunk.forEach((file) => { + formData.append("files", file); + }); + try { + const upload_url = upload_id + ? `${root_url}${this.api_prefix}/${UPLOAD_URL}?upload_id=${upload_id}` + : `${root_url}${this.api_prefix}/${UPLOAD_URL}`; + + response = await this.fetch(upload_url, { + method: "POST", + body: formData, + headers, + credentials: this.options.credentials ?? "same-origin" + }); + } catch (e) { + throw new Error(BROKEN_CONNECTION_MSG + (e as Error).message); + } + if (!response.ok) { + const error_text = await response.text(); + return { error: `HTTP ${response.status}: ${error_text}` }; + } + const output: UploadResponse["files"] = await response.json(); + if (output) { + uploadResponses.push(...output); + } + } + return { files: uploadResponses }; +} diff --git a/client/js/src/utils/view_api.ts b/client/js/src/utils/view_api.ts new file mode 100644 index 0000000..f849e64 --- /dev/null +++ b/client/js/src/utils/view_api.ts @@ -0,0 +1,56 @@ +import type { ApiInfo, ApiData } from "../types"; +import { API_INFO_URL, BROKEN_CONNECTION_MSG } from "../constants"; +import { Client } from "../client"; +import { SPACE_FETCHER_URL } from "../constants"; +import { join_urls, transform_api_info } from "../helpers/api_info"; + +export async function view_api(this: Client): Promise { + if (this.api_info) return this.api_info; + + const { token } = this.options; + const { config } = this; + + const headers: { + Authorization?: string; + } = {}; + + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + if (!config) { + return; + } + + try { + let response: Response; + let api_info: ApiInfo | { api: ApiInfo }; + if (typeof window !== "undefined" && window.gradio_api_info) { + api_info = window.gradio_api_info; + } else { + const url = join_urls(config.root, this.api_prefix, API_INFO_URL); + response = await this.fetch(url, { + headers, + credentials: this.options.credentials ?? "same-origin" + }); + if (!response.ok) { + throw new Error(BROKEN_CONNECTION_MSG); + } + api_info = await response.json(); + } + if ("api" in api_info) { + api_info = api_info.api; + } + + if ( + api_info.named_endpoints["/predict"] && + !api_info.unnamed_endpoints["0"] + ) { + api_info.unnamed_endpoints[0] = api_info.named_endpoints["/predict"]; + } + + return transform_api_info(api_info, config, this.api_map); + } catch (e) { + throw new Error("Could not get API info. " + (e as Error).message); + } +} diff --git a/client/js/src/vite-env.d.ts b/client/js/src/vite-env.d.ts new file mode 100644 index 0000000..d39b312 --- /dev/null +++ b/client/js/src/vite-env.d.ts @@ -0,0 +1,3 @@ +/// + +declare const BROWSER_BUILD: boolean; diff --git a/client/js/tsconfig.json b/client/js/tsconfig.json new file mode 100644 index 0000000..922e0fe --- /dev/null +++ b/client/js/tsconfig.json @@ -0,0 +1,28 @@ +{ + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts", "src/**/*.node-test.ts", "src/test/**/*"], + "compilerOptions": { + "allowJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "declarationMap": true, + "module": "ESNext", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "types": ["node"], + + /* Bundler */ + "moduleResolution": "Bundler", + "skipDefaultLibCheck": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + + /* Linting */ + "strict": true + } +} diff --git a/client/js/vite.config.ts b/client/js/vite.config.ts new file mode 100644 index 0000000..ae676ab --- /dev/null +++ b/client/js/vite.config.ts @@ -0,0 +1,104 @@ +import { defineConfig, createLogger } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { playwright } from "@vitest/browser-playwright"; + +const TEST_MODE = process.env.TEST_MODE || "browser"; + +const logger = createLogger(); +const originalWarning = logger.warn; + +logger.warn = (log, options) => { + if (log.includes("was created with unknown prop")) return false; + if (log.includes("https://svelte.dev")) return false; + if (log.includes("[vite-plugin-svelte]")) return false; + if (log && log.includes("[MSW]")) return false; + if (log && log.includes("node:")) return false; + originalWarning(log, options); +}; + +const originalError = logger.error; + +logger.error = (log, options) => { + if (log.includes("was created with unknown prop")) return false; + if (log.includes("https://svelte.dev")) return false; + if (log.includes("[vite-plugin-svelte]")) return false; + if (log && log.includes("[MSW]")) return false; + if (log && log.includes("(node:")) return false; + originalError(log, options); +}; + +const original_info = logger.info; +logger.info = (log, options) => { + if (log.includes("was created with unknown prop")) return false; + if (log.includes("https://svelte.dev")) return false; + if (log.includes("[vite-plugin-svelte]")) return false; + if (log && log.includes("[MSW]")) return false; + if (log && log.includes("node:")) return false; + original_info(log, options); +}; +export default defineConfig(({ mode }) => { + const production = mode === "production"; + const isBrowserBuild = process.env.BROWSER_BUILD === "true"; + + if (mode === "preview") { + return { + entry: "index.html" + }; + } + + return { + customLogger: logger, + build: { + emptyOutDir: false, + lib: { + entry: "src/index.ts", + formats: ["es"], + fileName: isBrowserBuild ? `browser` : `index` + }, + rollupOptions: { + input: "src/index.ts", + output: { + dir: "dist" + } + } + }, + plugins: [svelte()], + define: { + BROWSER_BUILD: JSON.stringify(isBrowserBuild) + }, + mode: process.env.MODE || "development", + test: { + include: ["./src/test/*.test.*"], + onConsoleLog(log) { + if (log.includes("[MSW]")) return false; + if (log.includes("node:")) return false; + if (log.includes("data: '\"404\"'")) return false; + if (log.includes("Too many arguments")) return false; + }, + ...(TEST_MODE === "node" + ? { environment: "node" } + : { + browser: { + enabled: true, + provider: playwright(), + instances: [ + { + browser: "chromium" + } + ] + } + }) + }, + ssr: { + target: "node", + format: "esm", + noExternal: [ + "ws", + "semiver", + "bufferutil", + "@gradio/upload", + "fetch-event-stream" + ] + } + }; +}); diff --git a/client/python/CHANGELOG.md b/client/python/CHANGELOG.md new file mode 100644 index 0000000..a03966b --- /dev/null +++ b/client/python/CHANGELOG.md @@ -0,0 +1,1256 @@ +# gradio_client + +## 2.5.0 + +### Features + +- [#13289](https://github.com/gradio-app/gradio/pull/13289) [`d6f24df`](https://github.com/gradio-app/gradio/commit/d6f24df6233e7882746ba6e49307a34a11101ea1) - Improve curl info. Thanks @freddyaboulton! + +### Fixes + +- [#13280](https://github.com/gradio-app/gradio/pull/13280) [`bb9c130`](https://github.com/gradio-app/gradio/commit/bb9c130e1e8c60b8a717da7184b02ab459be3f03) - Fix snippet generator crash on datetime values in Dataframe inputs. Thanks @ParamChordiya! + +## 2.4.1 + +### Features + +- [#13246](https://github.com/gradio-app/gradio/pull/13246) [`ff90963`](https://github.com/gradio-app/gradio/commit/ff909638e72f0b44e8629e5a28cea8276cdd9ab2) - Add Documentation Group for gr.Cache. Thanks @freddyaboulton! + +### Fixes + +- [#13204](https://github.com/gradio-app/gradio/pull/13204) [`9953db9`](https://github.com/gradio-app/gradio/commit/9953db94e406477df96f98adf3e47246181ceef9) - fix: preserve special characters in uploaded filenames. Thanks @xr843! + +## 2.4.0 + +### Features + +- [#13045](https://github.com/gradio-app/gradio/pull/13045) [`a35f589`](https://github.com/gradio-app/gradio/commit/a35f5896e43d2585d9206e8256b4d7e321fcd0fe) - Gradio Prediction CLI Commands. Thanks @freddyaboulton! + +### Fixes + +- [#12979](https://github.com/gradio-app/gradio/pull/12979) [`4a4c7f3`](https://github.com/gradio-app/gradio/commit/4a4c7f3b0d6fd8009fdafc580d5852984f961db1) - preserve file extension when filename stem is stripped entirely in gr.File. Thanks @giulio-leone! + +## 2.3.0 + +### Features + +- [#12879](https://github.com/gradio-app/gradio/pull/12879) [`c498688`](https://github.com/gradio-app/gradio/commit/c4986883b267570d76b442899c6fc09d14e3e222) - Ensure svelte version mismatches do not break custom components. Thanks @pngwn! + +### Fixes + +- [#12942](https://github.com/gradio-app/gradio/pull/12942) [`e5ba4fa`](https://github.com/gradio-app/gradio/commit/e5ba4fa992c0ac389c6af2d143c9ad4c33eea360) - perf: use deque for SSE pending message queues in gradio_client. Thanks @giulio-leone! + +## 2.2.0 + +### Features + +- [#12918](https://github.com/gradio-app/gradio/pull/12918) [`e29e1cc`](https://github.com/gradio-app/gradio/commit/e29e1ccd5874cb98b813ed4f7f72d9fef2935016) - Add Space-specific skill generation to `gradio skills add`. Thanks @abidlabs! + +## 2.1.0 + +### Features + +- [#12700](https://github.com/gradio-app/gradio/pull/12700) [`b01c95a`](https://github.com/gradio-app/gradio/commit/b01c95a58be8e18bb4ddef7f2ee238a7774e5be9) - Rewrite behavior section of docs. Thanks @aliabd! + +### Fixes + +- [#12882](https://github.com/gradio-app/gradio/pull/12882) [`fc7c01e`](https://github.com/gradio-app/gradio/commit/fc7c01ea1e581ef70be98fddf003b0c91315c7cc) - Validate proxy url host. Thanks @freddyaboulton! +- [#12811](https://github.com/gradio-app/gradio/pull/12811) [`8f8cef8`](https://github.com/gradio-app/gradio/commit/8f8cef87bfb3af64867804ad45f4385af09e07b4) - Fix windows tests. Thanks @freddyaboulton! + +## 2.0.3 + +### Fixes + +- [#12614](https://github.com/gradio-app/gradio/pull/12614) [`6222192`](https://github.com/gradio-app/gradio/commit/622219242412400df57a73aebfd3de96bb8499de) - fix(client): make WebP and VTT MIME type detection case-insensitive. Thanks @majiayu000! + +## 2.0.2 + +### Fixes + +- [#12585](https://github.com/gradio-app/gradio/pull/12585) [`1724a02`](https://github.com/gradio-app/gradio/commit/1724a02f37118af098cd9f51472955bf8ef8d794) - Remove checkmark from windows. Thanks @freddyaboulton! +- [#12600](https://github.com/gradio-app/gradio/pull/12600) [`1fafaba`](https://github.com/gradio-app/gradio/commit/1fafabaace315b1c699855cadb69eb17488de957) - Add x-gradio-user-header. Thanks @freddyaboulton! + +## 2.0.1 + +### Fixes + +- [#12480](https://github.com/gradio-app/gradio/pull/12480) [`b9732d1`](https://github.com/gradio-app/gradio/commit/b9732d10680ca66fa7b3f1e763d3cdd57b38c6ed) - [BUGFIX] Fix stream file download in gradio client. Thanks @frascuchon! +- [#12490](https://github.com/gradio-app/gradio/pull/12490) [`472e164`](https://github.com/gradio-app/gradio/commit/472e16439f2573b33d21b9635706416518405ddd) - Make client backwards compatible with version 5. Thanks @freddyaboulton! + +## 2.0.0-dev.3 + +### Features + +- [#12377](https://github.com/gradio-app/gradio/pull/12377) [`568644a`](https://github.com/gradio-app/gradio/commit/568644a88c4dbe1b6ad9468906b1e45ed07657f3) - Add a 6.0 migration guide and add deprecation warnings. Thanks @abidlabs! + +## 2.0.0-dev.2 + +### Features + +- [#12243](https://github.com/gradio-app/gradio/pull/12243) [`cb49168`](https://github.com/gradio-app/gradio/commit/cb49168b1d52b39e47f1faeda93d150e048bd625) - [Client] Fix ZeroGPU headers forwarding. Thanks @cbensimon! +- [#12217](https://github.com/gradio-app/gradio/pull/12217) [`681fa11`](https://github.com/gradio-app/gradio/commit/681fa11357de520f0b05605cbd300d9e276d8736) - Update ZeroGPU guide to reflect best practices on manually passing an IP token. Thanks @dawoodkhan82! + +## 2.0.0-dev.1 + +### Features + +- [#12069](https://github.com/gradio-app/gradio/pull/12069) [`9de88ca`](https://github.com/gradio-app/gradio/commit/9de88ca470ce529366d259f0deaa955f658000b9) - Rename show_api. Thanks @freddyaboulton! + +## 2.0.0-dev.0 + +### Features + +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`53d9098`](https://github.com/gradio-app/gradio/commit/53d9098cca378f6ebff9dec15d2faa8a3d2fb510) - [No Merge] Gradio 6.0. Thanks @freddyaboulton! + +## 1.13.3 + +### Fixes + +- [#11979](https://github.com/gradio-app/gradio/pull/11979) [`50a89f0`](https://github.com/gradio-app/gradio/commit/50a89f079250735f268dfeeeddf40ffea84d7919) - Allow gradio to run with huggingface_hub 1.x. Thanks @Wauplin! + +## 1.13.2 + +### Features + +- [#11962](https://github.com/gradio-app/gradio/pull/11962) [`fdfe4fe`](https://github.com/gradio-app/gradio/commit/fdfe4fec334726fe91be98062826eb4b537410c3) - fix(client): websockets minimum version for asyncio. Thanks @cbensimon! + +## 1.13.1 + +### Fixes + +- [#11896](https://github.com/gradio-app/gradio/pull/11896) [`915f3a2`](https://github.com/gradio-app/gradio/commit/915f3a2f2fa1843111868e4f23f543b097b9c839) - Send EventData when app is loaded via gr.load. Thanks @freddyaboulton! +- [#11924](https://github.com/gradio-app/gradio/pull/11924) [`a9167fb`](https://github.com/gradio-app/gradio/commit/a9167fb7d013b7f76784c98f9c673ce47214b119) - Update websocket import and type in get_pred_from_ws. Thanks @qgallouedec! + +## 1.13.0 + +### Features + +- [#11814](https://github.com/gradio-app/gradio/pull/11814) [`013784a`](https://github.com/gradio-app/gradio/commit/013784a7086047651e8e661a38bde7d5c7f10db7) - add validation support. Thanks @pngwn! + +## 1.12.1 + +### Features + +- [#11744](https://github.com/gradio-app/gradio/pull/11744) [`f54e454`](https://github.com/gradio-app/gradio/commit/f54e454d6c2e256873f49e8bfb89f7161ec0a040) - fix client. Thanks @abidlabs! + +## 1.12.0 + +### Features + +- [#11723](https://github.com/gradio-app/gradio/pull/11723) [`379f0c1`](https://github.com/gradio-app/gradio/commit/379f0c151943b5f269910eba4a4c7abc6145a11c) - Support MCP resources and prompts. Thanks @abidlabs! + +## 1.11.1 + +### Features + +- [#11700](https://github.com/gradio-app/gradio/pull/11700) [`37d4c48`](https://github.com/gradio-app/gradio/commit/37d4c4809616595642f8d4a60be37d9915317443) - Have `Client` send authorization token with the `X-HF-Authorization` token. Thanks @abidlabs! + +## 1.11.0 + +### Features + +- [#11567](https://github.com/gradio-app/gradio/pull/11567) [`150ed18`](https://github.com/gradio-app/gradio/commit/150ed18b856e34d5a96a9e17bd5ad510e11872a6) - Stream Progress Updates to MCP clients. Thanks @freddyaboulton! + +## 1.10.4 + +### Fixes + +- [#11041](https://github.com/gradio-app/gradio/pull/11041) [`474aa30`](https://github.com/gradio-app/gradio/commit/474aa30979c14793e00852ad2f4ef6c12a41b900) - Attach cookies to client after first request (to support multiple-replica setups with cookie-based affinity). Thanks @abidlabs! + +## 1.10.3 + +### Fixes + +- [#11347](https://github.com/gradio-app/gradio/pull/11347) [`fdce3a0`](https://github.com/gradio-app/gradio/commit/fdce3a094fe1278ae83fe2f8b134b4c268506cfe) - Fix `gr.api()` to support more types, including optional params. Thanks @abidlabs! + +## 1.10.2 + +### Fixes + +- [#11215](https://github.com/gradio-app/gradio/pull/11215) [`2186ae3`](https://github.com/gradio-app/gradio/commit/2186ae3f4d6f8ac83883c359aed88a5b72746b5d) - Allow httpx_kwargs to contain cookies. Thanks @santibreo! + +## 1.10.1 + +### Features + +- [#11185](https://github.com/gradio-app/gradio/pull/11185) [`e64b83b`](https://github.com/gradio-app/gradio/commit/e64b83bf42a813d9269e519189523f8390a72ec4) - Evaluate index variable in argument description. Thanks @emmanuel-ferdman! + +### Fixes + +- [#11172](https://github.com/gradio-app/gradio/pull/11172) [`b618571`](https://github.com/gradio-app/gradio/commit/b618571ed17a8cece4bf9f1f55ed2e8bf7b59c46) - Fix python client SSE decoding issue. Thanks @freddyaboulton! + +## 1.10.0 + +### Features + +- [#10984](https://github.com/gradio-app/gradio/pull/10984) [`8dab577`](https://github.com/gradio-app/gradio/commit/8dab5771c7d952c76f325681dbf364119c91b0b1) - Let Gradio apps also be MCP Servers. Thanks @abidlabs! + +## 1.9.1 + +### Fixes + +- [#11093](https://github.com/gradio-app/gradio/pull/11093) [`cb322df`](https://github.com/gradio-app/gradio/commit/cb322df1f4df858590e760a82f6410f3b6db899b) - Update client.py to always send file data, even for files without extensions. Thanks @edmcman! + +## 1.9.0 + +### Features + +- [#11043](https://github.com/gradio-app/gradio/pull/11043) [`62a0080`](https://github.com/gradio-app/gradio/commit/62a00806e198c4c76bc132255d4b78c7aa329157) - Pass any visible error modals from a Gradio app downstream to the app that has `gr.load`-ed it. Thanks @abidlabs! + +## 1.8.0 + +### Features + +- [#10809](https://github.com/gradio-app/gradio/pull/10809) [`99b69df`](https://github.com/gradio-app/gradio/commit/99b69df1c000da092373440800fc08abe6ae9e20) - Trigger python client release. Thanks @freddyaboulton! + +## 1.7.2 + +### Features + +- [#10664](https://github.com/gradio-app/gradio/pull/10664) [`0b1f729`](https://github.com/gradio-app/gradio/commit/0b1f72941fd50298562102e39f4feafaa16f5968) - Allow websocket version 15. Thanks @freddyaboulton! +- Test + +## 1.7.1 + +### Fixes + +- [#10580](https://github.com/gradio-app/gradio/pull/10580) [`4e70d74`](https://github.com/gradio-app/gradio/commit/4e70d74068b77ebb3d285aa78e9202fff76337a2) - Fix `gr.load()` for `gr.ChatInterface(save_history=True)` and any Gradio app where the upstream app includes a `gr.State` as input. Thanks @abidlabs! + +## 1.7.0 + +### Features + +- [#10470](https://github.com/gradio-app/gradio/pull/10470) [`3465fdb`](https://github.com/gradio-app/gradio/commit/3465fdb19087471598ca07c93bc4ff3e1b6b2abf) - Format backend with latest `ruff`. Thanks @abidlabs! +- [#10435](https://github.com/gradio-app/gradio/pull/10435) [`ef66fe5`](https://github.com/gradio-app/gradio/commit/ef66fe52b22448a5125a314581f2ec6c73c24145) - Sidebar Component. Thanks @dawoodkhan82! + +## 1.6.0 + +### Features + +- [#10352](https://github.com/gradio-app/gradio/pull/10352) [`6a7cfc4`](https://github.com/gradio-app/gradio/commit/6a7cfc4264822209148ad07d8f38a0550bdb32b7) - Compatibility between Client and ZeroGPU. Thanks @abidlabs! + +## 1.5.4 + +### Fixes + +- [#10332](https://github.com/gradio-app/gradio/pull/10332) [`e742dcc`](https://github.com/gradio-app/gradio/commit/e742dcccb376692c9ddd5a6c251080e7c5936574) - Allow users to add a custom API route. Thanks @aliabid94! + +## 1.5.3 + +### Features + +- [#10221](https://github.com/gradio-app/gradio/pull/10221) [`506bd28`](https://github.com/gradio-app/gradio/commit/506bd2884a9790fb6f8dbf5684576e80d2b8ee64) - Update Guides related to deploying Gradio chatbots to Discord, Slack, and website widgets. Thanks @abidlabs! + +### Fixes + +- [#10238](https://github.com/gradio-app/gradio/pull/10238) [`3f19210`](https://github.com/gradio-app/gradio/commit/3f192100d6997751d0246b396a4fd8eaa86a826b) - Declare exports in __all__ for type checking. Thanks @dustalov! + +## 1.5.2 + +### Features + +- [#10196](https://github.com/gradio-app/gradio/pull/10196) [`c9ba9a4`](https://github.com/gradio-app/gradio/commit/c9ba9a447596a9ccdd21955adb3b34b15cac7ade) - Use the modern lower-case Python types in the API typing information. Thanks @abidlabs! +- [#10193](https://github.com/gradio-app/gradio/pull/10193) [`424365b`](https://github.com/gradio-app/gradio/commit/424365bdbd0b805e3b2d0c44ccc0f47201b1d96a) - JSON type fix in Client and and typing fix for `/chat` endpoint in `gr.ChatInterface`. Thanks @abidlabs! + +## 1.5.1 + +### Fixes + +- [#10090](https://github.com/gradio-app/gradio/pull/10090) [`5ea3cb5`](https://github.com/gradio-app/gradio/commit/5ea3cb51a39ba01fda7f65ff31e59955e1d12cea) - Update `requirements.txt` for `gradio` and `gradio_client`. Thanks @abidlabs! + +## 1.5.0 + +### Features + +- [#10017](https://github.com/gradio-app/gradio/pull/10017) [`a95fda1`](https://github.com/gradio-app/gradio/commit/a95fda1f85e80ce8423f4373bb238422b9b7aa32) - fix small bug when join src & api_prefix. Thanks @Chandler-Bing! + +## 1.4.3 + +### Fixes + +- [#9913](https://github.com/gradio-app/gradio/pull/9913) [`d81f430`](https://github.com/gradio-app/gradio/commit/d81f430fd50546001b76c0ae5fded32c6d3093f7) - fix: Fix filename stripping to preserve extensions. Thanks @TakaSoap! + +## 1.4.2 + +### Fixes + +- [#9754](https://github.com/gradio-app/gradio/pull/9754) [`36a5076`](https://github.com/gradio-app/gradio/commit/36a50769095081a0e77f04f513d47a2e9d4531ba) - Update client.py: raise error on 429 get_config. Thanks @Pendrokar! + +## 1.4.1 + +### Fixes + +- [#9678](https://github.com/gradio-app/gradio/pull/9678) [`a25a26e`](https://github.com/gradio-app/gradio/commit/a25a26e208c3f3675ba857a889553c7ccc95e866) - Fix: `file_types` checking bug. Thanks @jasongzy! + +## 1.4.0-beta.5 + +### Features + +- [#9589](https://github.com/gradio-app/gradio/pull/9589) [`477f45c`](https://github.com/gradio-app/gradio/commit/477f45cb43be957684eb392e3d62c09490c22391) - Only move files to the cache that have a meta key. Thanks @freddyaboulton! + +## 1.4.0-beta.4 + +### Features + +- [#9550](https://github.com/gradio-app/gradio/pull/9550) [`b0fedd7`](https://github.com/gradio-app/gradio/commit/b0fedd7ef718c0df797ec277db7e773543a70a4d) - Fix most flaky Python tests in `5.0-dev` branch. Thanks @abidlabs! +- [#9483](https://github.com/gradio-app/gradio/pull/9483) [`8dc7c12`](https://github.com/gradio-app/gradio/commit/8dc7c12389311b60efcde1b9d3e3668a34d2dc00) - Send Streaming data over Websocket if possible. Also support base64 output format for images. Thanks @freddyaboulton! +- [#9522](https://github.com/gradio-app/gradio/pull/9522) [`3b71ed2`](https://github.com/gradio-app/gradio/commit/3b71ed21b7e2ecb67eb68fb946d25565169cb4df) - Api info fix. Thanks @freddyaboulton! + +## 1.4.0-beta.3 + +### Fixes + +- [#9431](https://github.com/gradio-app/gradio/pull/9431) [`7065e11`](https://github.com/gradio-app/gradio/commit/7065e11e465fcdfe14688bd6ca2aeed0a25fcc36) - Check for `file_types` parameter in the backend. Thanks @dawoodkhan82! + +## 1.4.0-beta.2 + +### Features + +- [#9339](https://github.com/gradio-app/gradio/pull/9339) [`4c8c6f2`](https://github.com/gradio-app/gradio/commit/4c8c6f2fe603081941c5fdc43f48a0632b9f31ad) - Ssr part 2. Thanks @pngwn! + +## 1.4.0-beta.1 + +### Features + +- [#9200](https://github.com/gradio-app/gradio/pull/9200) [`2e179d3`](https://github.com/gradio-app/gradio/commit/2e179d35be6ed60a5a6bfc7303178d63e41781ad) - prefix api routes. Thanks @pngwn! + +## 1.4.0-beta.0 + +### Features + +- [#9140](https://github.com/gradio-app/gradio/pull/9140) [`c054ec8`](https://github.com/gradio-app/gradio/commit/c054ec85e49ab102b15afd305583ee394151d16c) - Drop python 3.8 and 3.9. Thanks @abidlabs! +- [#8941](https://github.com/gradio-app/gradio/pull/8941) [`97a7bf6`](https://github.com/gradio-app/gradio/commit/97a7bf66a79179d1b91a3199d68e5c11216ca500) - Streaming inputs for 5.0. Thanks @freddyaboulton! + +## 1.3.0 + +### Features + +- [#8968](https://github.com/gradio-app/gradio/pull/8968) [`38b3682`](https://github.com/gradio-app/gradio/commit/38b3682c3a9b40fb4070f665d94711c43c6fe40e) - Improvements to FRP client download and usage. Thanks @abidlabs! +- [#9059](https://github.com/gradio-app/gradio/pull/9059) [`981731a`](https://github.com/gradio-app/gradio/commit/981731acb7da3e78555abeb20f47f3a8a5f0d861) - Fix flaky tests and tests on Windows. Thanks @abidlabs! + +## 1.2.0 + +### Features + +- [#8862](https://github.com/gradio-app/gradio/pull/8862) [`ac132e3`](https://github.com/gradio-app/gradio/commit/ac132e3cbc8dbc7bec3d607d52bef347e90feb41) - Support the use of custom authentication mechanism, timeouts, and other `httpx` parameters in Python Client. Thanks @valgai! +- [#8948](https://github.com/gradio-app/gradio/pull/8948) [`f7fbd2c`](https://github.com/gradio-app/gradio/commit/f7fbd2c23795d97071296463779d41bd0e937164) - Bump websockets version max for gradio-client. Thanks @evanscho! + +## 1.1.1 + +### Features + +- [#8757](https://github.com/gradio-app/gradio/pull/8757) [`6073736`](https://github.com/gradio-app/gradio/commit/60737366517f48d1a37ffce15425783a2887f305) - Document `FileData` class in docs. Thanks @hannahblair! + +## 1.1.0 + +### Fixes + +- [#8505](https://github.com/gradio-app/gradio/pull/8505) [`2943d6d`](https://github.com/gradio-app/gradio/commit/2943d6d68847314885dc6c5c0247083116017ca0) - Add Timer component. Thanks @aliabid94! + +## 1.0.2 + +### Features + +- [#8516](https://github.com/gradio-app/gradio/pull/8516) [`de6aa2b`](https://github.com/gradio-app/gradio/commit/de6aa2b67668605b65ad92842b2c798afa2c6d8a) - Add helper classes to docs. Thanks @aliabd! + +## 1.0.1 + +### Features + +- [#8481](https://github.com/gradio-app/gradio/pull/8481) [`41a4493`](https://github.com/gradio-app/gradio/commit/41a449383a34b7d6e4c83cfbf61c222fd5501206) - fix client flaky tests. Thanks @abidlabs! + +## 1.0.0 + +### Highlights + +#### Clients 1.0 Launch! ([#8468](https://github.com/gradio-app/gradio/pull/8468) [`7cc0a0c`](https://github.com/gradio-app/gradio/commit/7cc0a0c1abea585c3f50ffb1ff78d2b08ddbdd92)) + +We're excited to unveil the first major release of the Gradio clients. +We've made it even easier to turn any Gradio application into a production endpoint thanks to the clients' **ergonomic**, **transparent**, and **portable** design. + +#### Ergonomic API 💆 + +**Stream From a Gradio app in 5 lines** + +Use the `submit` method to get a job you can iterate over: + +```python +from gradio_client import Client + +client = Client("gradio/llm_stream") + +for result in client.submit("What's the best UI framework in Python?"): + print(result) +``` + +```ts +import { Client } from "@gradio/client"; + +const client = await Client.connect("gradio/llm_stream") +const job = client.submit("/predict", {"text": "What's the best UI framework in Python?"}) + +for await (const msg of job) console.log(msg.data) +``` + +**Use the same keyword arguments as the app** + + +```python +from gradio_client import Client + +client = Client("http://127.0.0.1:7860/") +result = client.predict( + message="Hello!!", + system_prompt="You are helpful AI.", + tokens=10, + api_name="/chat" +) +print(result) +``` + +```ts +import { Client } from "@gradio/client"; + +const client = await Client.connect("http://127.0.0.1:7860/"); +const result = await client.predict("/chat", { + message: "Hello!!", + system_prompt: "Hello!!", + tokens: 10, +}); + +console.log(result.data); +``` + +**Better Error Messages** + +If something goes wrong in the upstream app, the client will raise the same exception as the app provided that `show_error=True` in the original app's `launch()` function, or it's a `gr.Error` exception. + +#### Transparent Design 🪟 + +Anything you can do in the UI, you can do with the client: +* 🔒 Authentication +* 🛑 Job Cancelling +* ℹ️ Access Queue Position and API +* 📕 View the API information + +Here's an example showing how to display the queue position of a pending job: + +```python +from gradio_client import Client + +client = Client("gradio/diffusion_model") + +job = client.submit("A cute cat") +while not job.done(): + status = job.status() + print(f"Current in position {status.rank} out of {status.queue_size}") +``` + +#### Portable Design ⛺️ + +The client can run from pretty much any python and javascript environment (node, deno, the browser, Service Workers). + +Here's an example using the client from a Flask server using gevent: + +```python +from gevent import monkey +monkey.patch_all() + +from gradio_client import Client +from flask import Flask, send_file +import time + +app = Flask(__name__) + +imageclient = Client("gradio/diffusion_model") + +@app.route("/gen") +def gen(): + result = imageclient.predict( + "A cute cat", + api_name="/predict" + ) + return send_file(result) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) +``` + +#### 1.0 Migration Guide and Breaking Changes + +**Python** +- The `serialize` argument of the `Client` class was removed. Has no effect. +- The `upload_files` argument of the `Client` was removed. +- All filepaths must be wrapped in the `handle_file` method. Example: +```python +from gradio_client import Client, handle_file + +client = Client("gradio/image_captioner") +client.predict(handle_file("cute_cat.jpg")) +``` +- The `output_dir` argument was removed. It is not specified in the `download_files` argument. + + +**Javascript** +The client has been redesigned entirely. It was refactored from a function into a class. An instance can now be constructed by awaiting the `connect` method. + +```js +const app = await Client.connect("gradio/whisper") +``` +The app variable has the same methods as the python class (`submit`, `predict`, `view_api`, `duplicate`). + + + +#### Additional Changes + +- [#8243](https://github.com/gradio-app/gradio/pull/8243) - Set orig_name in python client file uploads. +- [#8264](https://github.com/gradio-app/gradio/pull/8264) - Make exceptions in the Client more specific. +- [#8247](https://github.com/gradio-app/gradio/pull/8247) - Fix api recorder. +- [#8276](https://github.com/gradio-app/gradio/pull/8276) - Fix bug where client could not connect to apps that had self signed certificates. +- [#8245](https://github.com/gradio-app/gradio/pull/8245) - Cancel server progress from the python client. +- [#8200](https://github.com/gradio-app/gradio/pull/8200) - Support custom components in gr.load +- [#8182](https://github.com/gradio-app/gradio/pull/8182) - Convert sse calls in client from async to sync. +- [#7732](https://github.com/gradio-app/gradio/pull/7732) - Adds support for kwargs and default arguments in the python client, and improves how parameter information is displayed in the "view API" page. +- [#7888](https://github.com/gradio-app/gradio/pull/7888) - Cache view_api info in server and python client. +- [#7575](https://github.com/gradio-app/gradio/pull/7575) - Files should now be supplied as `file(...)` in the Client, and some fixes to `gr.load()` as well. +- [#8401](https://github.com/gradio-app/gradio/pull/8401) - Add CDN installation to JS docs. +- [#8299](https://github.com/gradio-app/gradio/pull/8299) - Allow JS Client to work with authenticated spaces 🍪. +- [#8408](https://github.com/gradio-app/gradio/pull/8408) - Connect heartbeat if state created in render. Also fix config cleanup bug #8407. +- [#8258](https://github.com/gradio-app/gradio/pull/8258) - Improve URL handling in JS Client. +- [#8322](https://github.com/gradio-app/gradio/pull/8322) - ensure the client correctly handles all binary data. +- [#8296](https://github.com/gradio-app/gradio/pull/8296) - always create a jwt when connecting to a space if a hf_token is present. +- [#8285](https://github.com/gradio-app/gradio/pull/8285) - use the correct query param to pass the jwt to the heartbeat event. +- [#8272](https://github.com/gradio-app/gradio/pull/8272) - ensure client works for private spaces. +- [#8197](https://github.com/gradio-app/gradio/pull/8197) - Add support for passing keyword args to `data` in JS client. +- [#8252](https://github.com/gradio-app/gradio/pull/8252) - Client node fix. +- [#8209](https://github.com/gradio-app/gradio/pull/8209) - Rename `eventSource_Factory` and `fetch_implementation`. +- [#8109](https://github.com/gradio-app/gradio/pull/8109) - Implement JS Client tests. +- [#8211](https://github.com/gradio-app/gradio/pull/8211) - remove redundant event source logic. +- [#8179](https://github.com/gradio-app/gradio/pull/8179) - rework upload to be a class method + pass client into each component. +- [#8181](https://github.com/gradio-app/gradio/pull/8181) - Ensure connectivity to private HF spaces with SSE protocol. +- [#8169](https://github.com/gradio-app/gradio/pull/8169) - Only connect to heartbeat if needed. +- [#8118](https://github.com/gradio-app/gradio/pull/8118) - Add eventsource polyfill for Node.js and browser environments. +- [#7646](https://github.com/gradio-app/gradio/pull/7646) - Refactor JS Client. +- [#7974](https://github.com/gradio-app/gradio/pull/7974) - Fix heartbeat in the js client to be Lite compatible. +- [#7926](https://github.com/gradio-app/gradio/pull/7926) - Fixes streaming event race condition. + + Thanks @freddyaboulton! + +### Features + +- [#8444](https://github.com/gradio-app/gradio/pull/8444) [`2cd02ff`](https://github.com/gradio-app/gradio/commit/2cd02ff3b7c57cd69635d111ff25643eba30b9b0) - Remove deprecated parameters from Python Client. Thanks @abidlabs! + +## 0.17.0 + +### Features + +- [#8243](https://github.com/gradio-app/gradio/pull/8243) [`55f664f`](https://github.com/gradio-app/gradio/commit/55f664f2979a49acc29a73cde16c6ebdfcc91db2) - Add event listener support to render blocks. Thanks @aliabid94! +- [#8409](https://github.com/gradio-app/gradio/pull/8409) [`8028c33`](https://github.com/gradio-app/gradio/commit/8028c33bbc5a324a5e9e8b28906443db28683d79) - Render decorator documentation. Thanks @aliabid94! + +### Fixes + +- [#8371](https://github.com/gradio-app/gradio/pull/8371) [`a373b0e`](https://github.com/gradio-app/gradio/commit/a373b0edd36613a9a6a25a1a2893edd6533a7291) - Set orig_name in python client file uploads. Thanks @freddyaboulton! + +## 0.16.4 + +### Fixes + +- [#8247](https://github.com/gradio-app/gradio/pull/8247) [`8f46556`](https://github.com/gradio-app/gradio/commit/8f46556b38e35cffbadac74ff80445dceea3bcf5) - Fix api recorder. Thanks @abidlabs! + +## 0.16.3 + +### Features + +- [#8264](https://github.com/gradio-app/gradio/pull/8264) [`a9e1a8a`](https://github.com/gradio-app/gradio/commit/a9e1a8ac5633c5336fea1c63d7f66a9883e7e6e1) - Make exceptions in the Client more specific. Thanks @abidlabs! + +### Fixes + +- [#8276](https://github.com/gradio-app/gradio/pull/8276) [`0bf3d1a`](https://github.com/gradio-app/gradio/commit/0bf3d1a992db2753c1a55452b569027190f26ef6) - Fix bug where client could not connect to apps that had self signed certificates. Thanks @freddyaboulton! + +## 0.16.2 + +### Fixes + +- [#8245](https://github.com/gradio-app/gradio/pull/8245) [`c562a3d`](https://github.com/gradio-app/gradio/commit/c562a3d9a440c8f94ca070bd07b8d4121d6ab7b3) - Cancel server progress from the python client. Thanks @freddyaboulton! + +## 0.16.1 + +### Highlights + +#### Support custom components in gr.load ([#8200](https://github.com/gradio-app/gradio/pull/8200) [`72039be`](https://github.com/gradio-app/gradio/commit/72039be93acda856d92ceac7f21f1ec1a054fae2)) + +It is now possible to load a demo with a custom component with `gr.load`. + +The custom component must be installed in your system and imported in your python session. + +```python +import gradio as gr +import gradio_pdf + +demo = gr.load("freddyaboulton/gradiopdf", src="spaces") + +if __name__ == "__main__": + demo.launch() +``` + +image + + Thanks @freddyaboulton! + +### Fixes + +- [#8182](https://github.com/gradio-app/gradio/pull/8182) [`39791eb`](https://github.com/gradio-app/gradio/commit/39791eb186d3a4ce82c8c27979a28311c37a4067) - Convert sse calls in client from async to sync. Thanks @abidlabs! + +## 0.16.0 + +### Highlights + +#### Setting File Upload Limits ([#7909](https://github.com/gradio-app/gradio/pull/7909) [`2afca65`](https://github.com/gradio-app/gradio/commit/2afca6541912b37dc84f447c7ad4af21607d7c72)) + +We have added a `max_file_size` size parameter to `launch()` that limits to size of files uploaded to the server. This limit applies to each individual file. This parameter can be specified as a string or an integer (corresponding to the size in bytes). + +The following code snippet sets a max file size of 5 megabytes. + +```python +import gradio as gr + +demo = gr.Interface(lambda x: x, "image", "image") + +demo.launch(max_file_size="5mb") +# or +demo.launch(max_file_size=5 * gr.FileSize.MB) +``` + +![max_file_size_upload](https://github.com/gradio-app/gradio/assets/41651716/7547330c-a082-4901-a291-3f150a197e45) + + +#### Error states can now be cleared + +When a component encounters an error, the error state shown in the UI can now be cleared by clicking on the `x` icon in the top right of the component. This applies to all types of errors, whether it's raised in the UI or the server. + +![error_modal_calculator](https://github.com/gradio-app/gradio/assets/41651716/16cb071c-accd-45a6-9c18-0dea27d4bd98) + + Thanks @freddyaboulton! + +### Features + +- [#8100](https://github.com/gradio-app/gradio/pull/8100) [`cbdfbdf`](https://github.com/gradio-app/gradio/commit/cbdfbdfc973fa67665911fb5d8cb005a025b0e58) - upgrade `ruff` test dependency to `ruff==0.4.1`. Thanks @abidlabs! + +## 0.15.1 + +### Features + +- [#7850](https://github.com/gradio-app/gradio/pull/7850) [`2bae1cf`](https://github.com/gradio-app/gradio/commit/2bae1cfbd41ed8ae3eea031a64899611a22a1821) - Adds an "API Recorder" to the view API page, some internal methods have been made async. Thanks @abidlabs! + +## 0.15.0 + +### Highlights + +#### Automatically delete state after user has disconnected from the webpage ([#7829](https://github.com/gradio-app/gradio/pull/7829) [`6a4bf7a`](https://github.com/gradio-app/gradio/commit/6a4bf7abe29059dbdc6a342e0366fdaa2e4120ee)) + +Gradio now automatically deletes `gr.State` variables stored in the server's RAM when users close their browser tab. +The deletion will happen 60 minutes after the server detected a disconnect from the user's browser. +If the user connects again in that timeframe, their state will not be deleted. + +Additionally, Gradio now includes a `Blocks.unload()` event, allowing you to run arbitrary cleanup functions when users disconnect (this does not have a 60 minute delay). +You can think of the `unload` event as the opposite of the `load` event. + + +```python +with gr.Blocks() as demo: + gr.Markdown( +"""# State Cleanup Demo +🖼️ Images are saved in a user-specific directory and deleted when the users closes the page via demo.unload. +""") + with gr.Row(): + with gr.Column(scale=1): + with gr.Row(): + img = gr.Image(label="Generated Image", height=300, width=300) + with gr.Row(): + gen = gr.Button(value="Generate") + with gr.Row(): + history = gr.Gallery(label="Previous Generations", height=500, columns=10) + state = gr.State(value=[], delete_callback=lambda v: print("STATE DELETED")) + + demo.load(generate_random_img, [state], [img, state, history]) + gen.click(generate_random_img, [state], [img, state, history]) + demo.unload(delete_directory) + + +demo.launch(auth=lambda user,pwd: True, + auth_message="Enter any username and password to continue") +``` + + Thanks @freddyaboulton! + +### Fixes + +- [#7888](https://github.com/gradio-app/gradio/pull/7888) [`946487c`](https://github.com/gradio-app/gradio/commit/946487cf8e477cbf8d6fad4e772ff574a21782c3) - Cache view_api info in server and python client. Thanks @freddyaboulton! + +## 0.14.0 + +### Features + +- [#7800](https://github.com/gradio-app/gradio/pull/7800) [`b0a3ea9`](https://github.com/gradio-app/gradio/commit/b0a3ea951c06d4f3ff2755b567629fe988a3e30d) - Small fix to client.view_api() in the case of default file values. Thanks @abidlabs! +- [#7732](https://github.com/gradio-app/gradio/pull/7732) [`2efb05e`](https://github.com/gradio-app/gradio/commit/2efb05ed99a8a3575aab0a6c14a8d8b91f4e9ed7) - Adds support for kwargs and default arguments in the python client, and improves how parameter information is displayed in the "view API" page. Thanks @abidlabs! + +## 0.13.0 + +### Features + +- [#7691](https://github.com/gradio-app/gradio/pull/7691) [`84f81fe`](https://github.com/gradio-app/gradio/commit/84f81fec9287b041203a141bbf2852720f7d199c) - Closing stream from the backend. Thanks @aliabid94! + +### Fixes + +- [#7718](https://github.com/gradio-app/gradio/pull/7718) [`6390d0b`](https://github.com/gradio-app/gradio/commit/6390d0bf6c2be0aefa56102dd029f25161bfebc3) - Add support for python client connecting to gradio apps running with self-signed SSL certificates. Thanks @abidlabs! +- [#7706](https://github.com/gradio-app/gradio/pull/7706) [`bc61ff6`](https://github.com/gradio-app/gradio/commit/bc61ff6b1603eedf3111f1b5c3d2751629902d98) - Several fixes to `gr.load`. Thanks @abidlabs! + +## 0.12.0 + +### Fixes + +- [#7575](https://github.com/gradio-app/gradio/pull/7575) [`d0688b3`](https://github.com/gradio-app/gradio/commit/d0688b3c25feabb4fc7dfa0ab86086b3af7eb337) - Files should now be supplied as `file(...)` in the Client, and some fixes to `gr.load()` as well. Thanks @abidlabs! +- [#7618](https://github.com/gradio-app/gradio/pull/7618) [`0ae1e44`](https://github.com/gradio-app/gradio/commit/0ae1e4486c06e06bb7a4bad45d58d14f1f8d1b94) - Control which files get moved to cache with gr.set_static_paths. Thanks @freddyaboulton! + +## 0.11.0 + +### Features + +- [#7407](https://github.com/gradio-app/gradio/pull/7407) [`375bfd2`](https://github.com/gradio-app/gradio/commit/375bfd28d2def576b4e1c12e0a60127b7419e826) - Fix server_messages.py to use the patched BaseModel class for Wasm env. Thanks [@aliabid94](https://github.com/aliabid94)! + +### Fixes + +- [#7555](https://github.com/gradio-app/gradio/pull/7555) [`fc4c2db`](https://github.com/gradio-app/gradio/commit/fc4c2dbd994c49e37296978da1cb85e424080d1c) - Allow Python Client to upload/download files when connecting to Gradio apps with auth enabled. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.10.1 + +### Features + +- [#7495](https://github.com/gradio-app/gradio/pull/7495) [`ddd4d3e`](https://github.com/gradio-app/gradio/commit/ddd4d3e4d3883fb7540d1df240fb08202fc77705) - Enable Ruff S101. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#7443](https://github.com/gradio-app/gradio/pull/7443) [`b7a97f2`](https://github.com/gradio-app/gradio/commit/b7a97f29b84a72678a717db03d2932ed6caae6ce) - Update `httpx` to `httpx>=0.24.1` in `requirements.txt`. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.10.0 + +### Features + +- [#7183](https://github.com/gradio-app/gradio/pull/7183) [`49d9c48`](https://github.com/gradio-app/gradio/commit/49d9c48537aa706bf72628e3640389470138bdc6) - [WIP] Refactor file normalization to be in the backend and remove it from the frontend of each component. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#7377](https://github.com/gradio-app/gradio/pull/7377) [`6dfd40f`](https://github.com/gradio-app/gradio/commit/6dfd40fc6b2fa461490d2370ab91fcda7e07c0da) - Make set_documentation_group a no-op. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#7334](https://github.com/gradio-app/gradio/pull/7334) [`b95d0d0`](https://github.com/gradio-app/gradio/commit/b95d0d043c739926af986e573200af92732bbc01) - Allow setting custom headers in Python Client. Thanks [@abidlabs](https://github.com/abidlabs)! + +### Fixes + +- [#7350](https://github.com/gradio-app/gradio/pull/7350) [`7302a6e`](https://github.com/gradio-app/gradio/commit/7302a6e151dac553c17833be64d4639ee4cf97aa) - Fix `gr.load` for file-based Spaces. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.9.0 + +### Features + +- [#7062](https://github.com/gradio-app/gradio/pull/7062) [`0fddd0f`](https://github.com/gradio-app/gradio/commit/0fddd0f971761bff3ef6ccc7ab9deb1891cd80d0) - Determine documentation group automatically. Thanks [@akx](https://github.com/akx)! +- [#7102](https://github.com/gradio-app/gradio/pull/7102) [`68a54a7`](https://github.com/gradio-app/gradio/commit/68a54a7a310d8d7072fdae930bf1cfdf12c45a7f) - Improve chatbot streaming performance with diffs. Thanks [@aliabid94](https://github.com/aliabid94)!/n Note that this PR changes the API format for generator functions, which would be a breaking change for any clients reading the EventStream directly +- [#7116](https://github.com/gradio-app/gradio/pull/7116) [`3c8c4ac`](https://github.com/gradio-app/gradio/commit/3c8c4ac2db284e1cb503c397205a79a6dcc27e23) - Document the `gr.ParamViewer` component, and fix component preprocessing/postprocessing docstrings. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#7061](https://github.com/gradio-app/gradio/pull/7061) [`05d8a3c`](https://github.com/gradio-app/gradio/commit/05d8a3c8030b733bd47250f5db6f89f230f9a707) - Update ruff to 0.1.13, enable more rules, fix issues. Thanks [@akx](https://github.com/akx)! + +### Fixes + +- [#7178](https://github.com/gradio-app/gradio/pull/7178) [`9f23b0b`](https://github.com/gradio-app/gradio/commit/9f23b0bc54b4ef63c056b309370df52ec2c2a43c) - Optimize client view_api method. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#7322](https://github.com/gradio-app/gradio/pull/7322) [`b25e95e`](https://github.com/gradio-app/gradio/commit/b25e95e164e80d66203ef71ce6bdb67ceb6b24df) - Fix `processing_utils.save_url_to_cache()` to follow redirects when accessing the URL. Thanks [@whitphx](https://github.com/whitphx)! + +## 0.8.1 + +### Features + +- [#7075](https://github.com/gradio-app/gradio/pull/7075) [`1fc8a94`](https://github.com/gradio-app/gradio/commit/1fc8a941384775f587a6ef30365960f43353cb0d) - fix lint. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#7054](https://github.com/gradio-app/gradio/pull/7054) [`64c65d8`](https://github.com/gradio-app/gradio/commit/64c65d821983961111297a969946d87e2fc4105d) - Add encoding to open/writing files on the deploy_discord function. Thanks [@WilliamHarer](https://github.com/WilliamHarer)! + +## 0.8.0 + +### Fixes + +- [#6846](https://github.com/gradio-app/gradio/pull/6846) [`48d6534`](https://github.com/gradio-app/gradio/commit/48d6534b40f80e7e70a4061f97d9f2e23ba77fe1) - Add `show_api` parameter to events, and fix `gr.load()`. Also makes some minor improvements to the "view API" page when running on Spaces. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#6767](https://github.com/gradio-app/gradio/pull/6767) [`7bb561a`](https://github.com/gradio-app/gradio/commit/7bb561a294ca41d1044927cb34d8645c4175cae0) - Rewriting parts of the README and getting started guides for 4.0. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.7.3 + +### Fixes + +- [#6693](https://github.com/gradio-app/gradio/pull/6693) [`34f9431`](https://github.com/gradio-app/gradio/commit/34f943101bf7dd6b8a8974a6131c1ed7c4a0dac0) - Python client properly handles hearbeat and log messages. Also handles responses longer than 65k. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.7.2 + +### Features + +- [#6598](https://github.com/gradio-app/gradio/pull/6598) [`7cbf96e`](https://github.com/gradio-app/gradio/commit/7cbf96e0bdd12db7ecac7bf99694df0a912e5864) - Issue 5245: consolidate usage of requests and httpx. Thanks [@cswamy](https://github.com/cswamy)! +- [#6704](https://github.com/gradio-app/gradio/pull/6704) [`24e0481`](https://github.com/gradio-app/gradio/commit/24e048196e8f7bd309ef5c597d4ffc6ca4ed55d0) - Hotfix: update `huggingface_hub` dependency version. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#6543](https://github.com/gradio-app/gradio/pull/6543) [`8a70e83`](https://github.com/gradio-app/gradio/commit/8a70e83db9c7751b46058cdd2514e6bddeef6210) - switch from black to ruff formatter. Thanks [@DarhkVoyd](https://github.com/DarhkVoyd)! + +### Fixes + +- [#6556](https://github.com/gradio-app/gradio/pull/6556) [`d76bcaa`](https://github.com/gradio-app/gradio/commit/d76bcaaaf0734aaf49a680f94ea9d4d22a602e70) - Fix api event drops. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.7.1 + +### Fixes + +- [#6602](https://github.com/gradio-app/gradio/pull/6602) [`b8034a1`](https://github.com/gradio-app/gradio/commit/b8034a1e72c3aac649ee0ad9178ffdbaaa60fc61) - Fix: Gradio Client work with private Spaces. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.7.0 + +### Features + +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Add json schema unit tests. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Image v4. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Custom components. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Swap websockets for SSE. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.7.0-beta.2 + +### Features + +- [#6094](https://github.com/gradio-app/gradio/pull/6094) [`c476bd5a5`](https://github.com/gradio-app/gradio/commit/c476bd5a5b70836163b9c69bf4bfe068b17fbe13) - Image v4. Thanks [@pngwn](https://github.com/pngwn)! +- [#6069](https://github.com/gradio-app/gradio/pull/6069) [`bf127e124`](https://github.com/gradio-app/gradio/commit/bf127e1241a41401e144874ea468dff8474eb505) - Swap websockets for SSE. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.7.0-beta.1 + +### Features + +- [#6082](https://github.com/gradio-app/gradio/pull/6082) [`037e5af33`](https://github.com/gradio-app/gradio/commit/037e5af3363c5b321b95efc955ee8d6ec0f4504e) - WIP: Fix docs. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5970](https://github.com/gradio-app/gradio/pull/5970) [`0c571c044`](https://github.com/gradio-app/gradio/commit/0c571c044035989d6fe33fc01fee63d1780635cb) - Add json schema unit tests. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6073](https://github.com/gradio-app/gradio/pull/6073) [`abff6fb75`](https://github.com/gradio-app/gradio/commit/abff6fb758bd310053a23c938bf1dd8fbdc5d333) - Fix remaining xfail tests in backend. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.7.0-beta.0 + +### Features + +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`85ba6de13`](https://github.com/gradio-app/gradio/commit/85ba6de136a45b3e92c74e410bb27e3cbe7138d7) - Simplify how files are handled in components in 4.0. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`85ba6de13`](https://github.com/gradio-app/gradio/commit/85ba6de136a45b3e92c74e410bb27e3cbe7138d7) - Rename gradio_component to gradio component. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.6.1 + +### Fixes + +- [#5811](https://github.com/gradio-app/gradio/pull/5811) [`1d5b15a2d`](https://github.com/gradio-app/gradio/commit/1d5b15a2d24387154f2cfb40a36de25b331471d3) - Assert refactor in external.py. Thanks [@harry-urek](https://github.com/harry-urek)! + +## 0.6.0 + +### Highlights + +#### new `FileExplorer` component ([#5672](https://github.com/gradio-app/gradio/pull/5672) [`e4a307ed6`](https://github.com/gradio-app/gradio/commit/e4a307ed6cde3bbdf4ff2f17655739addeec941e)) + +Thanks to a new capability that allows components to communicate directly with the server _without_ passing data via the value, we have created a new `FileExplorer` component. + +This component allows you to populate the explorer by passing a glob, but only provides the selected file(s) in your prediction function. + +Users can then navigate the virtual filesystem and select files which will be accessible in your predict function. This component will allow developers to build more complex spaces, with more flexible input options. + +![output](https://github.com/pngwn/MDsveX/assets/12937446/ef108f0b-0e84-4292-9984-9dc66b3e144d) + +For more information check the [`FileExplorer` documentation](https://gradio.app/docs/fileexplorer). + + Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.5.3 + +### Features + +- [#5721](https://github.com/gradio-app/gradio/pull/5721) [`84e03fe50`](https://github.com/gradio-app/gradio/commit/84e03fe506e08f1f81bac6d504c9fba7924f2d93) - Adds copy buttons to website, and better descriptions to API Docs. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.5.2 + +### Features + +- [#5653](https://github.com/gradio-app/gradio/pull/5653) [`ea0e00b20`](https://github.com/gradio-app/gradio/commit/ea0e00b207b4b90a10e9d054c4202d4e705a29ba) - Prevent Clients from accessing API endpoints that set `api_name=False`. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.5.1 + +### Features + +- [#5514](https://github.com/gradio-app/gradio/pull/5514) [`52f783175`](https://github.com/gradio-app/gradio/commit/52f7831751b432411e109bd41add4ab286023a8e) - refactor: Use package.json for version management. Thanks [@DarhkVoyd](https://github.com/DarhkVoyd)! + +## 0.5.0 + +### Highlights + +#### Enable streaming audio in python client ([#5248](https://github.com/gradio-app/gradio/pull/5248) [`390624d8`](https://github.com/gradio-app/gradio/commit/390624d8ad2b1308a5bf8384435fd0db98d8e29e)) + +The `gradio_client` now supports streaming file outputs 🌊 + +No new syntax! Connect to a gradio demo that supports streaming file outputs and call `predict` or `submit` as you normally would. + +```python +import gradio_client as grc +client = grc.Client("gradio/stream_audio_out") + +# Get the entire generated audio as a local file +client.predict("/Users/freddy/Pictures/bark_demo.mp4", api_name="/predict") + +job = client.submit("/Users/freddy/Pictures/bark_demo.mp4", api_name="/predict") + +# Get the entire generated audio as a local file +job.result() + +# Each individual chunk +job.outputs() +``` + + Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Fixes + +- [#5295](https://github.com/gradio-app/gradio/pull/5295) [`7b8fa8aa`](https://github.com/gradio-app/gradio/commit/7b8fa8aa58f95f5046b9add64b40368bd3f1b700) - Allow caching examples with streamed output. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.4.0 + +### Highlights + +#### Client.predict will now return the final output for streaming endpoints ([#5057](https://github.com/gradio-app/gradio/pull/5057) [`35856f8b`](https://github.com/gradio-app/gradio/commit/35856f8b54548cae7bd3b8d6a4de69e1748283b2)) + +### This is a breaking change (for gradio_client only)! + +Previously, `Client.predict` would only return the first output of an endpoint that streamed results. This was causing confusion for developers that wanted to call these streaming demos via the client. + +We realize that developers using the client don't know the internals of whether a demo streams or not, so we're changing the behavior of predict to match developer expectations. + +Using `Client.predict` will now return the final output of a streaming endpoint. This will make it even easier to use gradio apps via the client. + + Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Features + +- [#5076](https://github.com/gradio-app/gradio/pull/5076) [`2745075a`](https://github.com/gradio-app/gradio/commit/2745075a26f80e0e16863d483401ff1b6c5ada7a) - Add deploy_discord to docs. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Fixes + +- [#5061](https://github.com/gradio-app/gradio/pull/5061) [`136adc9c`](https://github.com/gradio-app/gradio/commit/136adc9ccb23e5cb4d02d2e88f23f0b850041f98) - Ensure `gradio_client` is backwards compatible with `gradio==3.24.1`. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.3.0 + +### Highlights + +#### Create Discord Bots from Gradio Apps 🤖 ([#4960](https://github.com/gradio-app/gradio/pull/4960) [`46e4ef67`](https://github.com/gradio-app/gradio/commit/46e4ef67d287dd68a91473b73172b29cbad064bc)) + +We're excited to announce that Gradio can now automatically create a discord bot from any `gr.ChatInterface` app. + +It's as easy as importing `gradio_client`, connecting to the app, and calling `deploy_discord`! + +_🦙 Turning Llama 2 70b into a discord bot 🦙_ + +```python +import gradio_client as grc +grc.Client("ysharma/Explore_llamav2_with_TGI").deploy_discord(to_id="llama2-70b-discord-bot") +``` + + + +#### Getting started with template spaces + +To help get you started, we have created an organization on Hugging Face called [gradio-discord-bots](https://huggingface.co/gradio-discord-bots) with template spaces you can use to turn state of the art LLMs powered by Gradio to discord bots. + +Currently we have template spaces for: + +- [Llama-2-70b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/Llama-2-70b-chat-hf) powered by a FREE Hugging Face Inference Endpoint! +- [Llama-2-13b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/Llama-2-13b-chat-hf) powered by Hugging Face Inference Endpoints. +- [Llama-2-13b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/llama-2-13b-chat-transformers) powered by Hugging Face transformers. +- [falcon-7b-instruct](https://huggingface.co/spaces/gradio-discord-bots/falcon-7b-instruct) powered by Hugging Face Inference Endpoints. +- [gpt-3.5-turbo](https://huggingface.co/spaces/gradio-discord-bots/gpt-35-turbo), powered by openai. Requires an OpenAI key. + +But once again, you can deploy ANY `gr.ChatInterface` app exposed on the internet! So don't hesitate to try it on your own Chatbots. + +❗️ Additional Note ❗️: Technically, any gradio app that exposes an api route that takes in a single string and outputs a single string can be deployed to discord. But `gr.ChatInterface` apps naturally lend themselves to discord's chat functionality so we suggest you start with those. + +Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### New Features: + +- Endpoints that return layout components are now properly handled in the `submit` and `view_api` methods. Output layout components are not returned by the API but all other components are (excluding `gr.State`). By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4871](https://github.com/gradio-app/gradio/pull/4871) + +### Bug Fixes: + +No changes to highlight + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +# 0.2.9 + +### New Features: + +No changes to highlight + +### Bug Fixes: + +- Fix bug determining the api name when a demo has `api_name=False` by [@freddyboulton](https://github.com/freddyaboulton) in [PR 4886](https://github.com/gradio-app/gradio/pull/4886) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Pinned dependencies to major versions to reduce the likelihood of a broken `gradio_client` due to changes in downstream dependencies by [@abidlabs](https://github.com/abidlabs) in [PR 4885](https://github.com/gradio-app/gradio/pull/4885) + +# 0.2.8 + +### New Features: + +- Support loading gradio apps where `api_name=False` by [@abidlabs](https://github.com/abidlabs) in [PR 4683](https://github.com/gradio-app/gradio/pull/4683) + +### Bug Fixes: + +- Fix bug where space duplication would error if the demo has cpu-basic hardware by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4583](https://github.com/gradio-app/gradio/pull/4583) +- Fixes and optimizations to URL/download functions by [@akx](https://github.com/akx) in [PR 4695](https://github.com/gradio-app/gradio/pull/4695) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +# 0.2.7 + +### New Features: + +- The output directory for files downloaded via the Client can now be set by the `output_dir` parameter in `Client` by [@abidlabs](https://github.com/abidlabs) in [PR 4501](https://github.com/gradio-app/gradio/pull/4501) + +### Bug Fixes: + +- The output directory for files downloaded via the Client are now set to a temporary directory by default (instead of the working directory in some cases) by [@abidlabs](https://github.com/abidlabs) in [PR 4501](https://github.com/gradio-app/gradio/pull/4501) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +# 0.2.6 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fixed bug file deserialization didn't preserve all file extensions by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4440](https://github.com/gradio-app/gradio/pull/4440) +- Fixed bug where mounted apps could not be called via the client by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4435](https://github.com/gradio-app/gradio/pull/4435) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +# 0.2.5 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fixes parameter names not showing underscores by [@abidlabs](https://github.com/abidlabs) in [PR 4230](https://github.com/gradio-app/gradio/pull/4230) +- Fixes issue in which state was not handled correctly if `serialize=False` by [@abidlabs](https://github.com/abidlabs) in [PR 4230](https://github.com/gradio-app/gradio/pull/4230) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +# 0.2.4 + +### Bug Fixes: + +- Fixes missing serialization classes for several components: `Barplot`, `Lineplot`, `Scatterplot`, `AnnotatedImage`, `Interpretation` by [@abidlabs](https://github.com/abidlabs) in [PR 4167](https://github.com/gradio-app/gradio/pull/4167) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +# 0.2.3 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fix example inputs for `gr.File(file_count='multiple')` output components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4153](https://github.com/gradio-app/gradio/pull/4153) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +# 0.2.2 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Only send request to `/info` route if demo version is above `3.28.3` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4109](https://github.com/gradio-app/gradio/pull/4109) + +### Other Changes: + +- Fix bug in test from gradio 3.29.0 refactor by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4138](https://github.com/gradio-app/gradio/pull/4138) + +### Breaking Changes: + +No changes to highlight. + +# 0.2.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +Removes extraneous `State` component info from the `Client.view_api()` method by [@abidlabs](https://github.com/freddyaboulton) in [PR 4107](https://github.com/gradio-app/gradio/pull/4107) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +Separates flaky tests from non-flaky tests by [@abidlabs](https://github.com/freddyaboulton) in [PR 4107](https://github.com/gradio-app/gradio/pull/4107) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +# 0.1.4 + +### New Features: + +- Progress Updates from `gr.Progress()` can be accessed via `job.status().progress_data` by @freddyaboulton](https://github.com/freddyaboulton) in [PR 3924](https://github.com/gradio-app/gradio/pull/3924) + +### Bug Fixes: + +- Fixed bug where unnamed routes where displayed with `api_name` instead of `fn_index` in `view_api` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3972](https://github.com/gradio-app/gradio/pull/3972) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +# 0.1.3 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fixed bug where `Video` components in latest gradio were not able to be deserialized by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3860](https://github.com/gradio-app/gradio/pull/3860) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +# 0.1.2 + +First public release of the Gradio Client library! The `gradio_client` Python library that makes it very easy to use any Gradio app as an API. + +As an example, consider this [Hugging Face Space that transcribes audio files](https://huggingface.co/spaces/abidlabs/whisper) that are recorded from the microphone. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/whisper-screenshot.jpg) + +Using the `gradio_client` library, we can easily use the Gradio as an API to transcribe audio files programmatically. + +Here's the entire code to do it: + +```python +from gradio_client import Client + +client = Client("abidlabs/whisper") +client.predict("audio_sample.wav") + +>> "This is a test of the whisper speech recognition model." +``` + +Read more about how to use the `gradio_client` library here: https://gradio.app/getting-started-with-the-python-client/ \ No newline at end of file diff --git a/client/python/README.md b/client/python/README.md new file mode 100644 index 0000000..07049e8 --- /dev/null +++ b/client/python/README.md @@ -0,0 +1,143 @@ +# `gradio_client`: Use a Gradio app as an API -- in 3 lines of Python + +This directory contains the source code for `gradio_client`, a lightweight Python library that makes it very easy to use any Gradio app as an API. + +As an example, consider this [Hugging Face Space that transcribes audio files](https://huggingface.co/spaces/abidlabs/whisper) that are recorded from the microphone. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/whisper-screenshot.jpg) + +Using the `gradio_client` library, we can easily use the Gradio as an API to transcribe audio files programmatically. + +Here's the entire code to do it: + +```python +from gradio_client import Client + +client = Client("abidlabs/whisper") +client.predict("audio_sample.wav") + +>> "This is a test of the whisper speech recognition model." +``` + +The Gradio client works with any Gradio Space, whether it be an image generator, a stateful chatbot, or a tax calculator. + +## Installation + +If you already have a recent version of `gradio`, then the `gradio_client` is included as a dependency. + +Otherwise, the lightweight `gradio_client` package can be installed from pip (or pip3) and works with Python versions 3.10 or higher: + +```bash +$ pip install gradio_client +``` + +## Basic Usage + +### Connecting to a Space or a Gradio app + +Start by connecting instantiating a `Client` object and connecting it to a Gradio app that is running on Spaces (or anywhere else)! + +**Connecting to a Space** + +```python +from gradio_client import Client + +client = Client("abidlabs/en2fr") # a Space that translates from English to French +``` + +You can also connect to private Spaces by passing in your HF token with the `hf_token` parameter. You can get your HF token here: https://huggingface.co/settings/tokens + +```python +from gradio_client import Client + +client = Client("abidlabs/my-private-space", hf_token="...") +``` + +**Duplicating a Space for private use** + +While you can use any public Space as an API, you may get rate limited by Hugging Face if you make too many requests. For unlimited usage of a Space, simply duplicate the Space to create a private Space, +and then use it to make as many requests as you'd like! + +The `gradio_client` includes a class method: `Client.duplicate()` to make this process simple: + +```python +from gradio_client import Client + +client = Client.duplicate("abidlabs/whisper") +client.predict("audio_sample.wav") + +>> "This is a test of the whisper speech recognition model." +``` + +If you have previously duplicated a Space, re-running `duplicate()` will _not_ create a new Space. Instead, the Client will attach to the previously-created Space. So it is safe to re-run the `Client.duplicate()` method multiple times. + +**Note:** if the original Space uses GPUs, your private Space will as well, and your Hugging Face account will get billed based on the price of the GPU. To minimize charges, your Space will automatically go to sleep after 1 hour of inactivity. You can also set the hardware using the `hardware` parameter of `duplicate()`. + +**Connecting a general Gradio app** + +If your app is running somewhere else, just provide the full URL instead, including the "http://" or "https://". Here's an example of making predictions to a Gradio app that is running on a share URL: + +```python +from gradio_client import Client + +client = Client("https://bec81a83-5b5c-471e.gradio.live") +``` + +### Inspecting the API endpoints + +Once you have connected to a Gradio app, you can view the APIs that are available to you by calling the `.view_api()` method. For the Whisper Space, we see the following: + +``` +Client.predict() Usage Info +--------------------------- +Named API endpoints: 1 + + - predict(input_audio, api_name="/predict") -> value_0 + Parameters: + - [Audio] input_audio: str (filepath or URL) + Returns: + - [Textbox] value_0: str (value) +``` + +This shows us that we have 1 API endpoint in this space, and shows us how to use the API endpoint to make a prediction: we should call the `.predict()` method, providing a parameter `input_audio` of type `str`, which is a `filepath or URL`. + +We should also provide the `api_name='/predict'` argument. Although this isn't necessary if a Gradio app has a single named endpoint, it does allow us to call different endpoints in a single app if they are available. If an app has unnamed API endpoints, these can also be displayed by running `.view_api(all_endpoints=True)`. + +### Making a prediction + +The simplest way to make a prediction is simply to call the `.predict()` function with the appropriate arguments: + +```python +from gradio_client import Client + +client = Client("abidlabs/en2fr") +client.predict("Hello") + +>> Bonjour +``` + +If there are multiple parameters, then you should pass them as separate arguments to `.predict()`, like this: + +```python +from gradio_client import Client + +client = Client("gradio/calculator") +client.predict(4, "add", 5) + +>> 9.0 +``` + +For certain inputs, such as images, you should pass in the filepath or URL to the file. Likewise, for the corresponding output types, you will get a filepath or URL returned. + +```python +from gradio_client import Client + +client = Client("abidlabs/whisper") +client.predict("https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3") + +>> "My thought I have nobody by a beauty and will as you poured. Mr. Rochester is serve in that so don't find simpus, and devoted abode, to at might in a r—" +``` + +## Advanced Usage + +For more ways to use the Gradio Python Client, check out our dedicated Guide on the Python client, available here: https://www.gradio.app/guides/getting-started-with-the-python-client diff --git a/client/python/build_pypi.sh b/client/python/build_pypi.sh new file mode 100755 index 0000000..00068e1 --- /dev/null +++ b/client/python/build_pypi.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +cd "$(dirname ${0})" + +python3 -m pip install build +rm -rf dist/* +rm -rf build/* +python3 -m build diff --git a/client/python/gradio_client/CHANGELOG.md b/client/python/gradio_client/CHANGELOG.md new file mode 100644 index 0000000..a03966b --- /dev/null +++ b/client/python/gradio_client/CHANGELOG.md @@ -0,0 +1,1256 @@ +# gradio_client + +## 2.5.0 + +### Features + +- [#13289](https://github.com/gradio-app/gradio/pull/13289) [`d6f24df`](https://github.com/gradio-app/gradio/commit/d6f24df6233e7882746ba6e49307a34a11101ea1) - Improve curl info. Thanks @freddyaboulton! + +### Fixes + +- [#13280](https://github.com/gradio-app/gradio/pull/13280) [`bb9c130`](https://github.com/gradio-app/gradio/commit/bb9c130e1e8c60b8a717da7184b02ab459be3f03) - Fix snippet generator crash on datetime values in Dataframe inputs. Thanks @ParamChordiya! + +## 2.4.1 + +### Features + +- [#13246](https://github.com/gradio-app/gradio/pull/13246) [`ff90963`](https://github.com/gradio-app/gradio/commit/ff909638e72f0b44e8629e5a28cea8276cdd9ab2) - Add Documentation Group for gr.Cache. Thanks @freddyaboulton! + +### Fixes + +- [#13204](https://github.com/gradio-app/gradio/pull/13204) [`9953db9`](https://github.com/gradio-app/gradio/commit/9953db94e406477df96f98adf3e47246181ceef9) - fix: preserve special characters in uploaded filenames. Thanks @xr843! + +## 2.4.0 + +### Features + +- [#13045](https://github.com/gradio-app/gradio/pull/13045) [`a35f589`](https://github.com/gradio-app/gradio/commit/a35f5896e43d2585d9206e8256b4d7e321fcd0fe) - Gradio Prediction CLI Commands. Thanks @freddyaboulton! + +### Fixes + +- [#12979](https://github.com/gradio-app/gradio/pull/12979) [`4a4c7f3`](https://github.com/gradio-app/gradio/commit/4a4c7f3b0d6fd8009fdafc580d5852984f961db1) - preserve file extension when filename stem is stripped entirely in gr.File. Thanks @giulio-leone! + +## 2.3.0 + +### Features + +- [#12879](https://github.com/gradio-app/gradio/pull/12879) [`c498688`](https://github.com/gradio-app/gradio/commit/c4986883b267570d76b442899c6fc09d14e3e222) - Ensure svelte version mismatches do not break custom components. Thanks @pngwn! + +### Fixes + +- [#12942](https://github.com/gradio-app/gradio/pull/12942) [`e5ba4fa`](https://github.com/gradio-app/gradio/commit/e5ba4fa992c0ac389c6af2d143c9ad4c33eea360) - perf: use deque for SSE pending message queues in gradio_client. Thanks @giulio-leone! + +## 2.2.0 + +### Features + +- [#12918](https://github.com/gradio-app/gradio/pull/12918) [`e29e1cc`](https://github.com/gradio-app/gradio/commit/e29e1ccd5874cb98b813ed4f7f72d9fef2935016) - Add Space-specific skill generation to `gradio skills add`. Thanks @abidlabs! + +## 2.1.0 + +### Features + +- [#12700](https://github.com/gradio-app/gradio/pull/12700) [`b01c95a`](https://github.com/gradio-app/gradio/commit/b01c95a58be8e18bb4ddef7f2ee238a7774e5be9) - Rewrite behavior section of docs. Thanks @aliabd! + +### Fixes + +- [#12882](https://github.com/gradio-app/gradio/pull/12882) [`fc7c01e`](https://github.com/gradio-app/gradio/commit/fc7c01ea1e581ef70be98fddf003b0c91315c7cc) - Validate proxy url host. Thanks @freddyaboulton! +- [#12811](https://github.com/gradio-app/gradio/pull/12811) [`8f8cef8`](https://github.com/gradio-app/gradio/commit/8f8cef87bfb3af64867804ad45f4385af09e07b4) - Fix windows tests. Thanks @freddyaboulton! + +## 2.0.3 + +### Fixes + +- [#12614](https://github.com/gradio-app/gradio/pull/12614) [`6222192`](https://github.com/gradio-app/gradio/commit/622219242412400df57a73aebfd3de96bb8499de) - fix(client): make WebP and VTT MIME type detection case-insensitive. Thanks @majiayu000! + +## 2.0.2 + +### Fixes + +- [#12585](https://github.com/gradio-app/gradio/pull/12585) [`1724a02`](https://github.com/gradio-app/gradio/commit/1724a02f37118af098cd9f51472955bf8ef8d794) - Remove checkmark from windows. Thanks @freddyaboulton! +- [#12600](https://github.com/gradio-app/gradio/pull/12600) [`1fafaba`](https://github.com/gradio-app/gradio/commit/1fafabaace315b1c699855cadb69eb17488de957) - Add x-gradio-user-header. Thanks @freddyaboulton! + +## 2.0.1 + +### Fixes + +- [#12480](https://github.com/gradio-app/gradio/pull/12480) [`b9732d1`](https://github.com/gradio-app/gradio/commit/b9732d10680ca66fa7b3f1e763d3cdd57b38c6ed) - [BUGFIX] Fix stream file download in gradio client. Thanks @frascuchon! +- [#12490](https://github.com/gradio-app/gradio/pull/12490) [`472e164`](https://github.com/gradio-app/gradio/commit/472e16439f2573b33d21b9635706416518405ddd) - Make client backwards compatible with version 5. Thanks @freddyaboulton! + +## 2.0.0-dev.3 + +### Features + +- [#12377](https://github.com/gradio-app/gradio/pull/12377) [`568644a`](https://github.com/gradio-app/gradio/commit/568644a88c4dbe1b6ad9468906b1e45ed07657f3) - Add a 6.0 migration guide and add deprecation warnings. Thanks @abidlabs! + +## 2.0.0-dev.2 + +### Features + +- [#12243](https://github.com/gradio-app/gradio/pull/12243) [`cb49168`](https://github.com/gradio-app/gradio/commit/cb49168b1d52b39e47f1faeda93d150e048bd625) - [Client] Fix ZeroGPU headers forwarding. Thanks @cbensimon! +- [#12217](https://github.com/gradio-app/gradio/pull/12217) [`681fa11`](https://github.com/gradio-app/gradio/commit/681fa11357de520f0b05605cbd300d9e276d8736) - Update ZeroGPU guide to reflect best practices on manually passing an IP token. Thanks @dawoodkhan82! + +## 2.0.0-dev.1 + +### Features + +- [#12069](https://github.com/gradio-app/gradio/pull/12069) [`9de88ca`](https://github.com/gradio-app/gradio/commit/9de88ca470ce529366d259f0deaa955f658000b9) - Rename show_api. Thanks @freddyaboulton! + +## 2.0.0-dev.0 + +### Features + +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`53d9098`](https://github.com/gradio-app/gradio/commit/53d9098cca378f6ebff9dec15d2faa8a3d2fb510) - [No Merge] Gradio 6.0. Thanks @freddyaboulton! + +## 1.13.3 + +### Fixes + +- [#11979](https://github.com/gradio-app/gradio/pull/11979) [`50a89f0`](https://github.com/gradio-app/gradio/commit/50a89f079250735f268dfeeeddf40ffea84d7919) - Allow gradio to run with huggingface_hub 1.x. Thanks @Wauplin! + +## 1.13.2 + +### Features + +- [#11962](https://github.com/gradio-app/gradio/pull/11962) [`fdfe4fe`](https://github.com/gradio-app/gradio/commit/fdfe4fec334726fe91be98062826eb4b537410c3) - fix(client): websockets minimum version for asyncio. Thanks @cbensimon! + +## 1.13.1 + +### Fixes + +- [#11896](https://github.com/gradio-app/gradio/pull/11896) [`915f3a2`](https://github.com/gradio-app/gradio/commit/915f3a2f2fa1843111868e4f23f543b097b9c839) - Send EventData when app is loaded via gr.load. Thanks @freddyaboulton! +- [#11924](https://github.com/gradio-app/gradio/pull/11924) [`a9167fb`](https://github.com/gradio-app/gradio/commit/a9167fb7d013b7f76784c98f9c673ce47214b119) - Update websocket import and type in get_pred_from_ws. Thanks @qgallouedec! + +## 1.13.0 + +### Features + +- [#11814](https://github.com/gradio-app/gradio/pull/11814) [`013784a`](https://github.com/gradio-app/gradio/commit/013784a7086047651e8e661a38bde7d5c7f10db7) - add validation support. Thanks @pngwn! + +## 1.12.1 + +### Features + +- [#11744](https://github.com/gradio-app/gradio/pull/11744) [`f54e454`](https://github.com/gradio-app/gradio/commit/f54e454d6c2e256873f49e8bfb89f7161ec0a040) - fix client. Thanks @abidlabs! + +## 1.12.0 + +### Features + +- [#11723](https://github.com/gradio-app/gradio/pull/11723) [`379f0c1`](https://github.com/gradio-app/gradio/commit/379f0c151943b5f269910eba4a4c7abc6145a11c) - Support MCP resources and prompts. Thanks @abidlabs! + +## 1.11.1 + +### Features + +- [#11700](https://github.com/gradio-app/gradio/pull/11700) [`37d4c48`](https://github.com/gradio-app/gradio/commit/37d4c4809616595642f8d4a60be37d9915317443) - Have `Client` send authorization token with the `X-HF-Authorization` token. Thanks @abidlabs! + +## 1.11.0 + +### Features + +- [#11567](https://github.com/gradio-app/gradio/pull/11567) [`150ed18`](https://github.com/gradio-app/gradio/commit/150ed18b856e34d5a96a9e17bd5ad510e11872a6) - Stream Progress Updates to MCP clients. Thanks @freddyaboulton! + +## 1.10.4 + +### Fixes + +- [#11041](https://github.com/gradio-app/gradio/pull/11041) [`474aa30`](https://github.com/gradio-app/gradio/commit/474aa30979c14793e00852ad2f4ef6c12a41b900) - Attach cookies to client after first request (to support multiple-replica setups with cookie-based affinity). Thanks @abidlabs! + +## 1.10.3 + +### Fixes + +- [#11347](https://github.com/gradio-app/gradio/pull/11347) [`fdce3a0`](https://github.com/gradio-app/gradio/commit/fdce3a094fe1278ae83fe2f8b134b4c268506cfe) - Fix `gr.api()` to support more types, including optional params. Thanks @abidlabs! + +## 1.10.2 + +### Fixes + +- [#11215](https://github.com/gradio-app/gradio/pull/11215) [`2186ae3`](https://github.com/gradio-app/gradio/commit/2186ae3f4d6f8ac83883c359aed88a5b72746b5d) - Allow httpx_kwargs to contain cookies. Thanks @santibreo! + +## 1.10.1 + +### Features + +- [#11185](https://github.com/gradio-app/gradio/pull/11185) [`e64b83b`](https://github.com/gradio-app/gradio/commit/e64b83bf42a813d9269e519189523f8390a72ec4) - Evaluate index variable in argument description. Thanks @emmanuel-ferdman! + +### Fixes + +- [#11172](https://github.com/gradio-app/gradio/pull/11172) [`b618571`](https://github.com/gradio-app/gradio/commit/b618571ed17a8cece4bf9f1f55ed2e8bf7b59c46) - Fix python client SSE decoding issue. Thanks @freddyaboulton! + +## 1.10.0 + +### Features + +- [#10984](https://github.com/gradio-app/gradio/pull/10984) [`8dab577`](https://github.com/gradio-app/gradio/commit/8dab5771c7d952c76f325681dbf364119c91b0b1) - Let Gradio apps also be MCP Servers. Thanks @abidlabs! + +## 1.9.1 + +### Fixes + +- [#11093](https://github.com/gradio-app/gradio/pull/11093) [`cb322df`](https://github.com/gradio-app/gradio/commit/cb322df1f4df858590e760a82f6410f3b6db899b) - Update client.py to always send file data, even for files without extensions. Thanks @edmcman! + +## 1.9.0 + +### Features + +- [#11043](https://github.com/gradio-app/gradio/pull/11043) [`62a0080`](https://github.com/gradio-app/gradio/commit/62a00806e198c4c76bc132255d4b78c7aa329157) - Pass any visible error modals from a Gradio app downstream to the app that has `gr.load`-ed it. Thanks @abidlabs! + +## 1.8.0 + +### Features + +- [#10809](https://github.com/gradio-app/gradio/pull/10809) [`99b69df`](https://github.com/gradio-app/gradio/commit/99b69df1c000da092373440800fc08abe6ae9e20) - Trigger python client release. Thanks @freddyaboulton! + +## 1.7.2 + +### Features + +- [#10664](https://github.com/gradio-app/gradio/pull/10664) [`0b1f729`](https://github.com/gradio-app/gradio/commit/0b1f72941fd50298562102e39f4feafaa16f5968) - Allow websocket version 15. Thanks @freddyaboulton! +- Test + +## 1.7.1 + +### Fixes + +- [#10580](https://github.com/gradio-app/gradio/pull/10580) [`4e70d74`](https://github.com/gradio-app/gradio/commit/4e70d74068b77ebb3d285aa78e9202fff76337a2) - Fix `gr.load()` for `gr.ChatInterface(save_history=True)` and any Gradio app where the upstream app includes a `gr.State` as input. Thanks @abidlabs! + +## 1.7.0 + +### Features + +- [#10470](https://github.com/gradio-app/gradio/pull/10470) [`3465fdb`](https://github.com/gradio-app/gradio/commit/3465fdb19087471598ca07c93bc4ff3e1b6b2abf) - Format backend with latest `ruff`. Thanks @abidlabs! +- [#10435](https://github.com/gradio-app/gradio/pull/10435) [`ef66fe5`](https://github.com/gradio-app/gradio/commit/ef66fe52b22448a5125a314581f2ec6c73c24145) - Sidebar Component. Thanks @dawoodkhan82! + +## 1.6.0 + +### Features + +- [#10352](https://github.com/gradio-app/gradio/pull/10352) [`6a7cfc4`](https://github.com/gradio-app/gradio/commit/6a7cfc4264822209148ad07d8f38a0550bdb32b7) - Compatibility between Client and ZeroGPU. Thanks @abidlabs! + +## 1.5.4 + +### Fixes + +- [#10332](https://github.com/gradio-app/gradio/pull/10332) [`e742dcc`](https://github.com/gradio-app/gradio/commit/e742dcccb376692c9ddd5a6c251080e7c5936574) - Allow users to add a custom API route. Thanks @aliabid94! + +## 1.5.3 + +### Features + +- [#10221](https://github.com/gradio-app/gradio/pull/10221) [`506bd28`](https://github.com/gradio-app/gradio/commit/506bd2884a9790fb6f8dbf5684576e80d2b8ee64) - Update Guides related to deploying Gradio chatbots to Discord, Slack, and website widgets. Thanks @abidlabs! + +### Fixes + +- [#10238](https://github.com/gradio-app/gradio/pull/10238) [`3f19210`](https://github.com/gradio-app/gradio/commit/3f192100d6997751d0246b396a4fd8eaa86a826b) - Declare exports in __all__ for type checking. Thanks @dustalov! + +## 1.5.2 + +### Features + +- [#10196](https://github.com/gradio-app/gradio/pull/10196) [`c9ba9a4`](https://github.com/gradio-app/gradio/commit/c9ba9a447596a9ccdd21955adb3b34b15cac7ade) - Use the modern lower-case Python types in the API typing information. Thanks @abidlabs! +- [#10193](https://github.com/gradio-app/gradio/pull/10193) [`424365b`](https://github.com/gradio-app/gradio/commit/424365bdbd0b805e3b2d0c44ccc0f47201b1d96a) - JSON type fix in Client and and typing fix for `/chat` endpoint in `gr.ChatInterface`. Thanks @abidlabs! + +## 1.5.1 + +### Fixes + +- [#10090](https://github.com/gradio-app/gradio/pull/10090) [`5ea3cb5`](https://github.com/gradio-app/gradio/commit/5ea3cb51a39ba01fda7f65ff31e59955e1d12cea) - Update `requirements.txt` for `gradio` and `gradio_client`. Thanks @abidlabs! + +## 1.5.0 + +### Features + +- [#10017](https://github.com/gradio-app/gradio/pull/10017) [`a95fda1`](https://github.com/gradio-app/gradio/commit/a95fda1f85e80ce8423f4373bb238422b9b7aa32) - fix small bug when join src & api_prefix. Thanks @Chandler-Bing! + +## 1.4.3 + +### Fixes + +- [#9913](https://github.com/gradio-app/gradio/pull/9913) [`d81f430`](https://github.com/gradio-app/gradio/commit/d81f430fd50546001b76c0ae5fded32c6d3093f7) - fix: Fix filename stripping to preserve extensions. Thanks @TakaSoap! + +## 1.4.2 + +### Fixes + +- [#9754](https://github.com/gradio-app/gradio/pull/9754) [`36a5076`](https://github.com/gradio-app/gradio/commit/36a50769095081a0e77f04f513d47a2e9d4531ba) - Update client.py: raise error on 429 get_config. Thanks @Pendrokar! + +## 1.4.1 + +### Fixes + +- [#9678](https://github.com/gradio-app/gradio/pull/9678) [`a25a26e`](https://github.com/gradio-app/gradio/commit/a25a26e208c3f3675ba857a889553c7ccc95e866) - Fix: `file_types` checking bug. Thanks @jasongzy! + +## 1.4.0-beta.5 + +### Features + +- [#9589](https://github.com/gradio-app/gradio/pull/9589) [`477f45c`](https://github.com/gradio-app/gradio/commit/477f45cb43be957684eb392e3d62c09490c22391) - Only move files to the cache that have a meta key. Thanks @freddyaboulton! + +## 1.4.0-beta.4 + +### Features + +- [#9550](https://github.com/gradio-app/gradio/pull/9550) [`b0fedd7`](https://github.com/gradio-app/gradio/commit/b0fedd7ef718c0df797ec277db7e773543a70a4d) - Fix most flaky Python tests in `5.0-dev` branch. Thanks @abidlabs! +- [#9483](https://github.com/gradio-app/gradio/pull/9483) [`8dc7c12`](https://github.com/gradio-app/gradio/commit/8dc7c12389311b60efcde1b9d3e3668a34d2dc00) - Send Streaming data over Websocket if possible. Also support base64 output format for images. Thanks @freddyaboulton! +- [#9522](https://github.com/gradio-app/gradio/pull/9522) [`3b71ed2`](https://github.com/gradio-app/gradio/commit/3b71ed21b7e2ecb67eb68fb946d25565169cb4df) - Api info fix. Thanks @freddyaboulton! + +## 1.4.0-beta.3 + +### Fixes + +- [#9431](https://github.com/gradio-app/gradio/pull/9431) [`7065e11`](https://github.com/gradio-app/gradio/commit/7065e11e465fcdfe14688bd6ca2aeed0a25fcc36) - Check for `file_types` parameter in the backend. Thanks @dawoodkhan82! + +## 1.4.0-beta.2 + +### Features + +- [#9339](https://github.com/gradio-app/gradio/pull/9339) [`4c8c6f2`](https://github.com/gradio-app/gradio/commit/4c8c6f2fe603081941c5fdc43f48a0632b9f31ad) - Ssr part 2. Thanks @pngwn! + +## 1.4.0-beta.1 + +### Features + +- [#9200](https://github.com/gradio-app/gradio/pull/9200) [`2e179d3`](https://github.com/gradio-app/gradio/commit/2e179d35be6ed60a5a6bfc7303178d63e41781ad) - prefix api routes. Thanks @pngwn! + +## 1.4.0-beta.0 + +### Features + +- [#9140](https://github.com/gradio-app/gradio/pull/9140) [`c054ec8`](https://github.com/gradio-app/gradio/commit/c054ec85e49ab102b15afd305583ee394151d16c) - Drop python 3.8 and 3.9. Thanks @abidlabs! +- [#8941](https://github.com/gradio-app/gradio/pull/8941) [`97a7bf6`](https://github.com/gradio-app/gradio/commit/97a7bf66a79179d1b91a3199d68e5c11216ca500) - Streaming inputs for 5.0. Thanks @freddyaboulton! + +## 1.3.0 + +### Features + +- [#8968](https://github.com/gradio-app/gradio/pull/8968) [`38b3682`](https://github.com/gradio-app/gradio/commit/38b3682c3a9b40fb4070f665d94711c43c6fe40e) - Improvements to FRP client download and usage. Thanks @abidlabs! +- [#9059](https://github.com/gradio-app/gradio/pull/9059) [`981731a`](https://github.com/gradio-app/gradio/commit/981731acb7da3e78555abeb20f47f3a8a5f0d861) - Fix flaky tests and tests on Windows. Thanks @abidlabs! + +## 1.2.0 + +### Features + +- [#8862](https://github.com/gradio-app/gradio/pull/8862) [`ac132e3`](https://github.com/gradio-app/gradio/commit/ac132e3cbc8dbc7bec3d607d52bef347e90feb41) - Support the use of custom authentication mechanism, timeouts, and other `httpx` parameters in Python Client. Thanks @valgai! +- [#8948](https://github.com/gradio-app/gradio/pull/8948) [`f7fbd2c`](https://github.com/gradio-app/gradio/commit/f7fbd2c23795d97071296463779d41bd0e937164) - Bump websockets version max for gradio-client. Thanks @evanscho! + +## 1.1.1 + +### Features + +- [#8757](https://github.com/gradio-app/gradio/pull/8757) [`6073736`](https://github.com/gradio-app/gradio/commit/60737366517f48d1a37ffce15425783a2887f305) - Document `FileData` class in docs. Thanks @hannahblair! + +## 1.1.0 + +### Fixes + +- [#8505](https://github.com/gradio-app/gradio/pull/8505) [`2943d6d`](https://github.com/gradio-app/gradio/commit/2943d6d68847314885dc6c5c0247083116017ca0) - Add Timer component. Thanks @aliabid94! + +## 1.0.2 + +### Features + +- [#8516](https://github.com/gradio-app/gradio/pull/8516) [`de6aa2b`](https://github.com/gradio-app/gradio/commit/de6aa2b67668605b65ad92842b2c798afa2c6d8a) - Add helper classes to docs. Thanks @aliabd! + +## 1.0.1 + +### Features + +- [#8481](https://github.com/gradio-app/gradio/pull/8481) [`41a4493`](https://github.com/gradio-app/gradio/commit/41a449383a34b7d6e4c83cfbf61c222fd5501206) - fix client flaky tests. Thanks @abidlabs! + +## 1.0.0 + +### Highlights + +#### Clients 1.0 Launch! ([#8468](https://github.com/gradio-app/gradio/pull/8468) [`7cc0a0c`](https://github.com/gradio-app/gradio/commit/7cc0a0c1abea585c3f50ffb1ff78d2b08ddbdd92)) + +We're excited to unveil the first major release of the Gradio clients. +We've made it even easier to turn any Gradio application into a production endpoint thanks to the clients' **ergonomic**, **transparent**, and **portable** design. + +#### Ergonomic API 💆 + +**Stream From a Gradio app in 5 lines** + +Use the `submit` method to get a job you can iterate over: + +```python +from gradio_client import Client + +client = Client("gradio/llm_stream") + +for result in client.submit("What's the best UI framework in Python?"): + print(result) +``` + +```ts +import { Client } from "@gradio/client"; + +const client = await Client.connect("gradio/llm_stream") +const job = client.submit("/predict", {"text": "What's the best UI framework in Python?"}) + +for await (const msg of job) console.log(msg.data) +``` + +**Use the same keyword arguments as the app** + + +```python +from gradio_client import Client + +client = Client("http://127.0.0.1:7860/") +result = client.predict( + message="Hello!!", + system_prompt="You are helpful AI.", + tokens=10, + api_name="/chat" +) +print(result) +``` + +```ts +import { Client } from "@gradio/client"; + +const client = await Client.connect("http://127.0.0.1:7860/"); +const result = await client.predict("/chat", { + message: "Hello!!", + system_prompt: "Hello!!", + tokens: 10, +}); + +console.log(result.data); +``` + +**Better Error Messages** + +If something goes wrong in the upstream app, the client will raise the same exception as the app provided that `show_error=True` in the original app's `launch()` function, or it's a `gr.Error` exception. + +#### Transparent Design 🪟 + +Anything you can do in the UI, you can do with the client: +* 🔒 Authentication +* 🛑 Job Cancelling +* ℹ️ Access Queue Position and API +* 📕 View the API information + +Here's an example showing how to display the queue position of a pending job: + +```python +from gradio_client import Client + +client = Client("gradio/diffusion_model") + +job = client.submit("A cute cat") +while not job.done(): + status = job.status() + print(f"Current in position {status.rank} out of {status.queue_size}") +``` + +#### Portable Design ⛺️ + +The client can run from pretty much any python and javascript environment (node, deno, the browser, Service Workers). + +Here's an example using the client from a Flask server using gevent: + +```python +from gevent import monkey +monkey.patch_all() + +from gradio_client import Client +from flask import Flask, send_file +import time + +app = Flask(__name__) + +imageclient = Client("gradio/diffusion_model") + +@app.route("/gen") +def gen(): + result = imageclient.predict( + "A cute cat", + api_name="/predict" + ) + return send_file(result) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) +``` + +#### 1.0 Migration Guide and Breaking Changes + +**Python** +- The `serialize` argument of the `Client` class was removed. Has no effect. +- The `upload_files` argument of the `Client` was removed. +- All filepaths must be wrapped in the `handle_file` method. Example: +```python +from gradio_client import Client, handle_file + +client = Client("gradio/image_captioner") +client.predict(handle_file("cute_cat.jpg")) +``` +- The `output_dir` argument was removed. It is not specified in the `download_files` argument. + + +**Javascript** +The client has been redesigned entirely. It was refactored from a function into a class. An instance can now be constructed by awaiting the `connect` method. + +```js +const app = await Client.connect("gradio/whisper") +``` +The app variable has the same methods as the python class (`submit`, `predict`, `view_api`, `duplicate`). + + + +#### Additional Changes + +- [#8243](https://github.com/gradio-app/gradio/pull/8243) - Set orig_name in python client file uploads. +- [#8264](https://github.com/gradio-app/gradio/pull/8264) - Make exceptions in the Client more specific. +- [#8247](https://github.com/gradio-app/gradio/pull/8247) - Fix api recorder. +- [#8276](https://github.com/gradio-app/gradio/pull/8276) - Fix bug where client could not connect to apps that had self signed certificates. +- [#8245](https://github.com/gradio-app/gradio/pull/8245) - Cancel server progress from the python client. +- [#8200](https://github.com/gradio-app/gradio/pull/8200) - Support custom components in gr.load +- [#8182](https://github.com/gradio-app/gradio/pull/8182) - Convert sse calls in client from async to sync. +- [#7732](https://github.com/gradio-app/gradio/pull/7732) - Adds support for kwargs and default arguments in the python client, and improves how parameter information is displayed in the "view API" page. +- [#7888](https://github.com/gradio-app/gradio/pull/7888) - Cache view_api info in server and python client. +- [#7575](https://github.com/gradio-app/gradio/pull/7575) - Files should now be supplied as `file(...)` in the Client, and some fixes to `gr.load()` as well. +- [#8401](https://github.com/gradio-app/gradio/pull/8401) - Add CDN installation to JS docs. +- [#8299](https://github.com/gradio-app/gradio/pull/8299) - Allow JS Client to work with authenticated spaces 🍪. +- [#8408](https://github.com/gradio-app/gradio/pull/8408) - Connect heartbeat if state created in render. Also fix config cleanup bug #8407. +- [#8258](https://github.com/gradio-app/gradio/pull/8258) - Improve URL handling in JS Client. +- [#8322](https://github.com/gradio-app/gradio/pull/8322) - ensure the client correctly handles all binary data. +- [#8296](https://github.com/gradio-app/gradio/pull/8296) - always create a jwt when connecting to a space if a hf_token is present. +- [#8285](https://github.com/gradio-app/gradio/pull/8285) - use the correct query param to pass the jwt to the heartbeat event. +- [#8272](https://github.com/gradio-app/gradio/pull/8272) - ensure client works for private spaces. +- [#8197](https://github.com/gradio-app/gradio/pull/8197) - Add support for passing keyword args to `data` in JS client. +- [#8252](https://github.com/gradio-app/gradio/pull/8252) - Client node fix. +- [#8209](https://github.com/gradio-app/gradio/pull/8209) - Rename `eventSource_Factory` and `fetch_implementation`. +- [#8109](https://github.com/gradio-app/gradio/pull/8109) - Implement JS Client tests. +- [#8211](https://github.com/gradio-app/gradio/pull/8211) - remove redundant event source logic. +- [#8179](https://github.com/gradio-app/gradio/pull/8179) - rework upload to be a class method + pass client into each component. +- [#8181](https://github.com/gradio-app/gradio/pull/8181) - Ensure connectivity to private HF spaces with SSE protocol. +- [#8169](https://github.com/gradio-app/gradio/pull/8169) - Only connect to heartbeat if needed. +- [#8118](https://github.com/gradio-app/gradio/pull/8118) - Add eventsource polyfill for Node.js and browser environments. +- [#7646](https://github.com/gradio-app/gradio/pull/7646) - Refactor JS Client. +- [#7974](https://github.com/gradio-app/gradio/pull/7974) - Fix heartbeat in the js client to be Lite compatible. +- [#7926](https://github.com/gradio-app/gradio/pull/7926) - Fixes streaming event race condition. + + Thanks @freddyaboulton! + +### Features + +- [#8444](https://github.com/gradio-app/gradio/pull/8444) [`2cd02ff`](https://github.com/gradio-app/gradio/commit/2cd02ff3b7c57cd69635d111ff25643eba30b9b0) - Remove deprecated parameters from Python Client. Thanks @abidlabs! + +## 0.17.0 + +### Features + +- [#8243](https://github.com/gradio-app/gradio/pull/8243) [`55f664f`](https://github.com/gradio-app/gradio/commit/55f664f2979a49acc29a73cde16c6ebdfcc91db2) - Add event listener support to render blocks. Thanks @aliabid94! +- [#8409](https://github.com/gradio-app/gradio/pull/8409) [`8028c33`](https://github.com/gradio-app/gradio/commit/8028c33bbc5a324a5e9e8b28906443db28683d79) - Render decorator documentation. Thanks @aliabid94! + +### Fixes + +- [#8371](https://github.com/gradio-app/gradio/pull/8371) [`a373b0e`](https://github.com/gradio-app/gradio/commit/a373b0edd36613a9a6a25a1a2893edd6533a7291) - Set orig_name in python client file uploads. Thanks @freddyaboulton! + +## 0.16.4 + +### Fixes + +- [#8247](https://github.com/gradio-app/gradio/pull/8247) [`8f46556`](https://github.com/gradio-app/gradio/commit/8f46556b38e35cffbadac74ff80445dceea3bcf5) - Fix api recorder. Thanks @abidlabs! + +## 0.16.3 + +### Features + +- [#8264](https://github.com/gradio-app/gradio/pull/8264) [`a9e1a8a`](https://github.com/gradio-app/gradio/commit/a9e1a8ac5633c5336fea1c63d7f66a9883e7e6e1) - Make exceptions in the Client more specific. Thanks @abidlabs! + +### Fixes + +- [#8276](https://github.com/gradio-app/gradio/pull/8276) [`0bf3d1a`](https://github.com/gradio-app/gradio/commit/0bf3d1a992db2753c1a55452b569027190f26ef6) - Fix bug where client could not connect to apps that had self signed certificates. Thanks @freddyaboulton! + +## 0.16.2 + +### Fixes + +- [#8245](https://github.com/gradio-app/gradio/pull/8245) [`c562a3d`](https://github.com/gradio-app/gradio/commit/c562a3d9a440c8f94ca070bd07b8d4121d6ab7b3) - Cancel server progress from the python client. Thanks @freddyaboulton! + +## 0.16.1 + +### Highlights + +#### Support custom components in gr.load ([#8200](https://github.com/gradio-app/gradio/pull/8200) [`72039be`](https://github.com/gradio-app/gradio/commit/72039be93acda856d92ceac7f21f1ec1a054fae2)) + +It is now possible to load a demo with a custom component with `gr.load`. + +The custom component must be installed in your system and imported in your python session. + +```python +import gradio as gr +import gradio_pdf + +demo = gr.load("freddyaboulton/gradiopdf", src="spaces") + +if __name__ == "__main__": + demo.launch() +``` + +image + + Thanks @freddyaboulton! + +### Fixes + +- [#8182](https://github.com/gradio-app/gradio/pull/8182) [`39791eb`](https://github.com/gradio-app/gradio/commit/39791eb186d3a4ce82c8c27979a28311c37a4067) - Convert sse calls in client from async to sync. Thanks @abidlabs! + +## 0.16.0 + +### Highlights + +#### Setting File Upload Limits ([#7909](https://github.com/gradio-app/gradio/pull/7909) [`2afca65`](https://github.com/gradio-app/gradio/commit/2afca6541912b37dc84f447c7ad4af21607d7c72)) + +We have added a `max_file_size` size parameter to `launch()` that limits to size of files uploaded to the server. This limit applies to each individual file. This parameter can be specified as a string or an integer (corresponding to the size in bytes). + +The following code snippet sets a max file size of 5 megabytes. + +```python +import gradio as gr + +demo = gr.Interface(lambda x: x, "image", "image") + +demo.launch(max_file_size="5mb") +# or +demo.launch(max_file_size=5 * gr.FileSize.MB) +``` + +![max_file_size_upload](https://github.com/gradio-app/gradio/assets/41651716/7547330c-a082-4901-a291-3f150a197e45) + + +#### Error states can now be cleared + +When a component encounters an error, the error state shown in the UI can now be cleared by clicking on the `x` icon in the top right of the component. This applies to all types of errors, whether it's raised in the UI or the server. + +![error_modal_calculator](https://github.com/gradio-app/gradio/assets/41651716/16cb071c-accd-45a6-9c18-0dea27d4bd98) + + Thanks @freddyaboulton! + +### Features + +- [#8100](https://github.com/gradio-app/gradio/pull/8100) [`cbdfbdf`](https://github.com/gradio-app/gradio/commit/cbdfbdfc973fa67665911fb5d8cb005a025b0e58) - upgrade `ruff` test dependency to `ruff==0.4.1`. Thanks @abidlabs! + +## 0.15.1 + +### Features + +- [#7850](https://github.com/gradio-app/gradio/pull/7850) [`2bae1cf`](https://github.com/gradio-app/gradio/commit/2bae1cfbd41ed8ae3eea031a64899611a22a1821) - Adds an "API Recorder" to the view API page, some internal methods have been made async. Thanks @abidlabs! + +## 0.15.0 + +### Highlights + +#### Automatically delete state after user has disconnected from the webpage ([#7829](https://github.com/gradio-app/gradio/pull/7829) [`6a4bf7a`](https://github.com/gradio-app/gradio/commit/6a4bf7abe29059dbdc6a342e0366fdaa2e4120ee)) + +Gradio now automatically deletes `gr.State` variables stored in the server's RAM when users close their browser tab. +The deletion will happen 60 minutes after the server detected a disconnect from the user's browser. +If the user connects again in that timeframe, their state will not be deleted. + +Additionally, Gradio now includes a `Blocks.unload()` event, allowing you to run arbitrary cleanup functions when users disconnect (this does not have a 60 minute delay). +You can think of the `unload` event as the opposite of the `load` event. + + +```python +with gr.Blocks() as demo: + gr.Markdown( +"""# State Cleanup Demo +🖼️ Images are saved in a user-specific directory and deleted when the users closes the page via demo.unload. +""") + with gr.Row(): + with gr.Column(scale=1): + with gr.Row(): + img = gr.Image(label="Generated Image", height=300, width=300) + with gr.Row(): + gen = gr.Button(value="Generate") + with gr.Row(): + history = gr.Gallery(label="Previous Generations", height=500, columns=10) + state = gr.State(value=[], delete_callback=lambda v: print("STATE DELETED")) + + demo.load(generate_random_img, [state], [img, state, history]) + gen.click(generate_random_img, [state], [img, state, history]) + demo.unload(delete_directory) + + +demo.launch(auth=lambda user,pwd: True, + auth_message="Enter any username and password to continue") +``` + + Thanks @freddyaboulton! + +### Fixes + +- [#7888](https://github.com/gradio-app/gradio/pull/7888) [`946487c`](https://github.com/gradio-app/gradio/commit/946487cf8e477cbf8d6fad4e772ff574a21782c3) - Cache view_api info in server and python client. Thanks @freddyaboulton! + +## 0.14.0 + +### Features + +- [#7800](https://github.com/gradio-app/gradio/pull/7800) [`b0a3ea9`](https://github.com/gradio-app/gradio/commit/b0a3ea951c06d4f3ff2755b567629fe988a3e30d) - Small fix to client.view_api() in the case of default file values. Thanks @abidlabs! +- [#7732](https://github.com/gradio-app/gradio/pull/7732) [`2efb05e`](https://github.com/gradio-app/gradio/commit/2efb05ed99a8a3575aab0a6c14a8d8b91f4e9ed7) - Adds support for kwargs and default arguments in the python client, and improves how parameter information is displayed in the "view API" page. Thanks @abidlabs! + +## 0.13.0 + +### Features + +- [#7691](https://github.com/gradio-app/gradio/pull/7691) [`84f81fe`](https://github.com/gradio-app/gradio/commit/84f81fec9287b041203a141bbf2852720f7d199c) - Closing stream from the backend. Thanks @aliabid94! + +### Fixes + +- [#7718](https://github.com/gradio-app/gradio/pull/7718) [`6390d0b`](https://github.com/gradio-app/gradio/commit/6390d0bf6c2be0aefa56102dd029f25161bfebc3) - Add support for python client connecting to gradio apps running with self-signed SSL certificates. Thanks @abidlabs! +- [#7706](https://github.com/gradio-app/gradio/pull/7706) [`bc61ff6`](https://github.com/gradio-app/gradio/commit/bc61ff6b1603eedf3111f1b5c3d2751629902d98) - Several fixes to `gr.load`. Thanks @abidlabs! + +## 0.12.0 + +### Fixes + +- [#7575](https://github.com/gradio-app/gradio/pull/7575) [`d0688b3`](https://github.com/gradio-app/gradio/commit/d0688b3c25feabb4fc7dfa0ab86086b3af7eb337) - Files should now be supplied as `file(...)` in the Client, and some fixes to `gr.load()` as well. Thanks @abidlabs! +- [#7618](https://github.com/gradio-app/gradio/pull/7618) [`0ae1e44`](https://github.com/gradio-app/gradio/commit/0ae1e4486c06e06bb7a4bad45d58d14f1f8d1b94) - Control which files get moved to cache with gr.set_static_paths. Thanks @freddyaboulton! + +## 0.11.0 + +### Features + +- [#7407](https://github.com/gradio-app/gradio/pull/7407) [`375bfd2`](https://github.com/gradio-app/gradio/commit/375bfd28d2def576b4e1c12e0a60127b7419e826) - Fix server_messages.py to use the patched BaseModel class for Wasm env. Thanks [@aliabid94](https://github.com/aliabid94)! + +### Fixes + +- [#7555](https://github.com/gradio-app/gradio/pull/7555) [`fc4c2db`](https://github.com/gradio-app/gradio/commit/fc4c2dbd994c49e37296978da1cb85e424080d1c) - Allow Python Client to upload/download files when connecting to Gradio apps with auth enabled. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.10.1 + +### Features + +- [#7495](https://github.com/gradio-app/gradio/pull/7495) [`ddd4d3e`](https://github.com/gradio-app/gradio/commit/ddd4d3e4d3883fb7540d1df240fb08202fc77705) - Enable Ruff S101. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#7443](https://github.com/gradio-app/gradio/pull/7443) [`b7a97f2`](https://github.com/gradio-app/gradio/commit/b7a97f29b84a72678a717db03d2932ed6caae6ce) - Update `httpx` to `httpx>=0.24.1` in `requirements.txt`. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.10.0 + +### Features + +- [#7183](https://github.com/gradio-app/gradio/pull/7183) [`49d9c48`](https://github.com/gradio-app/gradio/commit/49d9c48537aa706bf72628e3640389470138bdc6) - [WIP] Refactor file normalization to be in the backend and remove it from the frontend of each component. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#7377](https://github.com/gradio-app/gradio/pull/7377) [`6dfd40f`](https://github.com/gradio-app/gradio/commit/6dfd40fc6b2fa461490d2370ab91fcda7e07c0da) - Make set_documentation_group a no-op. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#7334](https://github.com/gradio-app/gradio/pull/7334) [`b95d0d0`](https://github.com/gradio-app/gradio/commit/b95d0d043c739926af986e573200af92732bbc01) - Allow setting custom headers in Python Client. Thanks [@abidlabs](https://github.com/abidlabs)! + +### Fixes + +- [#7350](https://github.com/gradio-app/gradio/pull/7350) [`7302a6e`](https://github.com/gradio-app/gradio/commit/7302a6e151dac553c17833be64d4639ee4cf97aa) - Fix `gr.load` for file-based Spaces. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.9.0 + +### Features + +- [#7062](https://github.com/gradio-app/gradio/pull/7062) [`0fddd0f`](https://github.com/gradio-app/gradio/commit/0fddd0f971761bff3ef6ccc7ab9deb1891cd80d0) - Determine documentation group automatically. Thanks [@akx](https://github.com/akx)! +- [#7102](https://github.com/gradio-app/gradio/pull/7102) [`68a54a7`](https://github.com/gradio-app/gradio/commit/68a54a7a310d8d7072fdae930bf1cfdf12c45a7f) - Improve chatbot streaming performance with diffs. Thanks [@aliabid94](https://github.com/aliabid94)!/n Note that this PR changes the API format for generator functions, which would be a breaking change for any clients reading the EventStream directly +- [#7116](https://github.com/gradio-app/gradio/pull/7116) [`3c8c4ac`](https://github.com/gradio-app/gradio/commit/3c8c4ac2db284e1cb503c397205a79a6dcc27e23) - Document the `gr.ParamViewer` component, and fix component preprocessing/postprocessing docstrings. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#7061](https://github.com/gradio-app/gradio/pull/7061) [`05d8a3c`](https://github.com/gradio-app/gradio/commit/05d8a3c8030b733bd47250f5db6f89f230f9a707) - Update ruff to 0.1.13, enable more rules, fix issues. Thanks [@akx](https://github.com/akx)! + +### Fixes + +- [#7178](https://github.com/gradio-app/gradio/pull/7178) [`9f23b0b`](https://github.com/gradio-app/gradio/commit/9f23b0bc54b4ef63c056b309370df52ec2c2a43c) - Optimize client view_api method. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#7322](https://github.com/gradio-app/gradio/pull/7322) [`b25e95e`](https://github.com/gradio-app/gradio/commit/b25e95e164e80d66203ef71ce6bdb67ceb6b24df) - Fix `processing_utils.save_url_to_cache()` to follow redirects when accessing the URL. Thanks [@whitphx](https://github.com/whitphx)! + +## 0.8.1 + +### Features + +- [#7075](https://github.com/gradio-app/gradio/pull/7075) [`1fc8a94`](https://github.com/gradio-app/gradio/commit/1fc8a941384775f587a6ef30365960f43353cb0d) - fix lint. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#7054](https://github.com/gradio-app/gradio/pull/7054) [`64c65d8`](https://github.com/gradio-app/gradio/commit/64c65d821983961111297a969946d87e2fc4105d) - Add encoding to open/writing files on the deploy_discord function. Thanks [@WilliamHarer](https://github.com/WilliamHarer)! + +## 0.8.0 + +### Fixes + +- [#6846](https://github.com/gradio-app/gradio/pull/6846) [`48d6534`](https://github.com/gradio-app/gradio/commit/48d6534b40f80e7e70a4061f97d9f2e23ba77fe1) - Add `show_api` parameter to events, and fix `gr.load()`. Also makes some minor improvements to the "view API" page when running on Spaces. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#6767](https://github.com/gradio-app/gradio/pull/6767) [`7bb561a`](https://github.com/gradio-app/gradio/commit/7bb561a294ca41d1044927cb34d8645c4175cae0) - Rewriting parts of the README and getting started guides for 4.0. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.7.3 + +### Fixes + +- [#6693](https://github.com/gradio-app/gradio/pull/6693) [`34f9431`](https://github.com/gradio-app/gradio/commit/34f943101bf7dd6b8a8974a6131c1ed7c4a0dac0) - Python client properly handles hearbeat and log messages. Also handles responses longer than 65k. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.7.2 + +### Features + +- [#6598](https://github.com/gradio-app/gradio/pull/6598) [`7cbf96e`](https://github.com/gradio-app/gradio/commit/7cbf96e0bdd12db7ecac7bf99694df0a912e5864) - Issue 5245: consolidate usage of requests and httpx. Thanks [@cswamy](https://github.com/cswamy)! +- [#6704](https://github.com/gradio-app/gradio/pull/6704) [`24e0481`](https://github.com/gradio-app/gradio/commit/24e048196e8f7bd309ef5c597d4ffc6ca4ed55d0) - Hotfix: update `huggingface_hub` dependency version. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#6543](https://github.com/gradio-app/gradio/pull/6543) [`8a70e83`](https://github.com/gradio-app/gradio/commit/8a70e83db9c7751b46058cdd2514e6bddeef6210) - switch from black to ruff formatter. Thanks [@DarhkVoyd](https://github.com/DarhkVoyd)! + +### Fixes + +- [#6556](https://github.com/gradio-app/gradio/pull/6556) [`d76bcaa`](https://github.com/gradio-app/gradio/commit/d76bcaaaf0734aaf49a680f94ea9d4d22a602e70) - Fix api event drops. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.7.1 + +### Fixes + +- [#6602](https://github.com/gradio-app/gradio/pull/6602) [`b8034a1`](https://github.com/gradio-app/gradio/commit/b8034a1e72c3aac649ee0ad9178ffdbaaa60fc61) - Fix: Gradio Client work with private Spaces. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.7.0 + +### Features + +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Add json schema unit tests. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Image v4. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Custom components. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Swap websockets for SSE. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.7.0-beta.2 + +### Features + +- [#6094](https://github.com/gradio-app/gradio/pull/6094) [`c476bd5a5`](https://github.com/gradio-app/gradio/commit/c476bd5a5b70836163b9c69bf4bfe068b17fbe13) - Image v4. Thanks [@pngwn](https://github.com/pngwn)! +- [#6069](https://github.com/gradio-app/gradio/pull/6069) [`bf127e124`](https://github.com/gradio-app/gradio/commit/bf127e1241a41401e144874ea468dff8474eb505) - Swap websockets for SSE. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.7.0-beta.1 + +### Features + +- [#6082](https://github.com/gradio-app/gradio/pull/6082) [`037e5af33`](https://github.com/gradio-app/gradio/commit/037e5af3363c5b321b95efc955ee8d6ec0f4504e) - WIP: Fix docs. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5970](https://github.com/gradio-app/gradio/pull/5970) [`0c571c044`](https://github.com/gradio-app/gradio/commit/0c571c044035989d6fe33fc01fee63d1780635cb) - Add json schema unit tests. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6073](https://github.com/gradio-app/gradio/pull/6073) [`abff6fb75`](https://github.com/gradio-app/gradio/commit/abff6fb758bd310053a23c938bf1dd8fbdc5d333) - Fix remaining xfail tests in backend. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.7.0-beta.0 + +### Features + +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`85ba6de13`](https://github.com/gradio-app/gradio/commit/85ba6de136a45b3e92c74e410bb27e3cbe7138d7) - Simplify how files are handled in components in 4.0. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`85ba6de13`](https://github.com/gradio-app/gradio/commit/85ba6de136a45b3e92c74e410bb27e3cbe7138d7) - Rename gradio_component to gradio component. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.6.1 + +### Fixes + +- [#5811](https://github.com/gradio-app/gradio/pull/5811) [`1d5b15a2d`](https://github.com/gradio-app/gradio/commit/1d5b15a2d24387154f2cfb40a36de25b331471d3) - Assert refactor in external.py. Thanks [@harry-urek](https://github.com/harry-urek)! + +## 0.6.0 + +### Highlights + +#### new `FileExplorer` component ([#5672](https://github.com/gradio-app/gradio/pull/5672) [`e4a307ed6`](https://github.com/gradio-app/gradio/commit/e4a307ed6cde3bbdf4ff2f17655739addeec941e)) + +Thanks to a new capability that allows components to communicate directly with the server _without_ passing data via the value, we have created a new `FileExplorer` component. + +This component allows you to populate the explorer by passing a glob, but only provides the selected file(s) in your prediction function. + +Users can then navigate the virtual filesystem and select files which will be accessible in your predict function. This component will allow developers to build more complex spaces, with more flexible input options. + +![output](https://github.com/pngwn/MDsveX/assets/12937446/ef108f0b-0e84-4292-9984-9dc66b3e144d) + +For more information check the [`FileExplorer` documentation](https://gradio.app/docs/fileexplorer). + + Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.5.3 + +### Features + +- [#5721](https://github.com/gradio-app/gradio/pull/5721) [`84e03fe50`](https://github.com/gradio-app/gradio/commit/84e03fe506e08f1f81bac6d504c9fba7924f2d93) - Adds copy buttons to website, and better descriptions to API Docs. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.5.2 + +### Features + +- [#5653](https://github.com/gradio-app/gradio/pull/5653) [`ea0e00b20`](https://github.com/gradio-app/gradio/commit/ea0e00b207b4b90a10e9d054c4202d4e705a29ba) - Prevent Clients from accessing API endpoints that set `api_name=False`. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.5.1 + +### Features + +- [#5514](https://github.com/gradio-app/gradio/pull/5514) [`52f783175`](https://github.com/gradio-app/gradio/commit/52f7831751b432411e109bd41add4ab286023a8e) - refactor: Use package.json for version management. Thanks [@DarhkVoyd](https://github.com/DarhkVoyd)! + +## 0.5.0 + +### Highlights + +#### Enable streaming audio in python client ([#5248](https://github.com/gradio-app/gradio/pull/5248) [`390624d8`](https://github.com/gradio-app/gradio/commit/390624d8ad2b1308a5bf8384435fd0db98d8e29e)) + +The `gradio_client` now supports streaming file outputs 🌊 + +No new syntax! Connect to a gradio demo that supports streaming file outputs and call `predict` or `submit` as you normally would. + +```python +import gradio_client as grc +client = grc.Client("gradio/stream_audio_out") + +# Get the entire generated audio as a local file +client.predict("/Users/freddy/Pictures/bark_demo.mp4", api_name="/predict") + +job = client.submit("/Users/freddy/Pictures/bark_demo.mp4", api_name="/predict") + +# Get the entire generated audio as a local file +job.result() + +# Each individual chunk +job.outputs() +``` + + Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Fixes + +- [#5295](https://github.com/gradio-app/gradio/pull/5295) [`7b8fa8aa`](https://github.com/gradio-app/gradio/commit/7b8fa8aa58f95f5046b9add64b40368bd3f1b700) - Allow caching examples with streamed output. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 0.4.0 + +### Highlights + +#### Client.predict will now return the final output for streaming endpoints ([#5057](https://github.com/gradio-app/gradio/pull/5057) [`35856f8b`](https://github.com/gradio-app/gradio/commit/35856f8b54548cae7bd3b8d6a4de69e1748283b2)) + +### This is a breaking change (for gradio_client only)! + +Previously, `Client.predict` would only return the first output of an endpoint that streamed results. This was causing confusion for developers that wanted to call these streaming demos via the client. + +We realize that developers using the client don't know the internals of whether a demo streams or not, so we're changing the behavior of predict to match developer expectations. + +Using `Client.predict` will now return the final output of a streaming endpoint. This will make it even easier to use gradio apps via the client. + + Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Features + +- [#5076](https://github.com/gradio-app/gradio/pull/5076) [`2745075a`](https://github.com/gradio-app/gradio/commit/2745075a26f80e0e16863d483401ff1b6c5ada7a) - Add deploy_discord to docs. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Fixes + +- [#5061](https://github.com/gradio-app/gradio/pull/5061) [`136adc9c`](https://github.com/gradio-app/gradio/commit/136adc9ccb23e5cb4d02d2e88f23f0b850041f98) - Ensure `gradio_client` is backwards compatible with `gradio==3.24.1`. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.3.0 + +### Highlights + +#### Create Discord Bots from Gradio Apps 🤖 ([#4960](https://github.com/gradio-app/gradio/pull/4960) [`46e4ef67`](https://github.com/gradio-app/gradio/commit/46e4ef67d287dd68a91473b73172b29cbad064bc)) + +We're excited to announce that Gradio can now automatically create a discord bot from any `gr.ChatInterface` app. + +It's as easy as importing `gradio_client`, connecting to the app, and calling `deploy_discord`! + +_🦙 Turning Llama 2 70b into a discord bot 🦙_ + +```python +import gradio_client as grc +grc.Client("ysharma/Explore_llamav2_with_TGI").deploy_discord(to_id="llama2-70b-discord-bot") +``` + + + +#### Getting started with template spaces + +To help get you started, we have created an organization on Hugging Face called [gradio-discord-bots](https://huggingface.co/gradio-discord-bots) with template spaces you can use to turn state of the art LLMs powered by Gradio to discord bots. + +Currently we have template spaces for: + +- [Llama-2-70b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/Llama-2-70b-chat-hf) powered by a FREE Hugging Face Inference Endpoint! +- [Llama-2-13b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/Llama-2-13b-chat-hf) powered by Hugging Face Inference Endpoints. +- [Llama-2-13b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/llama-2-13b-chat-transformers) powered by Hugging Face transformers. +- [falcon-7b-instruct](https://huggingface.co/spaces/gradio-discord-bots/falcon-7b-instruct) powered by Hugging Face Inference Endpoints. +- [gpt-3.5-turbo](https://huggingface.co/spaces/gradio-discord-bots/gpt-35-turbo), powered by openai. Requires an OpenAI key. + +But once again, you can deploy ANY `gr.ChatInterface` app exposed on the internet! So don't hesitate to try it on your own Chatbots. + +❗️ Additional Note ❗️: Technically, any gradio app that exposes an api route that takes in a single string and outputs a single string can be deployed to discord. But `gr.ChatInterface` apps naturally lend themselves to discord's chat functionality so we suggest you start with those. + +Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### New Features: + +- Endpoints that return layout components are now properly handled in the `submit` and `view_api` methods. Output layout components are not returned by the API but all other components are (excluding `gr.State`). By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4871](https://github.com/gradio-app/gradio/pull/4871) + +### Bug Fixes: + +No changes to highlight + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +# 0.2.9 + +### New Features: + +No changes to highlight + +### Bug Fixes: + +- Fix bug determining the api name when a demo has `api_name=False` by [@freddyboulton](https://github.com/freddyaboulton) in [PR 4886](https://github.com/gradio-app/gradio/pull/4886) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Pinned dependencies to major versions to reduce the likelihood of a broken `gradio_client` due to changes in downstream dependencies by [@abidlabs](https://github.com/abidlabs) in [PR 4885](https://github.com/gradio-app/gradio/pull/4885) + +# 0.2.8 + +### New Features: + +- Support loading gradio apps where `api_name=False` by [@abidlabs](https://github.com/abidlabs) in [PR 4683](https://github.com/gradio-app/gradio/pull/4683) + +### Bug Fixes: + +- Fix bug where space duplication would error if the demo has cpu-basic hardware by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4583](https://github.com/gradio-app/gradio/pull/4583) +- Fixes and optimizations to URL/download functions by [@akx](https://github.com/akx) in [PR 4695](https://github.com/gradio-app/gradio/pull/4695) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +# 0.2.7 + +### New Features: + +- The output directory for files downloaded via the Client can now be set by the `output_dir` parameter in `Client` by [@abidlabs](https://github.com/abidlabs) in [PR 4501](https://github.com/gradio-app/gradio/pull/4501) + +### Bug Fixes: + +- The output directory for files downloaded via the Client are now set to a temporary directory by default (instead of the working directory in some cases) by [@abidlabs](https://github.com/abidlabs) in [PR 4501](https://github.com/gradio-app/gradio/pull/4501) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +# 0.2.6 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fixed bug file deserialization didn't preserve all file extensions by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4440](https://github.com/gradio-app/gradio/pull/4440) +- Fixed bug where mounted apps could not be called via the client by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4435](https://github.com/gradio-app/gradio/pull/4435) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +# 0.2.5 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fixes parameter names not showing underscores by [@abidlabs](https://github.com/abidlabs) in [PR 4230](https://github.com/gradio-app/gradio/pull/4230) +- Fixes issue in which state was not handled correctly if `serialize=False` by [@abidlabs](https://github.com/abidlabs) in [PR 4230](https://github.com/gradio-app/gradio/pull/4230) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +# 0.2.4 + +### Bug Fixes: + +- Fixes missing serialization classes for several components: `Barplot`, `Lineplot`, `Scatterplot`, `AnnotatedImage`, `Interpretation` by [@abidlabs](https://github.com/abidlabs) in [PR 4167](https://github.com/gradio-app/gradio/pull/4167) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +# 0.2.3 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fix example inputs for `gr.File(file_count='multiple')` output components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4153](https://github.com/gradio-app/gradio/pull/4153) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +# 0.2.2 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Only send request to `/info` route if demo version is above `3.28.3` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4109](https://github.com/gradio-app/gradio/pull/4109) + +### Other Changes: + +- Fix bug in test from gradio 3.29.0 refactor by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4138](https://github.com/gradio-app/gradio/pull/4138) + +### Breaking Changes: + +No changes to highlight. + +# 0.2.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +Removes extraneous `State` component info from the `Client.view_api()` method by [@abidlabs](https://github.com/freddyaboulton) in [PR 4107](https://github.com/gradio-app/gradio/pull/4107) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +Separates flaky tests from non-flaky tests by [@abidlabs](https://github.com/freddyaboulton) in [PR 4107](https://github.com/gradio-app/gradio/pull/4107) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +# 0.1.4 + +### New Features: + +- Progress Updates from `gr.Progress()` can be accessed via `job.status().progress_data` by @freddyaboulton](https://github.com/freddyaboulton) in [PR 3924](https://github.com/gradio-app/gradio/pull/3924) + +### Bug Fixes: + +- Fixed bug where unnamed routes where displayed with `api_name` instead of `fn_index` in `view_api` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3972](https://github.com/gradio-app/gradio/pull/3972) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +# 0.1.3 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fixed bug where `Video` components in latest gradio were not able to be deserialized by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3860](https://github.com/gradio-app/gradio/pull/3860) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +# 0.1.2 + +First public release of the Gradio Client library! The `gradio_client` Python library that makes it very easy to use any Gradio app as an API. + +As an example, consider this [Hugging Face Space that transcribes audio files](https://huggingface.co/spaces/abidlabs/whisper) that are recorded from the microphone. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/whisper-screenshot.jpg) + +Using the `gradio_client` library, we can easily use the Gradio as an API to transcribe audio files programmatically. + +Here's the entire code to do it: + +```python +from gradio_client import Client + +client = Client("abidlabs/whisper") +client.predict("audio_sample.wav") + +>> "This is a test of the whisper speech recognition model." +``` + +Read more about how to use the `gradio_client` library here: https://gradio.app/getting-started-with-the-python-client/ \ No newline at end of file diff --git a/client/python/gradio_client/__init__.py b/client/python/gradio_client/__init__.py new file mode 100644 index 0000000..037264e --- /dev/null +++ b/client/python/gradio_client/__init__.py @@ -0,0 +1,11 @@ +from gradio_client.client import Client +from gradio_client.data_classes import FileData +from gradio_client.utils import __version__, file, handle_file + +__all__ = [ + "Client", + "file", + "handle_file", + "FileData", + "__version__", +] diff --git a/client/python/gradio_client/client.py b/client/python/gradio_client/client.py new file mode 100644 index 0000000..59e71ac --- /dev/null +++ b/client/python/gradio_client/client.py @@ -0,0 +1,1611 @@ +"""The main Client class for the Python client.""" + +from __future__ import annotations + +import asyncio +import concurrent.futures +import hashlib +import json +import math +import os +import re +import secrets +import shutil +import tempfile +import threading +import time +import urllib.parse +import uuid +import warnings +from collections import deque +from collections.abc import AsyncGenerator, Callable +from concurrent.futures import Future +from contextvars import copy_context +from dataclasses import dataclass +from datetime import datetime +from functools import partial +from pathlib import Path +from threading import Lock +from typing import Any, Literal + +import httpx +import huggingface_hub +from huggingface_hub import SpaceHardware, SpaceStage +from huggingface_hub.utils import ( + RepositoryNotFoundError, + build_hf_headers, + send_telemetry, +) +from packaging import version + +from gradio_client import utils +from gradio_client.data_classes import ParameterInfo +from gradio_client.documentation import document +from gradio_client.exceptions import AppError, AuthenticationError, ValidationError +from gradio_client.utils import ( + Communicator, + JobStatus, + Message, + QueueError, + ServerMessage, + Status, + StatusUpdate, + Update, +) + +DEFAULT_TEMP_DIR = os.environ.get("GRADIO_TEMP_DIR") or str( + Path(tempfile.gettempdir()) / "gradio" +) + + +@document("predict", "submit", "view_api", "duplicate") +class Client: + """ + The main Client class for the Python client. This class is used to connect to a remote Gradio app and call its API endpoints. + + Example: + from gradio_client import Client + + client = Client("abidlabs/whisper-large-v2") # connecting to a Hugging Face Space + client.predict("test.mp4", api_name="/predict") + >> What a nice recording! # returns the result of the remote API call + + client = Client("https://bec81a83-5b5c-471e.gradio.live") # connecting to a temporary Gradio share URL + job = client.submit("hello", api_name="/predict") # runs the prediction in a background thread + job.result() + >> 49 # returns the result of the remote API call (blocking call) + """ + + def __init__( + self, + src: str, + token: str | None = None, + max_workers: int = 40, + verbose: bool = True, + auth: tuple[str, str] | None = None, + httpx_kwargs: dict[str, Any] | None = None, + *, + headers: dict[str, str] | None = None, + download_files: str | Path | Literal[False] = DEFAULT_TEMP_DIR, + ssl_verify: bool = True, + _skip_components: bool = True, # internal parameter to skip values certain components (e.g. State) that do not need to be displayed to users. + analytics_enabled: bool = True, + ): + """ + Parameters: + src: either the name of the Hugging Face Space to load, (e.g. "abidlabs/whisper-large-v2") or the full URL (including "http" or "https") of the hosted Gradio app to load (e.g. "http://mydomain.com/app" or "https://bec81a83-5b5c-471e.gradio.live/"). + token: optional Hugging Face token to use to access private Spaces. By default, the locally saved token is used if there is one. Find your tokens here: https://huggingface.co/settings/tokens. + max_workers: maximum number of thread workers that can be used to make requests to the remote Gradio app simultaneously. + verbose: whether the client should print statements to the console. + headers: additional headers to send to the remote Gradio app on every request. By default only the HF authorization and user-agent headers are sent. This parameter will override the default headers if they have the same keys. + download_files: directory where the client should download output files on the local machine from the remote API. By default, uses the value of the GRADIO_TEMP_DIR environment variable which, if not set by the user, is a temporary directory on your machine. If False, the client does not download files and returns a FileData dataclass object with the filepath on the remote machine instead. + ssl_verify: if False, skips certificate validation which allows the client to connect to Gradio apps that are using self-signed certificates. + httpx_kwargs: additional keyword arguments to pass to `httpx.Client`, `httpx.stream`, `httpx.get` and `httpx.post`. This can be used to set timeouts, proxies, http auth, etc. + analytics_enabled: Whether to allow basic telemetry. If None, will use GRADIO_ANALYTICS_ENABLED environment variable or default to True. + """ + self.verbose = verbose + self.token = token + self.download_files = download_files + self._skip_components = _skip_components + self.headers = build_hf_headers( + token=token, + library_name="gradio_client", + library_version=utils.__version__, + ) + if "authorization" in self.headers: + self.headers["x-hf-authorization"] = self.headers["authorization"] + del self.headers["authorization"] + if headers: + self.headers.update(headers) + self.ssl_verify = ssl_verify + self.space_id = None + self.httpx_kwargs = {} if httpx_kwargs is None else httpx_kwargs + self.cookies: dict[str, str] = dict( + (self.httpx_kwargs.pop("cookies", {})) or {} + ) + if isinstance(self.download_files, (str, Path)): + if not os.path.exists(self.download_files): + os.makedirs(self.download_files, exist_ok=True) + if not os.path.isdir(self.download_files): + raise ValueError(f"Path: {self.download_files} is not a directory.") + self.output_dir = str(self.download_files) + else: + self.output_dir = DEFAULT_TEMP_DIR + + if src.startswith("http://") or src.startswith("https://"): + _src = src if src.endswith("/") else src + "/" + else: + _src = self._space_name_to_src(src) + if _src is None: + raise ValueError( + f"Could not find Space: {src}. If it is a private Space, please provide a Hugging Face token." + ) + self.space_id = src + self.src = _src + state = self._get_space_state() + if state == SpaceStage.BUILDING: + if self.verbose: + print("Space is still building. Please wait...") + while self._get_space_state() == SpaceStage.BUILDING: + time.sleep(2) # so we don't get rate limited by the API + pass + if state in utils.INVALID_RUNTIME: + raise ValueError( + f"The current space is in the invalid state: {state}. " + "Please contact the owner to fix this." + ) + if self.verbose: + print(f"Loaded as API: {self.src}") + + if auth is not None: + self._login(auth) + + self.config = self._get_config() + self.protocol: Literal["ws", "sse", "sse_v1", "sse_v2", "sse_v2.1"] = ( + self.config.get("protocol", "ws") + ) + api_prefix: str = self.config.get("api_prefix", "") + self.api_prefix = api_prefix.lstrip("/") + "/" + self.src_prefixed = ( + urllib.parse.urljoin(self.src, self.api_prefix).rstrip("/") + "/" + ) + self.api_url = urllib.parse.urljoin(self.src_prefixed, utils.API_URL) + self.sse_url = urllib.parse.urljoin( + self.src_prefixed, + utils.SSE_URL_V0 if self.protocol == "sse" else utils.SSE_URL, + ) + self.heartbeat_url = urllib.parse.urljoin( + self.src_prefixed, utils.HEARTBEAT_URL + ) + self.sse_data_url = urllib.parse.urljoin( + self.src_prefixed, + utils.SSE_DATA_URL_V0 if self.protocol == "sse" else utils.SSE_DATA_URL, + ) + self.ws_url = urllib.parse.urljoin( + self.src_prefixed.replace("http", "ws", 1), utils.WS_URL + ) + self.upload_url = urllib.parse.urljoin(self.src_prefixed, utils.UPLOAD_URL) + self.reset_url = urllib.parse.urljoin(self.src_prefixed, utils.RESET_URL) + self.app_version = version.parse(self.config.get("version", "2.0")) + self._info = self._get_api_info() + self.session_hash = str(uuid.uuid4()) + + self.endpoints = { + dependency.get("id", fn_index): Endpoint( + self, dependency.get("id", fn_index), dependency, self.protocol + ) + for fn_index, dependency in enumerate(self.config["dependencies"]) + } + + # Create a pool of threads to handle the requests + self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) + + self.analytics_enabled = ( + analytics_enabled or os.getenv("GRADIO_ANALYTICS_ENABLED", "True") == "True" + ) + if self.analytics_enabled: + threading.Thread(target=self._telemetry_thread, daemon=True).start() + self._refresh_heartbeat = threading.Event() + self._kill_heartbeat = threading.Event() + + self.heartbeat = threading.Thread(target=self._stream_heartbeat, daemon=True) + self.heartbeat.start() + + self.stream_open = False + self.streaming_future: Future | None = None + self.pending_messages_per_event: dict[str, deque[Message | None]] = {} + self.pending_event_ids: set[str] = set() + + def close(self): + self._kill_heartbeat.set() + self.heartbeat.join(timeout=1) + + def _stream_heartbeat(self): + while True: + url = self.heartbeat_url.format(session_hash=self.session_hash) + try: + httpx_kwargs = self.httpx_kwargs.copy() + httpx_kwargs.setdefault("timeout", 20) + with httpx.stream( + "GET", + url, + headers=self.headers, + cookies=self.cookies, + verify=self.ssl_verify, + **httpx_kwargs, + ) as response: + for _ in response.iter_lines(): + if self._refresh_heartbeat.is_set(): + self._refresh_heartbeat.clear() + break + if self._kill_heartbeat.is_set(): + return + except httpx.TransportError: + return + + def stream_messages( + self, + protocol: Literal["sse_v1", "sse_v2", "sse_v2.1", "sse_v3"], + session_hash: str, + ) -> None: + try: + httpx_kwargs = self.httpx_kwargs.copy() + httpx_kwargs.setdefault("timeout", httpx.Timeout(timeout=None)) + with httpx.Client( + verify=self.ssl_verify, + **httpx_kwargs, + ) as client: + with client.stream( + "GET", + self.sse_url, + params={"session_hash": session_hash}, + headers=self.headers, + cookies=self.cookies, + ) as response: + buffer = b"" + for chunk in response.iter_bytes(): + buffer += chunk + while b"\n\n" in buffer: + line, buffer = buffer.split(b"\n\n", 1) + line = line.decode("utf-8").rstrip("\n") + if not len(line): + continue + if line.startswith("data:"): + resp = json.loads(line[5:]) + if resp["msg"] == ServerMessage.heartbeat: + continue + elif ( + resp.get("message", "") + == ServerMessage.server_stopped + ): + for ( + pending_messages + ) in self.pending_messages_per_event.values(): + pending_messages.append(resp) + return + elif resp["msg"] == ServerMessage.close_stream: + self.stream_open = False + return + event_id = resp["event_id"] + if event_id not in self.pending_messages_per_event: + self.pending_messages_per_event[event_id] = deque() + self.pending_messages_per_event[event_id].append(resp) + if resp["msg"] == ServerMessage.process_completed: + self.pending_event_ids.remove(event_id) + if ( + len(self.pending_event_ids) == 0 + and protocol != "sse_v3" + ): + self.stream_open = False + return + else: + raise ValueError(f"Unexpected SSE line: '{line}'") + except BaseException as e: + # If the job is cancelled the stream will close so we + # should not raise this httpx exception that comes from the + # stream abruply closing + if isinstance(e, httpx.RemoteProtocolError): + return + import traceback + + traceback.print_exc() + raise e + + def send_data(self, data, hash_data, protocol, request_headers): + headers = self.add_zero_gpu_headers(self.headers) + if request_headers is not None: + headers = {**request_headers, **headers} + req = httpx.post( + self.sse_data_url, + json={**data, **hash_data}, + headers=headers, + cookies=self.cookies, + verify=self.ssl_verify, + **self.httpx_kwargs, + ) + if req.status_code == 503: + raise QueueError("Queue is full! Please try again.") + if (validation_message := utils.extract_validation_message(req)) is not None: + raise ValidationError(validation_message) + req.raise_for_status() + resp = req.json() + event_id = resp["event_id"] + + if not self.stream_open: + self.stream_open = True + + def open_stream(): + return self.stream_messages( + protocol, session_hash=hash_data["session_hash"] + ) + + def close_stream(_): + self.stream_open = False + for _, pending_messages in self.pending_messages_per_event.items(): + pending_messages.append(None) + + if self.streaming_future is None or self.streaming_future.done(): + self.streaming_future = self.executor.submit(open_stream) + self.streaming_future.add_done_callback(close_stream) + + return event_id + + @classmethod + def duplicate( + cls, + from_id: str, + to_id: str | None = None, + token: str | None = None, + private: bool = True, + hardware: Literal[ + "cpu-basic", + "cpu-upgrade", + "t4-small", + "t4-medium", + "a10g-small", + "a10g-large", + "a100-large", + ] + | SpaceHardware + | None = None, + secrets: dict[str, str] | None = None, + sleep_timeout: int = 5, + max_workers: int = 40, + verbose: bool = True, + ): + """ + Duplicates a Hugging Face Space under your account and returns a Client object + for the new Space. No duplication is created if the Space already exists in your + account (to override this, provide a new name for the new Space using `to_id`). + To use this method, you must provide an `token` or be logged in via the Hugging + Face Hub CLI. + + The new Space will be private by default and use the same hardware as the original + Space. This can be changed by using the `private` and `hardware` parameters. For + hardware upgrades (beyond the basic CPU tier), you may be required to provide + billing information on Hugging Face: https://huggingface.co/settings/billing + + Parameters: + from_id: The name of the Hugging Face Space to duplicate in the format "{username}/{space_id}", e.g. "gradio/whisper". + to_id: The name of the new Hugging Face Space to create, e.g. "abidlabs/whisper-duplicate". If not provided, the new Space will be named "{your_HF_username}/{space_id}". + token: optional Hugging Face token to use to duplicating private Spaces. By default, no token is sent to the server. Set `token=None` to use the locally saved token if there is one. Find your tokens here: https://huggingface.co/settings/tokens. + private: Whether the new Space should be private (True) or public (False). Defaults to True. + hardware: The hardware tier to use for the new Space. Defaults to the same hardware tier as the original Space. Options include "cpu-basic", "cpu-upgrade", "t4-small", "t4-medium", "a10g-small", "a10g-large", "a100-large", subject to availability. + secrets: A dictionary of (secret key, secret value) to pass to the new Space. Defaults to None. Secrets are only used when the Space is duplicated for the first time, and are not updated if the duplicated Space already exists. + sleep_timeout: The number of minutes after which the duplicate Space will be puased if no requests are made to it (to minimize billing charges). Defaults to 5 minutes. + max_workers: The maximum number of thread workers that can be used to make requests to the remote Gradio app simultaneously. + verbose: Whether the client should print statements to the console. + Example: + import os + from gradio_client import Client + HF_TOKEN = os.environ.get("HF_TOKEN") + client = Client.duplicate("abidlabs/whisper", token=HF_TOKEN) + client.predict("audio_sample.wav") + >> "This is a test of the whisper speech recognition model." + """ + try: + original_info = huggingface_hub.get_space_runtime(from_id, token=token) + except RepositoryNotFoundError as rnfe: + raise ValueError( + f"Could not find Space: {from_id}. If it is a private Space, please provide a `token`." + ) from rnfe + if to_id: + if "/" in to_id: + to_id = to_id.split("/")[1] + space_id = huggingface_hub.get_full_repo_name(to_id, token=token) + else: + space_id = huggingface_hub.get_full_repo_name( + from_id.split("/")[1], token=token + ) + try: + huggingface_hub.get_space_runtime(space_id, token=token) + if verbose: + print( + f"Using your existing Space: {utils.SPACE_URL.format(space_id)} 🤗" + ) + if secrets is not None: + warnings.warn( + "Secrets are only used when the Space is duplicated for the first time, and are not updated if the duplicated Space already exists." + ) + except RepositoryNotFoundError: + if verbose: + print(f"Creating a duplicate of {from_id} for your own use... 🤗") + huggingface_hub.duplicate_space( + from_id=from_id, + to_id=space_id, + token=token, + exist_ok=True, + private=private, + ) + if secrets is not None: + for key, value in secrets.items(): + huggingface_hub.add_space_secret(space_id, key, value, token=token) + if verbose: + print(f"Created new Space: {utils.SPACE_URL.format(space_id)}") + current_info = huggingface_hub.get_space_runtime(space_id, token=token) + current_hardware = ( + current_info.hardware or huggingface_hub.SpaceHardware.CPU_BASIC + ) + hardware = hardware or original_info.hardware + if current_hardware != hardware: + huggingface_hub.request_space_hardware(space_id, hardware, token=token) # type: ignore + print( + f"-------\nNOTE: this Space uses upgraded hardware: {hardware}... see billing info at https://huggingface.co/settings/billing\n-------" + ) + # Setting a timeout only works if the hardware is not basic + # so set it here after the hardware has been requested + if hardware != huggingface_hub.SpaceHardware.CPU_BASIC: + utils.set_space_timeout( + space_id, token=token, timeout_in_seconds=sleep_timeout * 60 + ) + if verbose: + print("") + client = cls(space_id, token=token, max_workers=max_workers, verbose=verbose) + return client + + def _get_space_state(self): + if not self.space_id: + return None + info = huggingface_hub.get_space_runtime(self.space_id, token=self.token) + return info.stage + + def predict( + self, + *args, + api_name: str | None = None, + fn_index: int | None = None, + headers: dict[str, str] | None = None, + **kwargs, + ) -> Any: + """ + Calls the Gradio API and returns the result (this is a blocking call). Arguments can be provided as positional arguments or as keyword arguments (latter is recommended). + + Parameters: + args: The positional arguments to pass to the remote API endpoint. The order of the arguments must match the order of the inputs in the Gradio app. + api_name: The name of the API endpoint to call starting with a leading slash, e.g. "/predict". Does not need to be provided if the Gradio app has only one named API endpoint. + fn_index: As an alternative to api_name, this parameter takes the index of the API endpoint to call, e.g. 0. Both api_name and fn_index can be provided, but if they conflict, api_name will take precedence. + kwargs: The keyword arguments to pass to the remote API endpoint. + headers: Additional headers to send to the remote Gradio app on this request. This parameter will overrides the headers provided in the Client constructor if they have the same keys. + Returns: + The result of the API call. Will be a Tuple if the API has multiple outputs. + Example: + from gradio_client import Client + client = Client(src="gradio/calculator") + client.predict(5, "add", 4, api_name="/predict") + >> 9.0 + """ + return self.submit( + *args, api_name=api_name, fn_index=fn_index, headers=headers, **kwargs + ).result() + + def new_helper( + self, fn_index: int, headers: dict[str, str] | None = None + ) -> Communicator: + return Communicator( + Lock(), + JobStatus(), + self.endpoints[fn_index].process_predictions, + self.reset_url, + request_headers=headers, + ) + + def submit( + self, + *args, + api_name: str | None = None, + fn_index: int | None = None, + headers: dict[str, str] | None = None, + result_callbacks: Callable | list[Callable] | None = None, + **kwargs, + ) -> Job: + """ + Creates and returns a Job object which calls the Gradio API in a background thread. The job can be used to retrieve the status and result of the remote API call. + Arguments can be provided as positional arguments or as keyword arguments (latter is recommended). + + Parameters: + args: The arguments to pass to the remote API. The order of the arguments must match the order of the inputs in the Gradio app. + api_name: The name of the API endpoint to call starting with a leading slash, e.g. "/predict". Does not need to be provided if the Gradio app has only one named API endpoint. + fn_index: As an alternative to api_name, this parameter takes the index of the API endpoint to call, e.g. 0. Both api_name and fn_index can be provided, but if they conflict, api_name will take precedence. + result_callbacks: A callback function, or list of callback functions, to be called when the result is ready. If a list of functions is provided, they will be called in order. The return values from the remote API are provided as separate parameters into the callback. If None, no callback will be called. + kwargs: The keyword arguments to pass to the remote API endpoint. + headers: Additional headers to send to the remote Gradio app on this request. This parameter will overrides the headers provided in the Client constructor if they have the same keys. + Returns: + A Job object that can be used to retrieve the status and result of the remote API call. + Example: + from gradio_client import Client + client = Client(src="gradio/calculator") + job = client.submit(5, "add", 4, api_name="/predict") + job.status() + >> + job.result() # blocking call + >> 9.0 + """ + inferred_fn_index = self._infer_fn_index(api_name, fn_index) + + endpoint = self.endpoints[inferred_fn_index] + + if isinstance(endpoint, Endpoint): + args = utils.construct_args(endpoint.parameters_info, args, kwargs) + + helper = None + if endpoint.protocol in ( + "sse", + "sse_v1", + "sse_v2", + "sse_v2.1", + "sse_v3", + ): + headers = headers or {} + headers["x-gradio-user"] = "api" + helper = self.new_helper(inferred_fn_index, headers=headers) + end_to_end_fn = endpoint.make_end_to_end_fn(helper) + else: + raise ValueError("Unknown protocol: " + endpoint.protocol) + future = self.executor.submit(copy_context().run, end_to_end_fn, *args) + + cancel_fn = endpoint.make_cancel(helper) + + job = Job( + future, + communicator=helper, + verbose=self.verbose, + space_id=self.space_id, + _cancel_fn=cancel_fn, + ) + + if result_callbacks: + if isinstance(result_callbacks, Callable): + result_callbacks = [result_callbacks] + + def create_fn(callback) -> Callable: + def fn(future): + if isinstance(future.result(), tuple): + callback(*future.result()) + else: + callback(future.result()) + + return fn + + for callback in result_callbacks: + job.add_done_callback(create_fn(callback)) + + return job + + def _get_api_info(self): + api_info_url = urllib.parse.urljoin(self.src_prefixed, utils.RAW_API_INFO_URL) + if self.app_version > version.Version("3.36.1"): + r = httpx.get( + api_info_url, + headers=self.headers, + cookies=self.cookies, + verify=self.ssl_verify, + **self.httpx_kwargs, + ) + if r.is_success: + info = r.json() + else: + raise ValueError(f"Could not fetch api info for {self.src}: {r.text}") + else: + fetch = httpx.post( + utils.SPACE_FETCHER_URL, + json={ + "config": json.dumps(self.config), + "serialize": False, + }, + headers=self.headers, + cookies=self.cookies, + verify=self.ssl_verify, + **self.httpx_kwargs, + ) + if fetch.is_success: + info = fetch.json()["api"] + else: + raise ValueError( + f"Could not fetch api info for {self.src}: {fetch.text}" + ) + named_endpoints = {} + unnamed_endpoints = {} + for api_name, endpoint in info["named_endpoints"].items(): + endpoint.pop("code_snippets", None) + if ( + "api_visibility" in endpoint + and endpoint.pop("api_visibility") != "private" + ) or (endpoint.pop("show_api", True)): + named_endpoints[api_name] = endpoint + for fn_index, endpoint in info["unnamed_endpoints"].items(): + endpoint.pop("code_snippets", None) + if ( + "api_visibility" in endpoint + and endpoint.pop("api_visibility") != "private" + ) or (endpoint.pop("show_api", True)): + unnamed_endpoints[fn_index] = endpoint + info["unnamed_endpoints"] = unnamed_endpoints + info["named_endpoints"] = named_endpoints + return info + + def view_api( + self, + all_endpoints: bool | None = None, + print_info: bool = True, + return_format: Literal["dict", "str"] | None = None, + ) -> dict | str | None: + """ + Prints the usage info for the API. If the Gradio app has multiple API endpoints, the usage info for each endpoint will be printed separately. If return_format="dict" the info is returned in dictionary format, as shown in the example below. + + Parameters: + all_endpoints: If True, prints information for both named and unnamed endpoints in the Gradio app. If False, will only print info about named endpoints. If None (default), will print info about named endpoints, unless there aren't any -- in which it will print info about unnamed endpoints. + print_info: If True, prints the usage info to the console. If False, does not print the usage info. + return_format: If None, nothing is returned. If "str", returns the same string that would be printed to the console. If "dict", returns the usage info as a dictionary that can be programmatically parsed, and *all endpoints are returned in the dictionary* regardless of the value of `all_endpoints`. The format of the dictionary is in the docstring of this method. + Example: + from gradio_client import Client + client = Client(src="gradio/calculator") + client.view_api(return_format="dict") + >> { + 'named_endpoints': { + '/predict': { + 'parameters': [ + { + 'label': 'num1', + 'python_type': 'int | float', + 'type_description': 'numeric value', + 'component': 'Number', + 'example_input': '5' + }, + { + 'label': 'operation', + 'python_type': 'str', + 'type_description': 'string value', + 'component': 'Radio', + 'example_input': 'add' + }, + { + 'label': 'num2', + 'python_type': 'int | float', + 'type_description': 'numeric value', + 'component': 'Number', + 'example_input': '5' + }, + ], + 'returns': [ + { + 'label': 'output', + 'python_type': 'int | float', + 'type_description': 'numeric value', + 'component': 'Number', + }, + ] + }, + '/flag': { + 'parameters': [ + ... + ], + 'returns': [ + ... + ] + } + } + } + 'unnamed_endpoints': { + 2: { + 'parameters': [ + ... + ], + 'returns': [ + ... + ] + } + } + } + } + + """ + num_named_endpoints = len(self._info["named_endpoints"]) + num_unnamed_endpoints = len(self._info["unnamed_endpoints"]) + if num_named_endpoints == 0 and all_endpoints is None: + all_endpoints = True + + human_info = "Client.predict() Usage Info\n---------------------------\n" + human_info += f"Named API endpoints: {num_named_endpoints}\n" + + for api_name, endpoint_info in self._info["named_endpoints"].items(): + human_info += self._render_endpoints_info(api_name, endpoint_info) + + if all_endpoints: + human_info += f"\nUnnamed API endpoints: {num_unnamed_endpoints}\n" + for fn_index, endpoint_info in self._info["unnamed_endpoints"].items(): + # When loading from json, the fn_indices are read as strings + # because json keys can only be strings + human_info += self._render_endpoints_info(int(fn_index), endpoint_info) + elif num_unnamed_endpoints > 0: + human_info += f"\nUnnamed API endpoints: {num_unnamed_endpoints}, to view, run Client.view_api(all_endpoints=True)\n" + + if print_info: + print(human_info) + if return_format == "str": + return human_info + elif return_format == "dict": + return self._info + + def reset_session(self) -> None: + self.session_hash = str(uuid.uuid4()) + self._refresh_heartbeat.set() + + def add_zero_gpu_headers(self, headers: dict[str, str]) -> dict[str, str]: + """ + Adds the x-ip-token header to the headers dictionary to pass it to a Zero-GPU Space. This allows a user's + ZeroGPU quota to be tracked and used by the underlying Space. For the x-ip-token header to be present, + this method needs to be called when a Gradio app's LocalContext is defined. i.e. this method needs to be called + when a Gradio app's LocalContext is defined. i.e. must be called from inside a Gradio app's + event listener function or will not have any effect. + """ + if not self.space_id: + return headers + try: + from gradio.context import LocalContext + except ( + ImportError + ): # this is not running within a Gradio app as Gradio is not installed + return headers + request = LocalContext.request.get(None) + if request and hasattr(request, "headers") and "x-ip-token" in request.headers: + headers["x-ip-token"] = request.headers["x-ip-token"] + return headers + + def _render_endpoints_info( + self, + name_or_index: str | int, + endpoints_info: dict[str, list[ParameterInfo]], + ) -> str: + parameter_info = endpoints_info["parameters"] + parameter_names = [ + p.get("parameter_name") or p["label"] for p in parameter_info + ] + parameter_names = [utils.sanitize_parameter_names(p) for p in parameter_names] + rendered_parameters = ", ".join(parameter_names) + if rendered_parameters: + rendered_parameters = rendered_parameters + ", " + return_values = [p["label"] for p in endpoints_info["returns"]] + return_values = [utils.sanitize_parameter_names(r) for r in return_values] + rendered_return_values = ", ".join(return_values) + if len(return_values) > 1: + rendered_return_values = f"({rendered_return_values})" + + if isinstance(name_or_index, str): + final_param = f'api_name="{name_or_index}"' + elif isinstance(name_or_index, int): + final_param = f"fn_index={name_or_index}" + else: + raise ValueError("name_or_index must be a string or integer") + + human_info = f"\n - predict({rendered_parameters}{final_param}) -> {rendered_return_values}\n" + human_info += " Parameters:\n" + if parameter_info: + for info in parameter_info: + desc = ( + f" ({info['python_type']['description']})" + if info["python_type"].get("description") + else "" + ) + default_value = info.get("parameter_default") + default_value = utils.traverse( + default_value, + lambda x: f'handle_file("{x["url"]}")', + utils.is_file_obj_with_meta, + ) + default_info = ( + "(required)" + if not info.get("parameter_has_default", False) + else f"(not required, defaults to: {default_value})" + ) + type_ = info["python_type"]["type"] + if info.get("parameter_has_default", False) and default_value is None: + type_ += " | None" + human_info += f" - [{info['component']}] {utils.sanitize_parameter_names(info.get('parameter_name') or info['label'])}: {type_} {default_info} {desc} \n" + else: + human_info += " - None\n" + human_info += " Returns:\n" + if endpoints_info["returns"]: + for info in endpoints_info["returns"]: + desc = ( + f" ({info['python_type']['description']})" + if info["python_type"].get("description") + else "" + ) + type_ = info["python_type"]["type"] + human_info += f" - [{info['component']}] {utils.sanitize_parameter_names(info['label'])}: {type_}{desc} \n" + else: + human_info += " - None\n" + + return human_info + + def __repr__(self): + return self.view_api(print_info=False, return_format="str") + + def __str__(self): + return self.view_api(print_info=False, return_format="str") + + def _telemetry_thread(self) -> None: + # Disable telemetry by setting the env variable HF_HUB_DISABLE_TELEMETRY=1 + data = { + "src": self.src, + } + try: + send_telemetry( + topic="py_client/initiated", + library_name="gradio_client", + library_version=utils.__version__, + user_agent=data, + ) + except Exception: + pass + + def _infer_fn_index(self, api_name: str | None, fn_index: int | None) -> int: + inferred_fn_index = None + if api_name is not None: + for i, d in enumerate(self.config["dependencies"]): + config_api_name = d.get("api_name") + # config_api_name may be false in 5.0 to indicate private APIs + if ( + config_api_name is None + or config_api_name is False + or d.get("api_visibility") == "private" + ): + continue + if "/" + config_api_name == api_name: + inferred_fn_index = d.get("id", i) + break + else: + error_message = f"Cannot find a function with `api_name`: {api_name}." + if not api_name.startswith("/"): + error_message += " Did you mean to use a leading slash?" + raise ValueError(error_message) + elif fn_index is not None: + inferred_fn_index = fn_index + if ( + inferred_fn_index not in self.endpoints + or not self.endpoints[inferred_fn_index].is_valid + ): + raise ValueError(f"Invalid function index: {fn_index}.") + else: + valid_endpoints = [ + e + for e in self.endpoints.values() + if e.is_valid + and e.api_name is not None + and e.backend_fn is not None + and e.api_visibility == "public" + ] + if len(valid_endpoints) == 1: + inferred_fn_index = valid_endpoints[0].fn_index + else: + raise ValueError( + "This Gradio app might have multiple endpoints. Please specify an `api_name` or `fn_index`" + ) + return inferred_fn_index + + def __del__(self): + if hasattr(self, "executor"): + self.executor.shutdown(wait=True) + + def _space_name_to_src(self, space) -> str | None: + return huggingface_hub.space_info(space, token=self.token).host # type: ignore + + def _login(self, auth: tuple[str, str]): + """ + Logs in to `utils.LOGIN_URL` using provided `auth` credentials. + Warning: This method overwrites `self.cookies`. + """ + resp = httpx.post( + urllib.parse.urljoin(self.src, utils.LOGIN_URL), + data={"username": auth[0], "password": auth[1]}, + verify=self.ssl_verify, + **self.httpx_kwargs, + ) + if not resp.is_success: + if resp.status_code == 401: + raise AuthenticationError( + f"Could not login to {self.src}. Invalid credentials." + ) + else: + raise ValueError(f"Could not login to {self.src}.") + self.cookies = { + name: value for name, value in resp.cookies.items() if value is not None + } + + def _get_config(self) -> dict: + r = httpx.get( + urllib.parse.urljoin(self.src, utils.CONFIG_URL), + headers=self.headers, + cookies=self.cookies, + verify=self.ssl_verify, + **self.httpx_kwargs, + ) + if r.is_success: + # Cookies are sometimes needed to correctly route requests if the Gradio app is + # running on multiple replicas e.g. using cookie session-affinity in Kubernetes. + # This approach attaches cookies from the first response to subsequent requests + # without overriding existing cookies. + new_cookies = { + name: value + for name, value in r.cookies.items() + if value is not None and name not in self.cookies + } + self.cookies.update(new_cookies) + return r.json() + elif r.status_code == 401: + raise AuthenticationError( + f"Could not load {self.src} as credentials were not provided. Please login." + ) + elif r.status_code == 429: + raise utils.TooManyRequestsError( + "Too many requests to the API, please try again later." + ) from None + else: # to support older versions of Gradio + r = httpx.get( + self.src, + headers=self.headers, + cookies=self.cookies, + verify=self.ssl_verify, + **self.httpx_kwargs, + ) + if not r.is_success: + raise ValueError(f"Could not fetch config for {self.src}") + # some basic regex to extract the config + result = re.search(r"window.gradio_config = (.*?);[\s]*", r.text) + try: + config = json.loads(result.group(1)) # type: ignore + except AttributeError as ae: + raise ValueError( + f"Could not get Gradio config from: {self.src}" + ) from ae + if "allow_flagging" in config: + raise ValueError( + "Gradio 2.x is not supported by this client. Please upgrade your Gradio app to Gradio 3.x or higher." + ) + return config + + +@dataclass +class ComponentApiType: + skip: bool + value_is_file: bool + is_state: bool + + +@dataclass +class ReplaceMe: + index: int + + +class Endpoint: + """Helper class for storing all the information about a single API endpoint.""" + + def __init__( + self, client: Client, fn_index: int, dependency: dict, protocol: str = "sse_v1" + ): + self.client: Client = client + self.fn_index = fn_index + self.dependency = dependency + api_name = dependency.get("api_name") + self.api_name: str | None = ( + "/" + api_name if isinstance(api_name, str) else api_name + ) + self._info = self.client._info + self.protocol = protocol + self.input_component_types = [ + self._get_component_type(id_) for id_ in dependency["inputs"] + ] + self.output_component_types = [ + self._get_component_type(id_) for id_ in dependency["outputs"] + ] + self.parameters_info = self._get_parameters_info() + self.root_url = self.client.src_prefixed + self.backend_fn = dependency.get("backend_fn") + + # Disallow hitting endpoints that the Gradio app has disabled + if "api_visibility" in dependency: + self.is_valid = dependency["api_visibility"] != "private" + self.api_visibility = dependency["api_visibility"] + else: + self.is_valid = dependency.get("api_name") is not False + if not self.is_valid: + self.api_visibility = "private" + elif dependency.get("show_api") is False: + self.api_visibility = "undocumented" + else: + self.api_visibility = "public" + + def _get_component_type(self, component_id: int): + component = next( + i for i in self.client.config["components"] if i["id"] == component_id + ) + skip_api = component.get("skip_api", component["type"] in utils.SKIP_COMPONENTS) + return ComponentApiType( + skip_api, + self.value_is_file(component), + component["type"] == "state", + ) + + def _get_parameters_info(self) -> list[ParameterInfo] | None: + if self.api_name in self._info["named_endpoints"]: + return self._info["named_endpoints"][self.api_name]["parameters"] + return None + + @staticmethod + def value_is_file(component: dict) -> bool: + # This is still hacky as it does not tell us which part of the payload is a file. + # If a component has a complex payload, part of which is a file, this will simply + # return True, which means that all parts of the payload will be uploaded as files + # if they are valid file paths. We will deprecate this 1.0. + if "api_info" not in component: + return False + return utils.value_is_file(component["api_info"]) + + def __repr__(self): + return f"Endpoint src: {self.client.src}, api_name: {self.api_name}, fn_index: {self.fn_index}" + + def __str__(self): + return self.__repr__() + + def make_end_to_end_fn(self, helper: Communicator): + _predict = self.make_predict(helper) + + def _inner(*data, **kwargs): + if not self.is_valid: + raise utils.InvalidAPIEndpointError() + + if self.client._skip_components: + data = self.insert_empty_state(*data) + data = self.process_input_files(*data) + predictions = _predict(*data, **kwargs) + predictions = self.process_predictions(*predictions) + + # Append final output only if not already present + # for consistency between generators and not generators + if helper: + with helper.lock: + if not helper.job.outputs: + helper.job.outputs.append(predictions) + return predictions + + return _inner + + def make_cancel( + self, + helper: Communicator | None, + ): + if helper is None: + return + if self.client.app_version > version.Version("4.29.0"): + url = urllib.parse.urljoin(self.client.src_prefixed, utils.CANCEL_URL) + + # The event_id won't be set on the helper until later + # so need to create the data in a function that's run at cancel time + def post_data(): + return { + "fn_index": self.fn_index, + "session_hash": self.client.session_hash, + "event_id": helper.event_id, + } + + cancel_msg = None + cancellable = True + else: + candidates: list[tuple[int, list[int]]] = [] + for i, dep in enumerate(self.client.config["dependencies"]): + if self.fn_index in dep["cancels"]: + candidates.append( + (i, [d for d in dep["cancels"] if d != self.fn_index]) + ) + + fn_index, other_cancelled = ( + min(candidates, key=lambda x: len(x[1])) if candidates else (None, None) + ) + cancellable = fn_index is not None + cancel_msg = None + if cancellable and other_cancelled: + other_api_names = [ + "/" + self.client.config["dependencies"][i].get("api_name") + for i in other_cancelled + ] + cancel_msg = ( + f"Cancelled this job will also cancel any jobs for {', '.join(other_api_names)} " + "that are currently running." + ) + elif not cancellable: + cancel_msg = ( + "Cancelling this job will not stop the server from running. " + "To fix this, an event must be added to the upstream app that explicitly cancels this one or " + "the upstream app must be running Gradio 4.29.0 and greater." + ) + + def post_data(): + return { + "data": [], + "fn_index": fn_index, + "session_hash": self.client.session_hash, + } + + url = self.client.api_url + + def _cancel(): + if cancel_msg: + warnings.warn(cancel_msg) + if cancellable: + httpx.post( + url, + json=post_data(), + headers=self.client.headers, + cookies=self.client.cookies, + verify=self.client.ssl_verify, + **self.client.httpx_kwargs, + ) + + return _cancel + + def make_predict(self, helper: Communicator): + def _predict(*data, **kwargs) -> tuple: + data = { + "data": data or [], + "fn_index": self.fn_index, + **kwargs, + } + + hash_data = { + "fn_index": self.fn_index, + "session_hash": kwargs.get("session_hash", self.client.session_hash), + } + + if self.protocol == "sse": + result = self._sse_fn_v0(data, hash_data, helper) # type: ignore + elif self.protocol in ("sse_v1", "sse_v2", "sse_v2.1", "sse_v3"): + event_id = self.client.send_data( + data, hash_data, self.protocol, helper.request_headers + ) + self.client.pending_event_ids.add(event_id) + self.client.pending_messages_per_event[event_id] = deque() + helper.event_id = event_id + result = self._sse_fn_v1plus(helper, event_id, self.protocol) + else: + raise ValueError(f"Unsupported protocol: {self.protocol}") + + if "error" in result: + if result["error"] is None: + raise AppError( + "The upstream Gradio app has raised an exception but has not enabled " + "verbose error reporting. To enable, set show_error=True in launch()." + ) + else: + message = result.pop("error") + raise AppError(message=message, **result) + + try: + output = result["data"] + except KeyError as ke: + is_public_space = ( + self.client.space_id + and not huggingface_hub.space_info(self.client.space_id).private + ) + if "error" in result and "429" in result["error"] and is_public_space: + raise utils.TooManyRequestsError( + f"Too many requests to the API, please try again later. To avoid being rate-limited, " + f"please duplicate the Space using Client.duplicate({self.client.space_id}) " + f"and pass in your Hugging Face token." + ) from None + elif "error" in result: + raise ValueError(result["error"]) from None + raise KeyError( + f"Could not find 'data' key in response. Response received: {result}" + ) from ke + return tuple(output) + + return _predict + + def insert_empty_state(self, *data) -> tuple: + data = list(data) + for i, input_component_type in enumerate(self.input_component_types): + if input_component_type.is_state: + data.insert(i, None) + return tuple(data) + + def process_input_files(self, *data) -> tuple: + data_ = [] + for i, d in enumerate(data): + d = utils.traverse( + d, + partial(self._upload_file, data_index=i), + utils.is_file_obj_with_meta, + ) + data_.append(d) + return tuple(data_) + + def process_predictions(self, *predictions): + # If self.download_file is True, we assume that that the user is using the Client directly (as opposed + # within gr.load) and therefore, download any files generated by the server and skip values for + # components that the user likely does not want to see (e.g. gr.State, gr.Tab). + if self.client.download_files: + predictions = self.download_files(*predictions) + if self.client._skip_components: + predictions = self.remove_skipped_components(*predictions) + predictions = self.reduce_singleton_output(*predictions) + return predictions + + def download_files(self, *data) -> tuple: + data_ = list(data) + if self.client.protocol == "sse_v2.1": + data_ = utils.traverse( + data_, self._download_file, utils.is_file_obj_with_meta + ) + else: + data_ = utils.traverse(data_, self._download_file, utils.is_file_obj) + return tuple(data_) + + def remove_skipped_components(self, *data) -> tuple: + """""" + data = [ + d + for d, oct in zip(data, self.output_component_types, strict=False) + if not oct.skip + ] + return tuple(data) + + def reduce_singleton_output(self, *data) -> Any: + if self.client._skip_components: + effective_output_components = [ + o for o in self.output_component_types if not o.skip + ] + else: + effective_output_components = self.output_component_types + if len(effective_output_components) == 1: + return data[0] + else: + return data + + def _upload_file(self, f: dict, data_index: int) -> dict[str, Any]: + file_path = f["path"] + orig_name = Path(file_path) + if not utils.is_http_url_like(file_path): + component_id = self.dependency["inputs"][data_index] + component_config = next( + ( + c + for c in self.client.config["components"] + if c["id"] == component_id + ), + {}, + ) + max_file_size = self.client.config.get("max_file_size", None) + max_file_size = math.inf if max_file_size is None else max_file_size + if os.path.getsize(file_path) > max_file_size: + raise ValueError( + f"File {file_path} exceeds the maximum file size of {max_file_size} bytes " + f"set in {component_config.get('label', '') + ''} component." + ) + with open(file_path, "rb") as f_: + files = [("files", (orig_name.name, f_))] + r = httpx.post( + self.client.upload_url, + headers=self.client.headers, + cookies=self.client.cookies, + verify=self.client.ssl_verify, + files=files, + **self.client.httpx_kwargs, + ) + r.raise_for_status() + result = r.json() + file_path = result[0] + # Only return orig_name if has a suffix because components + # use the suffix of the original name to determine format to save it to in cache. + return { + "path": file_path, + "orig_name": utils.strip_invalid_filename_characters(orig_name.name), + "meta": {"_type": "gradio.FileData"}, + } + + def _download_file(self, x: dict) -> str: + # For streams, use the URL directly if available, as streams are located at different paths + if x.get("is_stream", False) and "url" in x: + url_path = x["url"] + # If the URL is relative, prepend the root URL + if not url_path.startswith(("http://", "https://")): + url_path = self.root_url + url_path.lstrip("/") + else: + url_path = self.root_url + "file=" + x["path"] + + if self.client.output_dir is not None: + os.makedirs(self.client.output_dir, exist_ok=True) + + sha = hashlib.sha256() + temp_dir = Path(tempfile.gettempdir()) / secrets.token_hex(20) + temp_dir.mkdir(exist_ok=True, parents=True) + + with httpx.stream( + "GET", + url_path, + headers=self.client.headers, + cookies=self.client.cookies, + verify=self.client.ssl_verify, + follow_redirects=True, + **self.client.httpx_kwargs, + ) as response: + response.raise_for_status() + with open(temp_dir / Path(url_path).name, "wb") as f: + for chunk in response.iter_bytes(chunk_size=128 * sha.block_size): + sha.update(chunk) + f.write(chunk) + + directory = Path(self.client.output_dir) / sha.hexdigest() + directory.mkdir(exist_ok=True, parents=True) + dest = directory / Path(url_path).name + shutil.move(temp_dir / Path(url_path).name, dest) + return str(dest.resolve()) + + def _sse_fn_v0(self, data: dict, hash_data: dict, helper: Communicator): + with httpx.Client( + timeout=httpx.Timeout(timeout=None), + verify=self.client.ssl_verify, + **self.client.httpx_kwargs, + ) as client: + return utils.get_pred_from_sse_v0( + client, + data, + hash_data, + helper, + self.client.sse_url, + self.client.sse_data_url, + self.client.headers, + self.client.cookies, + self.client.ssl_verify, + self.client.executor, + ) + + def _sse_fn_v1plus( + self, + helper: Communicator, + event_id: str, + protocol: Literal["sse_v1", "sse_v2", "sse_v2.1", "sse_v3"], + ): + return utils.get_pred_from_sse_v1plus( + helper, + self.client.headers, + self.client.cookies, + self.client.pending_messages_per_event, + event_id, + protocol, + self.client.ssl_verify, + self.client.executor, + ) + + +@document("result", "outputs", "status") +class Job(Future): + """ + A Job is a wrapper over the Future class that represents a prediction call that has been + submitted by the Gradio client. This class is not meant to be instantiated directly, but rather + is created by the Client.submit() method. + + A Job object includes methods to get the status of the prediction call, as well to get the outputs + of the prediction call. Job objects are also iterable, and can be used in a loop to get the outputs + of prediction calls as they become available for generator endpoints. + """ + + def __init__( + self, + future: Future, + communicator: Communicator | None = None, + verbose: bool = True, + space_id: str | None = None, + _cancel_fn: Callable[[], None] | None = None, + ): + """ + Parameters: + future: The future object that represents the prediction call, created by the Client.submit() method + communicator: The communicator object that is used to communicate between the client and the background thread running the job + verbose: Whether to print any status-related messages to the console + space_id: The space ID corresponding to the Client object that created this Job object + """ + self.future = future + self.communicator = communicator + self._counter = 0 + self.verbose = verbose + self.space_id = space_id + self.cancel_fn = _cancel_fn + + def __iter__(self) -> Job: + return self + + def __next__(self) -> tuple | Any: + if not self.communicator: + raise StopIteration() + + while True: + with self.communicator.lock: + if len(self.communicator.job.outputs) >= self._counter + 1: + o = self.communicator.job.outputs[self._counter] + self._counter += 1 + return o + if ( + self.communicator.job.latest_status.code == Status.FINISHED + and self._counter >= len(self.communicator.job.outputs) + ): + raise StopIteration() + time.sleep(0.001) + + async def __aiter__(self) -> AsyncGenerator[Update, None]: + """Async iterator that yields all updates from the communicator.updates queue.""" + if not self.communicator: + return + + while True: + get = self.communicator.updates.get() + try: + update = await asyncio.wait_for(get, timeout=0.5) + yield update + except asyncio.TimeoutError: + if self.done(): + return + continue + + def result(self, timeout: float | None = None) -> Any: + """ + Return the result of the call that the future represents. Raises CancelledError: If the future was cancelled, TimeoutError: If the future didn't finish executing before the given timeout, and Exception: If the call raised then that exception will be raised. + + Parameters: + timeout: The number of seconds to wait for the result if the future isn't done. If None, then there is no limit on the wait time. + Returns: + The result of the call that the future represents. For generator functions, it will return the final iteration. + Example: + from gradio_client import Client + calculator = Client(src="gradio/calculator") + job = calculator.submit("foo", "add", 4, fn_index=0) + job.result(timeout=5) + >> 9 + """ + return super().result(timeout=timeout) + + def outputs(self) -> list[tuple | Any]: + """ + Returns a list containing the latest outputs from the Job. + + If the endpoint has multiple output components, the list will contain + a tuple of results. Otherwise, it will contain the results without storing them + in tuples. + + For endpoints that are queued, this list will contain the final job output even + if that endpoint does not use a generator function. + + Example: + from gradio_client import Client + client = Client(src="gradio/count_generator") + job = client.submit(3, api_name="/count") + while not job.done(): + time.sleep(0.1) + job.outputs() + >> ['0', '1', '2'] + """ + if not self.communicator: + return [] + else: + with self.communicator.lock: + return self.communicator.job.outputs + + def status(self) -> StatusUpdate: + """ + Returns the latest status update from the Job in the form of a StatusUpdate + object, which contains the following fields: code, rank, queue_size, success, time, eta, and progress_data. + + progress_data is a list of updates emitted by the gr.Progress() tracker of the event handler. Each element + of the list has the following fields: index, length, unit, progress, desc. If the event handler does not have + a gr.Progress() tracker, the progress_data field will be None. + + Example: + from gradio_client import Client + client = Client(src="gradio/calculator") + job = client.submit(5, "add", 4, api_name="/predict") + job.status() + >> + job.status().eta + >> 43.241 # seconds + """ + time = datetime.now() + cancelled = False + if self.communicator: + with self.communicator.lock: + cancelled = self.communicator.should_cancel + if cancelled: + return StatusUpdate( + code=Status.CANCELLED, + rank=0, + queue_size=None, + success=False, + time=time, + eta=None, + progress_data=None, + ) + if self.done(): + if not self.future._exception: # type: ignore + return StatusUpdate( + code=Status.FINISHED, + rank=0, + queue_size=None, + success=True, + time=time, + eta=None, + progress_data=None, + ) + else: + return StatusUpdate( + code=Status.FINISHED, + rank=0, + queue_size=None, + success=False, + time=time, + eta=None, + progress_data=None, + ) + elif not self.communicator: + return StatusUpdate( + code=Status.PROCESSING, + rank=0, + queue_size=None, + success=None, + time=time, + eta=None, + progress_data=None, + ) + else: + with self.communicator.lock: + eta = self.communicator.job.latest_status.eta + if self.verbose and self.space_id and eta and eta > 30: + print( + f"Due to heavy traffic on this app, the prediction will take approximately {int(eta)} seconds." + f"For faster predictions without waiting in queue, you may duplicate the space using: Client.duplicate({self.space_id})" + ) + return self.communicator.job.latest_status + + def cancel(self) -> bool: + """Cancels the job as best as possible. + + If the app you are connecting to has the gradio queue enabled, the job + will be cancelled locally as soon as possible. For apps that do not use the + queue, the job cannot be cancelled if it's been sent to the local executor + (for the time being). + + Note: In general, this DOES not stop the process from running in the upstream server + except for the following situations: + + 1. If the job is queued upstream, it will be removed from the queue and the server will not run the job + 2. If the job has iterative outputs, the job will finish as soon as the current iteration finishes running + 3. If the job has not been picked up by the queue yet, the queue will not pick up the job + """ + if self.communicator: + with self.communicator.lock: + self.communicator.should_cancel = True + if self.cancel_fn: + self.cancel_fn() + return True + return self.future.cancel() + + def __getattr__(self, name): + """Forwards any properties to the Future class.""" + return getattr(self.future, name) diff --git a/client/python/gradio_client/data_classes.py b/client/python/gradio_client/data_classes.py new file mode 100644 index 0000000..267be1e --- /dev/null +++ b/client/python/gradio_client/data_classes.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import Any, TypedDict + +from typing_extensions import NotRequired + + +class FileData(TypedDict): + name: str | None # filename + data: str | None # base64 encoded data + size: NotRequired[int | None] # size in bytes + is_file: NotRequired[ + bool + ] # whether the data corresponds to a file or base64 encoded data + orig_name: NotRequired[str] # original filename + mime_type: NotRequired[str] + is_stream: NotRequired[bool] + + +class ParameterInfo(TypedDict): + label: str + parameter_name: str + parameter_has_default: NotRequired[bool] + parameter_default: NotRequired[Any] + type: dict + python_type: dict + component: str + example_input: Any diff --git a/client/python/gradio_client/documentation.py b/client/python/gradio_client/documentation.py new file mode 100644 index 0000000..a6fde04 --- /dev/null +++ b/client/python/gradio_client/documentation.py @@ -0,0 +1,371 @@ +"""Contains methods that generate documentation for Gradio functions and classes.""" + +from __future__ import annotations + +import dataclasses +import inspect +import warnings +from collections import defaultdict +from collections.abc import Callable +from functools import lru_cache + +classes_to_document = defaultdict(list) +classes_inherit_documentation = {} + + +def set_documentation_group(m): # noqa: ARG001 + """A no-op for backwards compatibility of custom components published prior to 4.16.0""" + pass + + +def extract_instance_attr_doc(cls, attr): + code = inspect.getsource(cls.__init__) + lines = [line.strip() for line in code.split("\n")] + i = None + for i, line in enumerate(lines): # noqa: B007 + if line.startswith("self." + attr + ":") or line.startswith( + "self." + attr + " =" + ): + break + if i is None: + raise NameError(f"Could not find {attr} in {cls.__name__}") + start_line = lines.index('"""', i) + end_line = lines.index('"""', start_line + 1) + for j in range(i + 1, start_line): + if lines[j].startswith("self."): + raise ValueError( + f"Found another attribute before docstring for {attr} in {cls.__name__}: " + + lines[j] + + "\n start:" + + lines[i] + ) + doc_string = " ".join(lines[start_line + 1 : end_line]) + return doc_string + + +_module_prefixes = [ + ("gradio._simple_templates", "component"), + ("gradio.server", "block"), + ("gradio.block", "block"), + ("gradio.chat", "chatinterface"), + ("gradio.component", "component"), + ("gradio.events", "helpers"), + ("gradio.data_classes", "helpers"), + ("gradio.exceptions", "helpers"), + ("gradio.external", "helpers"), + ("gradio.flag", "flagging"), + ("gradio.helpers", "helpers"), + ("gradio.interface", "interface"), + ("gradio.layout", "layout"), + ("gradio.route", "routes"), + ("gradio.theme", "themes"), + ("gradio_client.", "py-client"), + ("gradio.utils", "helpers"), + ("gradio.renderable", "renderable"), + ("gradio.validators", "validators"), + ("gradio.caching", "helpers"), +] + + +@lru_cache(maxsize=10) +def _get_module_documentation_group(modname) -> str: + for prefix, group in _module_prefixes: + if modname.startswith(prefix): + return group + raise ValueError(f"No known documentation group for module {modname!r}") + + +def document(*fns, inherit=False, documentation_group=None): + """ + Defines the @document decorator which adds classes or functions to the Gradio + documentation at www.gradio.app/docs. + + Usage examples: + - Put @document() above a class to document the class and its constructor. + - Put @document("fn1", "fn2") above a class to also document methods fn1 and fn2. + - Put @document("*fn3") with an asterisk above a class to document the instance attribute methods f3. + """ + _documentation_group = documentation_group + + def inner_doc(cls): + functions = list(fns) + if hasattr(cls, "EVENTS"): + functions += cls.EVENTS + if inherit: + classes_inherit_documentation[cls] = None + + documentation_group = _documentation_group # avoid `nonlocal` reassignment + if _documentation_group is None: + try: + modname = inspect.getmodule(cls).__name__ # type: ignore + if modname.startswith("gradio.") or modname.startswith( + "gradio_client." + ): + documentation_group = _get_module_documentation_group(modname) + else: + # Then this is likely a custom Gradio component that we do not include in the documentation + pass + except Exception as exc: + warnings.warn(f"Could not get documentation group for {cls}: {exc}") + classes_to_document[documentation_group].append((cls, functions)) + return cls + + return inner_doc + + +def document_fn(fn: Callable, cls) -> tuple[str, list[dict], dict, str | None]: + """ + Generates documentation for any function. + Parameters: + fn: Function to document + Returns: + description: General description of fn + parameters: A list of dicts for each parameter, storing data for the parameter name, annotation and doc + return: A dict storing data for the returned annotation and doc + example: Code for an example use of the fn + """ + doc_str = inspect.getdoc(fn) or "" + doc_lines = doc_str.split("\n") + signature = inspect.signature(fn) + description, parameters, returns, examples = [], {}, [], [] + mode = "description" + current_parameter = None + base_indent = None + for line in doc_lines: + line = line.rstrip() + if line == "Parameters:": + mode = "parameter" + base_indent = None + elif line.startswith("Example:"): + mode = "example" + base_indent = None + if "(" in line and ")" in line: + c = line.split("(")[1].split(")")[0] + if c != cls.__name__: + mode = "ignore" + elif line == "Returns:": + mode = "return" + base_indent = None + else: + if mode == "description": + description.append(line if line.strip() else "
") + continue + if not (line.startswith(" ") or line.strip() == ""): + print(line) + if not (line.startswith(" ") or line.strip() == ""): + raise SyntaxError( + f"Documentation format for {fn.__name__} has format error in line: {line}" + ) + line = line[4:] + if mode == "parameter": + if ": " in line and not line.startswith(" "): + colon_index = line.index(": ") + if colon_index < -1: + raise SyntaxError( + f"Documentation format for {fn.__name__} has format error in line: {line}" + ) + current_parameter = line[:colon_index] + parameter_doc = line[colon_index + 2 :] + parameters[current_parameter] = parameter_doc + base_indent = None + elif current_parameter and line.strip(): + if base_indent is None: + base_indent = len(line) - len(line.lstrip()) + if base_indent > 0 and line.startswith(" " * base_indent): + line = line[base_indent:] + parameters[current_parameter] += "\n" + line + elif mode == "return": + returns.append(line) + elif mode == "example": + examples.append(line) + description_doc = " ".join(description) + parameter_docs = [] + for param_name, param in signature.parameters.items(): + if param_name.startswith("_"): + continue + if param_name == "self": + continue + if param_name in ["kwargs", "args"] and param_name not in parameters: + continue + parameter_doc = { + "name": param_name, + "annotation": param.annotation, + "doc": parameters.get(param_name), + } + if param_name in parameters: + del parameters[param_name] + if param.default != inspect.Parameter.empty: + default = param.default + if isinstance(default, str): + default = '"' + default + '"' + if default.__class__.__module__ != "builtins": + default = f"{default.__class__.__name__}()" + parameter_doc["default"] = default + elif parameter_doc["doc"] is not None: + if "kwargs" in parameter_doc["doc"]: + parameter_doc["kwargs"] = True + if "args" in parameter_doc["doc"]: + parameter_doc["args"] = True + parameter_docs.append(parameter_doc) + if parameters: + raise ValueError( + f"Documentation format for {fn.__name__} documents " + f"nonexistent parameters: {', '.join(parameters.keys())}. " + f"Valid parameters: {', '.join(signature.parameters.keys())}" + ) + if len(returns) == 0: + return_docs = {} + else: + return_doc_text = "\n".join(returns) + return_docs = { + "annotation": signature.return_annotation, + "doc": return_doc_text, + } + examples_doc = "\n".join(examples) if len(examples) > 0 else None + return description_doc, parameter_docs, return_docs, examples_doc + + +def document_cls(cls): + doc_str = inspect.getdoc(cls) + if doc_str is None: + return "", {}, "" + tags = {} + description_lines = [] + mode = "description" + for line in doc_str.split("\n"): + line = line.rstrip() + if line.endswith(":") and " " not in line: + mode = line[:-1].lower() + tags[mode] = [] + elif line.split(" ")[0].endswith(":") and not line.startswith(" "): + tag = line[: line.index(":")].lower() + value = line[line.index(":") + 2 :] + tags[tag] = value + elif mode == "description": + description_lines.append(line if line.strip() else "
") + else: + if not (line.startswith(" ") or not line.strip()): + raise SyntaxError( + f"Documentation format for {cls.__name__} has format error in line: {line}" + ) + tags[mode].append(line[4:]) + if "example" in tags: + example = "\n".join(tags["example"]) + del tags["example"] + else: + example = None + for key, val in tags.items(): + if isinstance(val, list): + tags[key] = "
".join(val) + description = " ".join(description_lines).replace("\n", "
") + return description, tags, example + + +def generate_documentation(): + documentation = {} + for mode, class_list in classes_to_document.items(): + documentation[mode] = [] + for cls, fns in class_list: + fn_to_document = ( + cls + if inspect.isfunction(cls) or dataclasses.is_dataclass(cls) + else cls.__init__ + ) + _, parameter_doc, return_doc, _ = document_fn(fn_to_document, cls) + if ( + hasattr(cls, "preprocess") + and callable(cls.preprocess) # type: ignore + and hasattr(cls, "postprocess") + and callable(cls.postprocess) # type: ignore + ): + preprocess_doc = document_fn(cls.preprocess, cls) # type: ignore + postprocess_doc = document_fn(cls.postprocess, cls) # type: ignore + preprocess_doc, postprocess_doc = ( + { + "parameter_doc": preprocess_doc[1], + "return_doc": preprocess_doc[2], + }, + { + "parameter_doc": postprocess_doc[1], + "return_doc": postprocess_doc[2], + }, + ) + cls_description, cls_tags, cls_example = document_cls(cls) + cls_documentation = { + "class": cls, + "name": cls.__name__, + "description": cls_description, + "tags": cls_tags, + "parameters": parameter_doc, + "returns": return_doc, + "example": cls_example, + "fns": [], + } + if ( + hasattr(cls, "preprocess") + and callable(cls.preprocess) # type: ignore + and hasattr(cls, "postprocess") + and callable(cls.postprocess) # type: ignore + ): + cls_documentation["preprocess"] = preprocess_doc # type: ignore + cls_documentation["postprocess"] = postprocess_doc # type: ignore + for fn_name in fns: + instance_attribute_fn = fn_name.startswith("*") + if instance_attribute_fn: + fn_name = fn_name[1:] + # Instance attribute fns are classes + # whose __call__ method determines their behavior + fn = getattr(cls(), fn_name).__call__ + else: + fn = getattr(cls, fn_name) + if not callable(fn): + description_doc = str(fn) + parameter_docs = {} + return_docs = {} + examples_doc = "" + override_signature = f"gr.{cls.__name__}.{fn_name}" + else: + ( + description_doc, + parameter_docs, + return_docs, + examples_doc, + ) = document_fn(fn, cls) + if fn_name in getattr(cls, "EVENTS", []): + parameter_docs = parameter_docs[1:] + override_signature = None + if instance_attribute_fn: + description_doc = extract_instance_attr_doc(cls, fn_name) + cls_documentation["fns"].append( + { + "fn": fn, + "name": fn_name, + "description": description_doc, + "tags": {}, + "parameters": parameter_docs, + "returns": return_docs, + "example": examples_doc, + "override_signature": override_signature, + } + ) + documentation[mode].append(cls_documentation) + if cls in classes_inherit_documentation: + classes_inherit_documentation[cls] = cls_documentation["fns"] + for mode, class_list in classes_to_document.items(): + for i, (cls, _) in enumerate(class_list): + for super_class, fns in classes_inherit_documentation.items(): + if ( + inspect.isclass(cls) + and issubclass(cls, super_class) + and cls != super_class + ): + for inherited_fn in fns: + inherited_fn = dict(inherited_fn) + try: + inherited_fn["description"] = extract_instance_attr_doc( + cls, inherited_fn["name"] + ) + except ValueError: + pass + documentation[mode][i]["fns"].append(inherited_fn) + return documentation diff --git a/client/python/gradio_client/exceptions.py b/client/python/gradio_client/exceptions.py new file mode 100644 index 0000000..961a750 --- /dev/null +++ b/client/python/gradio_client/exceptions.py @@ -0,0 +1,43 @@ +class SerializationSetupError(ValueError): + """Raised when a serializers cannot be set up correctly.""" + + pass + + +class AuthenticationError(ValueError): + """Raised when the client is unable to authenticate itself to a Gradio app due to invalid or missing credentials.""" + + pass + + +class ValidationError(ValueError): + """Raised when the data that is passed into the Gradio app fails developer-defined validation.""" + + pass + + +class AppError(ValueError): + """Raised when the upstream Gradio app throws an error because of the value submitted by the client.""" + + def __init__( + self, + message: str = "Error raised.", + duration: float | None = 10, + visible: bool = True, + title: str = "Error", + print_exception: bool = True, + ): + """ + Parameters: + message: The error message to be displayed to the user. Can be HTML, which will be rendered in the modal. + duration: The duration in seconds to display the error message. If None or 0, the error message will be displayed until the user closes it. + visible: Whether the error message should be displayed in the UI. + title: The title to be displayed to the user at the top of the error modal. + print_exception: Whether to print traceback of the error to the console when the error is raised. + """ + self.title = title + self.message = message + self.duration = duration + self.visible = visible + self.print_exception = print_exception + super().__init__(self.message) diff --git a/client/python/gradio_client/package.json b/client/python/gradio_client/package.json new file mode 100644 index 0000000..aeaddd5 --- /dev/null +++ b/client/python/gradio_client/package.json @@ -0,0 +1,8 @@ +{ + "name": "gradio_client", + "version": "2.5.0", + "description": "", + "python": "true", + "main_changeset": true, + "private": true +} diff --git a/client/python/gradio_client/snippet.py b/client/python/gradio_client/snippet.py new file mode 100644 index 0000000..0e82155 --- /dev/null +++ b/client/python/gradio_client/snippet.py @@ -0,0 +1,276 @@ +"""Centralized code snippet generation for Gradio API endpoints. Generates Python, JavaScript, and cURL code snippets from API info dicts.""" + +import copy +import json +import re +from typing import Any + +BLOB_COMPONENTS = { + "Audio", + "DownloadButton", + "File", + "Image", + "ImageSlider", + "Model3D", + "UploadButton", + "Video", +} + + +def _is_file_data(obj: Any) -> bool: + return ( + isinstance(obj, dict) + and "url" in obj + and obj.get("url") + and "meta" in obj + and isinstance(obj.get("meta"), dict) + and obj["meta"].get("_type") == "gradio.FileData" + ) + + +def _has_file_data(obj: Any) -> bool: + if isinstance(obj, dict): + if _is_file_data(obj): + return True + return any(_has_file_data(v) for v in obj.values()) + if isinstance(obj, (list, tuple)): + return any(_has_file_data(item) for item in obj) + return False + + +def _replace_file_data_py(obj: Any) -> Any: + if isinstance(obj, dict) and _is_file_data(obj): + return f"handle_file('{obj['url']}')" + if isinstance(obj, (list, tuple)): + return [_replace_file_data_py(item) for item in obj] + if isinstance(obj, dict): + return {k: _replace_file_data_py(v) for k, v in obj.items()} + return obj + + +def _simplify_file_data(obj: Any) -> Any: + if isinstance(obj, dict) and _is_file_data(obj): + return {"path": obj["url"], "meta": {"_type": "gradio.FileData"}} + if isinstance(obj, (list, tuple)): + return [_simplify_file_data(item) for item in obj] + if isinstance(obj, dict): + return {k: _simplify_file_data(v) for k, v in obj.items()} + return obj + + +_UNQUOTED = "UNQUOTED_GRADIO_" + + +def _stringify_py(obj: Any) -> str: + def _prepare(o: Any) -> Any: + if o is None: + return f"{_UNQUOTED}None" + if isinstance(o, bool): + return f"{_UNQUOTED}True" if o else f"{_UNQUOTED}False" + if isinstance(o, str) and o.startswith("handle_file(") and o.endswith(")"): + return f"{_UNQUOTED}{o}" + if isinstance(o, (list, tuple)): + return [_prepare(item) for item in o] + if isinstance(o, dict): + return {k: _prepare(v) for k, v in o.items()} + return o + + prepared = _prepare(obj) + result = json.dumps(prepared, default=str) + result = re.sub( + rf'"{_UNQUOTED}(handle_file\([^)]*\))"', + r"\1", + result, + ) + result = result.replace(f'"{_UNQUOTED}None"', "None") + result = result.replace(f'"{_UNQUOTED}True"', "True") + result = result.replace(f'"{_UNQUOTED}False"', "False") + return result + + +def _represent_value(value: Any, python_type: str | None, lang: str) -> str: + if python_type is None: + return "None" if lang == "py" else "null" + if value is None: + return "None" if lang == "py" else "null" + if python_type in ("string", "str"): + return f'"{value}"' + if python_type == "number": + return str(value) + if python_type in ("boolean", "bool"): + if lang == "py": + return "True" if value else "False" + return str(value).lower() if isinstance(value, bool) else str(value) + if python_type == "List[str]": + return json.dumps(value) + if python_type.startswith("Literal['"): + return f'"{value}"' + + if isinstance(value, str): + if value == "": + return "None" if lang == "py" else "null" + return value + + value = copy.deepcopy(value) + if lang == "bash": + value = _simplify_file_data(value) + if lang == "py": + value = _replace_file_data_py(value) + return _stringify_py(value) + + +def _get_param_value(param: dict) -> Any: + if param.get("parameter_has_default"): + return param.get("parameter_default") + return param.get("example_input") + + +def generate_python_snippet( + api_name: str, + params: list[dict], + src: str, +) -> str: + has_file = any(_has_file_data(p.get("example_input")) for p in params) + imports = "from gradio_client import Client" + if has_file: + imports += ", handle_file" + + lines = [imports, ""] + lines.append(f'client = Client("{src}")') + + predict_args = [] + for p in params: + name = p.get("parameter_name") or p.get("label", "input") + value = _get_param_value(p) + ptype = p.get("python_type", {}).get("type") + formatted = _represent_value(value, ptype, "py") + predict_args.append(f"\t{name}={formatted},") + + lines.append("result = client.predict(") + lines.extend(predict_args) + lines.append(f'\tapi_name="{api_name}",') + lines.append(")") + lines.append("print(result)") + + return "\n".join(lines) + + +def generate_js_snippet( + api_name: str, + params: list[dict], + src: str, +) -> str: + blob_params = [p for p in params if p.get("component") in BLOB_COMPONENTS] + + lines = ['import { Client } from "@gradio/client";', ""] + + for i, bp in enumerate(blob_params): + example = bp.get("example_input", {}) + url = example.get("url", "") if isinstance(example, dict) else "" + component = bp.get("component", "") + lines.append(f'const response_{i} = await fetch("{url}");') + lines.append(f"const example{component} = await response_{i}.blob();") + + if blob_params: + lines.append("") + + lines.append(f'const client = await Client.connect("{src}");') + + blob_component_names = {bp.get("component") for bp in blob_params} + + predict_args = [] + for p in params: + name = p.get("parameter_name") or p.get("label", "input") + component = p.get("component", "") + if component in blob_component_names: + predict_args.append(f"\t\t{name}: example{component},") + else: + value = _get_param_value(p) + ptype = p.get("python_type", {}).get("type") + formatted = _represent_value(value, ptype, "js") + predict_args.append(f"\t\t{name}: {formatted},") + + lines.append(f'const result = await client.predict("{api_name}", {{') + lines.extend(predict_args) + lines.append("});") + lines.append("") + lines.append("console.log(result.data);") + + return "\n".join(lines) + + +def generate_bash_snippet( + api_name: str, + params: list[dict], + root: str, + api_prefix: str = "/", +) -> str: + normalised_root = root.rstrip("/") + normalised_prefix = api_prefix if api_prefix else "/" + endpoint_name = api_name.lstrip("/") + + has_file = any(_has_file_data(p.get("example_input")) for p in params) + upload_url = f"{normalised_root}{normalised_prefix}upload" + + lines: list[str] = [] + + file_param_names: list[str] = [] + if has_file: + for p in params: + if _has_file_data(p.get("example_input")): + name = p.get("parameter_name") or p.get("label", "input") + file_param_names.append(name) + lines.append( + f"FILE_PATH=$(curl -s -X POST {upload_url}" + " -F 'files=@/path/to/your/file'" + " | tr -d '[]\" ')" + ) + lines.append("") + + data_dict = {} + for p in params: + name = p.get("parameter_name") or p.get("label", "input") + if name in file_param_names: + data_dict[name] = "FILE_PATH_PLACEHOLDER" + else: + value = _get_param_value(p) + ptype = p.get("python_type", {}).get("type") + formatted = _represent_value(value, ptype, "bash") + data_dict[name] = formatted + + data_entries = ", ".join(f'"{k}": {v}' for k, v in data_dict.items()) + data_str = "{" + data_entries + "}" + for _ in file_param_names: + replacement = '{"path": "\'$FILE_PATH\'", "meta": {"_type": "gradio.FileData"}}' + data_str = data_str.replace("FILE_PATH_PLACEHOLDER", replacement) + + base_url = f"{normalised_root}{normalised_prefix}call/v2/{endpoint_name}" + get_url = f"{normalised_root}{normalised_prefix}call/{endpoint_name}" + + lines.extend( + [ + f'curl -X POST {base_url} -s -H "Content-Type: application/json" \\', + f" -d '{data_str}' \\", + " | awk -F'\"' '{ print $4}' \\", + f" | read EVENT_ID; curl -N {get_url}/$EVENT_ID", + ] + ) + + return "\n".join(lines) + + +def generate_code_snippets( + api_name: str, + endpoint_info: dict, + root: str, + space_id: str | None = None, + api_prefix: str = "/", +) -> dict[str, str]: + params = endpoint_info.get("parameters", []) + src = space_id or root + + return { + "python": generate_python_snippet(api_name, params, src), + "javascript": generate_js_snippet(api_name, params, src), + "bash": generate_bash_snippet(api_name, params, root, api_prefix), + } diff --git a/client/python/gradio_client/templates/discord_chat.py b/client/python/gradio_client/templates/discord_chat.py new file mode 100644 index 0000000..229a8f6 --- /dev/null +++ b/client/python/gradio_client/templates/discord_chat.py @@ -0,0 +1,192 @@ +import asyncio +import os +import threading +from threading import Event + +import discord +import gradio as gr +from discord import Permissions +from discord.ext import commands +from discord.utils import oauth_url + +import gradio_client as grc +from gradio_client.utils import QueueError + +event = Event() + +DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") + + +async def wait(job): + while not job.done(): + await asyncio.sleep(0.2) + + +def get_client(session: str | None = None) -> grc.Client: + client = grc.Client("<>", token=os.getenv("HF_TOKEN")) + if session: + client.session_hash = session + return client + + +def truncate_response(response: str) -> str: + ending = "...\nTruncating response to 2000 characters due to discord api limits." + if len(response) > 2000: + return response[: 2000 - len(ending)] + ending + else: + return response + + +intents = discord.Intents.default() +intents.message_content = True +bot = commands.Bot(command_prefix="/", intents=intents) + + +@bot.event +async def on_ready(): + print(f"Logged in as {bot.user} (ID: {bot.user.id})") + synced = await bot.tree.sync() + print(f"Synced commands: {', '.join([s.name for s in synced])}.") + event.set() + print("------") + + +thread_to_client = {} +thread_to_user = {} + + +@bot.hybrid_command( + name="<>", + description="Enter some text to chat with the bot! Like this: /<> Hello, how are you?", +) +async def chat(ctx, prompt: str): + if ctx.author.id == bot.user.id: + return + try: + message = await ctx.send("Creating thread...") + + thread = await message.create_thread(name=prompt) + loop = asyncio.get_running_loop() + client = await loop.run_in_executor(None, get_client, None) + job = client.submit(prompt, api_name="/<>") + await wait(job) + + try: + job.result() + response = job.outputs()[-1] + await thread.send(truncate_response(response)) + thread_to_client[thread.id] = client + thread_to_user[thread.id] = ctx.author.id + except QueueError: + await thread.send( + "The gradio space powering this bot is really busy! Please try again later!" + ) + + except Exception as e: + print(f"{e}") + + +async def continue_chat(message): + """Continues a given conversation based on chathistory""" + try: + client = thread_to_client[message.channel.id] + prompt = message.content + job = client.submit(prompt, api_name="/<>") + await wait(job) + try: + job.result() + response = job.outputs()[-1] + await message.reply(truncate_response(response)) + except QueueError: + await message.reply( + "The gradio space powering this bot is really busy! Please try again later!" + ) + + except Exception as e: + print(f"Error: {e}") + + +@bot.event +async def on_message(message): + """Continue the chat""" + try: + if not message.author.bot: + if message.channel.id in thread_to_user: + if thread_to_user[message.channel.id] == message.author.id: + await continue_chat(message) + else: + await bot.process_commands(message) + + except Exception as e: + print(f"Error: {e}") + + +# running in thread +def run_bot(): + if not DISCORD_TOKEN: + print("DISCORD_TOKEN NOT SET") + event.set() + else: + bot.run(DISCORD_TOKEN) + + +threading.Thread(target=run_bot).start() + +event.wait() + +if not DISCORD_TOKEN: + welcome_message = """ + + ## You have not specified a DISCORD_TOKEN, which means you have not created a bot account. Please follow these steps: + + ### 1. Go to https://discord.com/developers/applications and click 'New Application' + + ### 2. Give your bot a name 🤖 + + ![](https://gradio-builds.s3.amazonaws.com/demo-files/discordbots/BotName.png) + + ## 3. In Settings > Bot, click the 'Reset Token' button to get a new token. Write it down and keep it safe 🔐 + + ![](https://gradio-builds.s3.amazonaws.com/demo-files/discordbots/ResetToken.png) + + ## 4. Optionally make the bot public if you want anyone to be able to add it to their servers + + ## 5. Scroll down and enable 'Message Content Intent' under 'Priviledged Gateway Intents' + + ![](https://gradio-builds.s3.amazonaws.com/demo-files/discordbots/MessageContentIntent.png) + + ## 6. Save your changes! + + ## 7. The token from step 3 is the DISCORD_TOKEN. Rerun the deploy_discord command, e.g client.deploy_discord(discord_bot_token=DISCORD_TOKEN, ...), or add the token as a space secret manually. +""" +else: + permissions = Permissions(326417525824) + url = oauth_url(bot.user.id, permissions=permissions) + welcome_message = f""" + ## Add this bot to your server by clicking this link: + + {url} + + ## How to use it? + + The bot can be triggered via `/<>` followed by your text prompt. + + This will create a thread with the bot's response to your text prompt. + You can reply in the thread (without `/<>`) to continue the conversation. + In the thread, the bot will only reply to the original author of the command. + + ⚠️ Note ⚠️: Please make sure this bot's command does have the same name as another command in your server. + + ⚠️ Note ⚠️: Bot commands do not work in DMs with the bot as of now. + """ + + +with gr.Blocks() as demo: + gr.Markdown( + f""" + # Discord bot of <> + {welcome_message} + """ + ) + +demo.launch() diff --git a/client/python/gradio_client/types.json b/client/python/gradio_client/types.json new file mode 100644 index 0000000..2fc1d42 --- /dev/null +++ b/client/python/gradio_client/types.json @@ -0,0 +1,199 @@ +{ + "SimpleSerializable": { + "type": {}, + "description": "any valid value" + }, + "StringSerializable": { + "type": "string" + }, + "ListStringSerializable": { + "type": "array", + "items": { + "type": "string" + } + }, + "BooleanSerializable": { + "type": "boolean" + }, + "NumberSerializable": { + "type": "number" + }, + "ImgSerializable": { + "type": "string", + "description": "base64 representation of an image" + }, + "FileSerializable": { + "oneOf": [ + { + "type": "string", + "description": "filepath on your computer (or URL) of file" + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "description": "name of file" }, + "data": { + "type": "string", + "description": "base64 representation of file" + }, + "size": { + "type": "integer", + "description": "size of image in bytes" + }, + "is_file": { + "type": "boolean", + "description": "true if the file has been uploaded to the server" + }, + "orig_name": { + "type": "string", + "description": "original name of the file" + } + }, + "required": ["name", "data"] + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "description": "filepath on your computer (or URL) of file" + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "description": "name of file" }, + "data": { + "type": "string", + "description": "base64 representation of file" + }, + "size": { + "type": "integer", + "description": "size of image in bytes" + }, + "is_file": { + "type": "boolean", + "description": "true if the file has been uploaded to the server" + }, + "orig_name": { + "type": "string", + "description": "original name of the file" + } + }, + "required": ["name", "data"] + } + ] + } + } + ] + }, + "SingleFileSerializable": { + "oneOf": [ + { + "type": "string", + "description": "filepath on your computer (or URL) of file" + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "description": "name of file" }, + "data": { + "type": "string", + "description": "base64 representation of file" + }, + "size": { + "type": "integer", + "description": "size of image in bytes" + }, + "is_file": { + "type": "boolean", + "description": "true if the file has been uploaded to the server" + }, + "orig_name": { + "type": "string", + "description": "original name of the file" + } + }, + "required": ["name", "data"] + } + ] + }, + "MultipleFileSerializable": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string", + "description": "filepath on your computer (or URL) of file" + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "description": "name of file" }, + "data": { + "type": "string", + "description": "base64 representation of file" + }, + "size": { + "type": "integer", + "description": "size of image in bytes" + }, + "is_file": { + "type": "boolean", + "description": "true if the file has been uploaded to the server" + }, + "orig_name": { + "type": "string", + "description": "original name of the file" + } + }, + "required": ["name", "data"] + } + ] + } + }, + "JSONSerializable": { + "type": {}, + "description": "any valid json" + }, + "GallerySerializable": { + "type": "array", + "items": { + "type": "array", + "items": false, + "maxSize": 2, + "minSize": 2, + "prefixItems": [ + { + "type": "object", + "properties": { + "name": { "type": "string", "description": "name of file" }, + "data": { + "type": "string", + "description": "base64 representation of file" + }, + "size": { + "type": "integer", + "description": "size of image in bytes" + }, + "is_file": { + "type": "boolean", + "description": "true if the file has been uploaded to the server" + }, + "orig_name": { + "type": "string", + "description": "original name of the file" + } + }, + "required": ["name", "data"] + }, + { + "oneOf": [ + { "type": "string", "description": "caption of image" }, + { "type": "null" } + ] + } + ] + } + } +} diff --git a/client/python/gradio_client/utils.py b/client/python/gradio_client/utils.py new file mode 100644 index 0000000..7ee9849 --- /dev/null +++ b/client/python/gradio_client/utils.py @@ -0,0 +1,1379 @@ +from __future__ import annotations + +import asyncio +import base64 +import concurrent.futures +import copy +import inspect +import json +import mimetypes +import os +import pkgutil +import re +import secrets +import shutil +import tempfile +import time +import warnings +from collections import deque +from collections.abc import Callable, Coroutine +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from pathlib import Path +from threading import Lock +from typing import ( + TYPE_CHECKING, + Any, + Literal, + NewType, + TypedDict, + Union, + get_args, + get_origin, + get_type_hints, +) + +import fsspec.asyn +import httpx +import huggingface_hub +from huggingface_hub import SpaceStage + +if TYPE_CHECKING: + from gradio_client.data_classes import ParameterInfo + +API_URL = "api/predict/" +SSE_URL_V0 = "queue/join" +SSE_DATA_URL_V0 = "queue/data" +SSE_URL = "queue/data" +SSE_DATA_URL = "queue/join" +WS_URL = "queue/join" +UPLOAD_URL = "upload" +LOGIN_URL = "login" +CONFIG_URL = "config" +API_INFO_URL = "info?all_endpoints=True" +RAW_API_INFO_URL = "info?serialize=False" +SPACE_FETCHER_URL = "https://gradio-space-api-fetcher-v2.hf.space/api" +RESET_URL = "reset" +SPACE_URL = "https://hf.space/{}" +HEARTBEAT_URL = "heartbeat/{session_hash}" +CANCEL_URL = "cancel" + +STATE_COMPONENT = "state" +INVALID_RUNTIME = [ + SpaceStage.NO_APP_FILE, + SpaceStage.CONFIG_ERROR, + SpaceStage.BUILD_ERROR, + SpaceStage.RUNTIME_ERROR, + SpaceStage.PAUSED, +] + +# Characters forbidden in filenames across OS and shell environments: +# < > : " / \ | ? * – forbidden on Windows +# \x00-\x1f – null byte and ASCII control characters +# \x7f – DEL character +# ` $ ! { } – shell-dangerous characters +_FORBIDDEN_RE = re.compile(r'[<>:"/\\|?*\x00-\x1f\x7f`$!{}]') + + +class Message(TypedDict, total=False): + msg: str + output: dict[str, Any] + event_id: str + rank: int + rank_eta: float + queue_size: int + success: bool + progress_data: list[dict] + log: str + level: str + + +def get_package_version() -> str: + try: + package_json_data = ( + pkgutil.get_data(__name__, "package.json").decode("utf-8").strip() # type: ignore + ) + package_data = json.loads(package_json_data) + version = package_data.get("version", "") + return version + except Exception: + return "" + + +__version__ = get_package_version() + + +class TooManyRequestsError(Exception): + """Raised when the API returns a 429 status code.""" + + pass + + +class QueueError(Exception): + """Raised when the queue is full or there is an issue adding a job to the queue.""" + + pass + + +class InvalidAPIEndpointError(Exception): + """Raised when the API endpoint is invalid.""" + + pass + + +class SpaceDuplicationError(Exception): + """Raised when something goes wrong with a Space Duplication.""" + + pass + + +class ServerMessage(str, Enum): + send_hash = "send_hash" + queue_full = "queue_full" + estimation = "estimation" + send_data = "send_data" + process_starts = "process_starts" + process_generating = "process_generating" + process_completed = "process_completed" + log = "log" + progress = "progress" + heartbeat = "heartbeat" + server_stopped = "Server stopped unexpectedly." + unexpected_error = "unexpected_error" + close_stream = "close_stream" + process_streaming = "process_streaming" + + +class Status(Enum): + """Status codes presented to client users.""" + + STARTING = "STARTING" + JOINING_QUEUE = "JOINING_QUEUE" + QUEUE_FULL = "QUEUE_FULL" + IN_QUEUE = "IN_QUEUE" + SENDING_DATA = "SENDING_DATA" + PROCESSING = "PROCESSING" + ITERATING = "ITERATING" + PROGRESS = "PROGRESS" + FINISHED = "FINISHED" + CANCELLED = "CANCELLED" + LOG = "LOG" + + @staticmethod + def ordering(status: Status) -> int: + """Order of messages. Helpful for testing.""" + order = [ + Status.STARTING, + Status.JOINING_QUEUE, + Status.QUEUE_FULL, + Status.IN_QUEUE, + Status.SENDING_DATA, + Status.PROCESSING, + Status.PROGRESS, + Status.ITERATING, + Status.FINISHED, + Status.CANCELLED, + ] + return order.index(status) + + def __lt__(self, other: Status): + return self.ordering(self) < self.ordering(other) + + @staticmethod + def msg_to_status(msg: str) -> Status: + """Map the raw message from the backend to the status code presented to users.""" + return { + ServerMessage.send_hash: Status.JOINING_QUEUE, + ServerMessage.queue_full: Status.QUEUE_FULL, + ServerMessage.estimation: Status.IN_QUEUE, + ServerMessage.send_data: Status.SENDING_DATA, + ServerMessage.process_starts: Status.PROCESSING, + ServerMessage.process_generating: Status.ITERATING, + ServerMessage.process_completed: Status.FINISHED, + ServerMessage.progress: Status.PROGRESS, + ServerMessage.log: Status.LOG, + ServerMessage.server_stopped: Status.FINISHED, + }[msg] # type: ignore + + +@dataclass +class ProgressUnit: + index: int | None + length: int | None + unit: str | None + progress: float | None + desc: str | None + + @classmethod + def from_msg(cls, data: list[dict]) -> list[ProgressUnit]: + return [ + cls( + index=d.get("index"), + length=d.get("length"), + unit=d.get("unit"), + progress=d.get("progress"), + desc=d.get("desc"), + ) + for d in data + ] + + +@dataclass +class StatusUpdate: + """Update message sent from the worker thread to the Job on the main thread.""" + + code: Status + rank: int | None + queue_size: int | None + eta: float | None + success: bool | None + time: datetime | None + progress_data: list[ProgressUnit] | None + log: tuple[str, str] | None = None + type: Literal["status", "output"] = "status" + + +@dataclass +class OutputUpdate: + """Update message sent from the worker thread to the Job on the main thread.""" + + outputs: list[Any] + success: bool + type: Literal["output"] = "output" + final: bool = False + + +Update = NewType("Update", StatusUpdate | OutputUpdate) + + +def create_initial_status_update(): + return StatusUpdate( + code=Status.STARTING, + rank=None, + queue_size=None, + eta=None, + success=None, + time=datetime.now(), + progress_data=None, + ) + + +@dataclass +class JobStatus: + """The job status. + + Keeps track of the latest status update and intermediate outputs (not yet implements). + """ + + latest_status: StatusUpdate = field(default_factory=create_initial_status_update) + outputs: list[Any] = field(default_factory=list) + + +@dataclass +class Communicator: + """Helper class to help communicate between the worker thread and main thread.""" + + lock: Lock + job: JobStatus + prediction_processor: Callable[..., tuple] + reset_url: str + should_cancel: bool = False + event_id: str | None = None + thread_complete: bool = False + request_headers: dict[str, str] | None = None + updates: asyncio.Queue[Update] = field(default_factory=asyncio.Queue) + + +######################## +# Network utils +######################## + + +def is_http_url_like(possible_url) -> bool: + """ + Check if the given value is a string that looks like an HTTP(S) URL. + """ + if not isinstance(possible_url, str): + return False + return possible_url.startswith(("http://", "https://")) + + +def probe_url(possible_url: str) -> bool: + """ + Probe the given URL to see if it responds with a 200 status code (to HEAD, then to GET). + """ + headers = {"User-Agent": "gradio (https://gradio.app/; gradio-team@huggingface.co)"} + try: + with httpx.Client() as client: + head_request = httpx.head(possible_url, headers=headers) + if head_request.status_code == 405: + return client.get(possible_url, headers=headers).is_success + return head_request.is_success + except Exception: + return False + + +def is_valid_url(possible_url: str) -> bool: + """ + Check if the given string is a valid URL. + """ + warnings.warn( + "is_valid_url should not be used. " + "Use is_http_url_like() and probe_url(), as suitable, instead.", + ) + return is_http_url_like(possible_url) and probe_url(possible_url) + + +def get_pred_from_sse_v0( + client: httpx.Client, + data: dict, + hash_data: dict, + helper: Communicator, + sse_url: str, + sse_data_url: str, + headers: dict[str, str], + cookies: dict[str, str] | None, + ssl_verify: bool, + executor: concurrent.futures.ThreadPoolExecutor, +) -> dict[str, Any] | None: + helper.thread_complete = False + future_cancel = executor.submit( + check_for_cancel, helper, headers, cookies, ssl_verify + ) + future_sse = executor.submit( + stream_sse_v0, + client, + data, + hash_data, + helper, + sse_url, + sse_data_url, + headers, + cookies, + ) + done, _ = concurrent.futures.wait( + [future_cancel, future_sse], # type: ignore + return_when=concurrent.futures.FIRST_COMPLETED, + ) + helper.thread_complete = True + + if len(done) != 1: + raise ValueError(f"Did not expect {len(done)} tasks to be done.") + for future in done: + return future.result() + + +def get_pred_from_sse_v1plus( + helper: Communicator, + headers: dict[str, str], + cookies: dict[str, str] | None, + pending_messages_per_event: dict[str, deque[Message | None]], + event_id: str, + protocol: Literal["sse_v1", "sse_v2", "sse_v2.1"], + ssl_verify: bool, + executor: concurrent.futures.ThreadPoolExecutor, +) -> dict[str, Any] | None: + helper.thread_complete = False + future_cancel = executor.submit( + check_for_cancel, helper, headers, cookies, ssl_verify + ) + future_sse = executor.submit( + stream_sse_v1plus, helper, pending_messages_per_event, event_id, protocol + ) + done, _ = concurrent.futures.wait( + [future_cancel, future_sse], # type: ignore + return_when=concurrent.futures.FIRST_COMPLETED, + ) + helper.thread_complete = True + + if len(done) != 1: + raise ValueError(f"Did not expect {len(done)} tasks to be done.") + for future in done: + exception = future.exception() + if exception: + raise exception + return future.result() + + +def check_for_cancel( + helper: Communicator, + headers: dict[str, str], + cookies: dict[str, str] | None, + ssl_verify: bool, +): + while True: + time.sleep(0.05) + with helper.lock: + if helper.should_cancel: + break + if helper.thread_complete: + raise concurrent.futures.CancelledError() + if helper.event_id: + httpx.post( + helper.reset_url, + json={"event_id": helper.event_id}, + headers=headers, + cookies=cookies, + verify=ssl_verify, + ) + raise concurrent.futures.CancelledError() + + +def stream_sse_v0( + client: httpx.Client, + data: dict, + hash_data: dict, + helper: Communicator, + sse_url: str, + sse_data_url: str, + headers: dict[str, str], + cookies: dict[str, str] | None, +) -> dict[str, Any]: + try: + with client.stream( + "GET", + sse_url, + params=hash_data, + headers=headers, + cookies=cookies, + ) as response: + for line in response.iter_lines(): + line = line.rstrip("\n") + if len(line) == 0: + continue + if line.startswith("data:"): + resp = json.loads(line[5:]) + if resp["msg"] in [ServerMessage.log, ServerMessage.heartbeat]: + continue + with helper.lock: + has_progress = "progress_data" in resp + status_update = StatusUpdate( + code=Status.msg_to_status(resp["msg"]), + queue_size=resp.get("queue_size"), + rank=resp.get("rank", None), + success=resp.get("success"), + time=datetime.now(), + eta=resp.get("rank_eta"), + progress_data=ProgressUnit.from_msg(resp["progress_data"]) + if has_progress + else None, + ) + output = resp.get("output", {}).get("data", []) + if output and status_update.code != Status.FINISHED: + try: + result = helper.prediction_processor(*output) + except Exception as e: + result = [e] + helper.job.outputs.append(result) + helper.job.latest_status = status_update + if helper.thread_complete: + raise concurrent.futures.CancelledError() + if resp["msg"] == "queue_full": + raise QueueError("Queue is full! Please try again.") + elif resp["msg"] == "send_data": + event_id = resp["event_id"] + helper.event_id = event_id + req = client.post( + sse_data_url, + json={"event_id": event_id, **data, **hash_data}, + headers=headers, + cookies=cookies, + ) + req.raise_for_status() + elif resp["msg"] == "process_completed": + return resp["output"] + else: + raise ValueError(f"Unexpected message: {line}") + raise ValueError("Did not receive process_completed message.") + except concurrent.futures.CancelledError: + raise + + +def stream_sse_v1plus( + helper: Communicator, + pending_messages_per_event: dict[str, deque[Message | None]], + event_id: str, + protocol: Literal["sse_v1", "sse_v2", "sse_v2.1", "sse_v3"], +) -> dict[str, Any]: + try: + pending_messages = pending_messages_per_event[event_id] + pending_responses_for_diffs = None + + while True: + if len(pending_messages) > 0: + msg = pending_messages.popleft() + else: + time.sleep(0.05) + continue + + if msg is None or helper.thread_complete: + raise concurrent.futures.CancelledError() + + with helper.lock: + log_message = None + if msg["msg"] == ServerMessage.log: + log = msg.get("log") + level = msg.get("level") + if log and level: + log_message = (log, level) + status_update = StatusUpdate( + code=Status.msg_to_status(msg["msg"]), + queue_size=msg.get("queue_size"), + rank=msg.get("rank", None), + success=msg.get("success"), + time=datetime.now(), + eta=msg.get("rank_eta"), + progress_data=ProgressUnit.from_msg(msg["progress_data"]) + if "progress_data" in msg + else None, + log=log_message, + ) + output = msg.get("output", {}).get("data", []) + if msg["msg"] == ServerMessage.process_generating and protocol in [ + "sse_v2", + "sse_v2.1", + "sse_v3", + ]: + if pending_responses_for_diffs is None: + pending_responses_for_diffs = list(output) + else: + for i, value in enumerate(output): + prev_output = pending_responses_for_diffs[i] + new_output = apply_diff(prev_output, value) + pending_responses_for_diffs[i] = new_output + output[i] = new_output + + if output and status_update.code != Status.FINISHED: + try: + result = helper.prediction_processor(*output) + except Exception as e: + result = [e] + helper.job.outputs.append(result) + helper.updates.put_nowait( + OutputUpdate(outputs=result, success=msg.get("success", True)) + ) + helper.job.latest_status = status_update + helper.updates.put_nowait(status_update) + if msg["msg"] == ServerMessage.process_completed: + del pending_messages_per_event[event_id] + if not msg.get("success", True): + # Create a new copy of the error dict so we + # can preserve the error message (it gets popped later) + output = dict(msg["output"].items()) + else: + output = msg["output"] + helper.updates.put_nowait( + OutputUpdate( + outputs=output, + final=True, + success=msg.get("success", True), + ) + ) + return msg["output"] + elif msg["msg"] == ServerMessage.server_stopped: + raise ValueError("Server stopped.") + + except concurrent.futures.CancelledError: + raise + + +def apply_diff(obj, diff): + obj = copy.deepcopy(obj) + + def apply_edit(target, path, action, value): + if len(path) == 0: + if action == "replace": + return value + elif action == "append": + return target + value + else: + raise ValueError(f"Unsupported action: {action}") + + current = target + for i in range(len(path) - 1): + current = current[path[i]] + + last_path = path[-1] + if action == "replace": + current[last_path] = value + elif action == "append": + current[last_path] += value + elif action == "add": + if isinstance(current, list): + current.insert(int(last_path), value) + else: + current[last_path] = value + elif action == "delete": + if isinstance(current, list): + del current[int(last_path)] + else: + del current[last_path] + else: + raise ValueError(f"Unknown action: {action}") + + return target + + for action, path, value in diff: + obj = apply_edit(obj, path, action, value) + + return obj + + +######################## +# Data processing utils +######################## + + +def create_tmp_copy_of_file(file_path: str, dir: str | None = None) -> str: + directory = Path(dir or tempfile.gettempdir()) / secrets.token_hex(20) + directory.mkdir(exist_ok=True, parents=True) + dest = directory / Path(file_path).name + shutil.copy2(file_path, dest) + return str(dest.resolve()) + + +def download_tmp_copy_of_file( + url_path: str, token: str | None = None, dir: str | None = None +) -> str: + """Kept for backwards compatibility for 3.x spaces.""" + if dir is not None: + os.makedirs(dir, exist_ok=True) + headers = {"Authorization": "Bearer " + token} if token else {} + directory = Path(dir or tempfile.gettempdir()) / secrets.token_hex(20) + directory.mkdir(exist_ok=True, parents=True) + file_path = directory / Path(url_path).name + + with httpx.stream( + "GET", url_path, headers=headers, follow_redirects=True + ) as response: + response.raise_for_status() + with open(file_path, "wb") as f: + for chunk in response.iter_raw(): + f.write(chunk) + return str(file_path.resolve()) + + +def get_mimetype(filename: str) -> str | None: + filename_lower = filename.lower() + if filename_lower.endswith(".vtt"): + return "text/vtt" + if filename_lower.endswith(".webp"): + return "image/webp" + mimetype = mimetypes.guess_type(filename)[0] + if mimetype is not None: + mimetype = mimetype.replace("x-wav", "wav").replace("x-flac", "flac") + return mimetype + + +def get_extension(encoding: str) -> str | None: + encoding = encoding.replace("audio/wav", "audio/x-wav") + type = mimetypes.guess_type(encoding)[0] + if type == "audio/flac": # flac is not supported by mimetypes + return "flac" + elif type is None: + return None + extension = mimetypes.guess_extension(type) + if extension is not None and extension.startswith("."): + extension = extension[1:] + return extension + + +def is_valid_file(file_path: str, file_types: list[str]) -> bool: + mime_type = get_mimetype(file_path) + for file_type in file_types: + if file_type == "file": + return True + if file_type.startswith("."): + file_type = file_type.lstrip(".").lower() + file_ext = Path(file_path).suffix.lstrip(".").lower() + if file_type == file_ext: + return True + elif mime_type is not None and mime_type.startswith(f"{file_type}/"): + return True + return False + + +def encode_file_to_base64(f: str | Path): + with open(f, "rb") as file: + encoded_string = base64.b64encode(file.read()) + base64_str = str(encoded_string, "utf-8") + mimetype = get_mimetype(str(f)) + return ( + "data:" + + (mimetype if mimetype is not None else "") + + ";base64," + + base64_str + ) + + +def encode_url_to_base64(url: str): + resp = httpx.get(url) + resp.raise_for_status() + encoded_string = base64.b64encode(resp.content) + base64_str = str(encoded_string, "utf-8") + mimetype = get_mimetype(url) + return ( + "data:" + (mimetype if mimetype is not None else "") + ";base64," + base64_str + ) + + +def encode_url_or_file_to_base64(path: str | Path): + path = str(path) + if is_http_url_like(path): + return encode_url_to_base64(path) + return encode_file_to_base64(path) + + +def download_byte_stream(url: str, token=None): + arr = bytearray() + headers = {"Authorization": "Bearer " + token} if token else {} + with httpx.stream("GET", url, headers=headers) as r: + for data in r.iter_bytes(): + arr += data + yield data + yield arr + + +def decode_base64_to_binary(encoding: str) -> tuple[bytes, str | None]: + extension = get_extension(encoding) + data = encoding.rsplit(",", 1)[-1] + return base64.b64decode(data), extension + + +def strip_invalid_filename_characters(filename: str, max_bytes: int = 200) -> str: + """ + Strips invalid characters from a filename and ensures it does not exceed the maximum byte length. + Only removes characters that are truly dangerous for file systems: path separators, + null bytes, control characters, and shell-dangerous characters. Preserves all other + characters including parentheses, brackets, unicode characters, etc. + The filename may include an extension (in which case it is preserved exactly as is), + or could be just a name without an extension. + """ + name, ext = os.path.splitext(filename) + name = _FORBIDDEN_RE.sub("", name) + # Also sanitize the extension (excluding the leading dot) + if ext: + ext = "." + _FORBIDDEN_RE.sub("", ext[1:]) + # If the stem was stripped entirely but an extension exists, use a + # fallback name so that the extension is not mistaken for a dotfile + # stem (e.g. "#.txt" → ".txt" → Path(".txt").suffix == ""). + if not name and ext: + name = "file" + filename = name + ext + filename_len = len(filename.encode()) + if filename_len > max_bytes: + while filename_len > max_bytes: + if len(name) == 0: + break + name = name[:-1] + filename = name + ext + filename_len = len(filename.encode()) + return filename + + +def sanitize_parameter_names(original_name: str) -> str: + """Cleans up a Python parameter name to make the API info more readable.""" + return ( + "".join([char for char in original_name if char.isalnum() or char in " _"]) + .replace(" ", "_") + .lower() + ) + + +def decode_base64_to_file( + encoding: str, + file_path: str | None = None, + dir: str | Path | None = None, + prefix: str | None = None, +): + directory = Path(dir or tempfile.gettempdir()) / secrets.token_hex(20) + directory.mkdir(exist_ok=True, parents=True) + data, extension = decode_base64_to_binary(encoding) + if file_path is not None and prefix is None: + filename = Path(file_path).name + prefix = filename + if "." in filename: + prefix = filename[0 : filename.index(".")] + extension = filename[filename.index(".") + 1 :] + + if prefix is not None: + prefix = strip_invalid_filename_characters(prefix) + + if extension is None: + file_obj = tempfile.NamedTemporaryFile( + delete=False, prefix=prefix, dir=directory + ) + else: + file_obj = tempfile.NamedTemporaryFile( + delete=False, + prefix=prefix, + suffix="." + extension, + dir=directory, + ) + file_obj.write(data) + file_obj.flush() + return file_obj + + +def dict_or_str_to_json_file(jsn: str | dict | list, dir: str | Path | None = None): + if dir is not None: + os.makedirs(dir, exist_ok=True) + + file_obj = tempfile.NamedTemporaryFile( + delete=False, suffix=".json", dir=dir, mode="w+" + ) + if isinstance(jsn, str): + jsn = json.loads(jsn) + json.dump(jsn, file_obj) + file_obj.flush() + return file_obj + + +def file_to_json(file_path: str | Path) -> dict | list: + with open(file_path) as f: + return json.load(f) + + +########################### +# HuggingFace Hub API Utils +########################### +def set_space_timeout( + space_id: str, + token: str | None = None, + timeout_in_seconds: int = 300, +): + headers = huggingface_hub.utils.build_hf_headers( + token=token, + library_name="gradio_client", + library_version=__version__, + ) + try: + httpx.post( + f"https://huggingface.co/api/spaces/{space_id}/sleeptime", + json={"seconds": timeout_in_seconds}, + headers=headers, + ) + except httpx.HTTPStatusError as e: + raise SpaceDuplicationError( + f"Could not set sleep timeout on duplicated Space. Please visit {SPACE_URL.format(space_id)} " + "to set a timeout manually to reduce billing charges." + ) from e + + +######################## +# Misc utils +######################## + + +def synchronize_async(func: Callable, *args, **kwargs) -> Any: + """ + Runs async functions in sync scopes. Can be used in any scope. + + Example: + if inspect.iscoroutinefunction(block_fn.fn): + predictions = utils.synchronize_async(block_fn.fn, *processed_input) + + Args: + func: + *args: + **kwargs: + """ + return fsspec.asyn.sync(fsspec.asyn.get_loop(), func, *args, **kwargs) # type: ignore + + +class APIInfoParseError(ValueError): + pass + + +def get_type(schema: dict): + if "const" in schema: + return "const" + if "enum" in schema: + return "enum" + elif "type" in schema: + return schema["type"] + elif schema.get("$ref"): + return "$ref" + elif schema.get("oneOf"): + return "oneOf" + elif schema.get("anyOf"): + return "anyOf" + elif schema.get("allOf"): + return "allOf" + elif "type" not in schema: + return {} + else: + raise APIInfoParseError(f"Cannot parse type for {schema}") + + +FILE_DATA_FORMATS = [ + "Dict(path: str | None (Path to a local file), url: str | None (Publicly available url or base64 encoded image), size: int | None (Size of image in bytes), orig_name: str | None (Original filename), mime_type: str | None (mime type of image), is_stream: bool (Can always be set to False), meta: Dict())", + "dict(path: str | None (Path to a local file), url: str | None (Publicly available url or base64 encoded image), size: int | None (Size of image in bytes), orig_name: str | None (Original filename), mime_type: str | None (mime type of image), is_stream: bool (Can always be set to False), meta: dict())", + "Dict(path: str, url: str | None, size: int | None, orig_name: str | None, mime_type: str | None)", + "Dict(path: str, url: str | None, size: int | None, orig_name: str | None, mime_type: str | None, is_stream: bool)", + "Dict(path: str, url: str | None, size: int | None, orig_name: str | None, mime_type: str | None, is_stream: bool, meta: Dict())", + "dict(path: str, url: str | None, size: int | None, orig_name: str | None, mime_type: str | None, is_stream: bool, meta: dict())", + "dict(path: str, url: str | None, size: int | None, orig_name: str | None, mime_type: str | None, is_stream: bool, meta: dict(_type: Literal[gradio.FileData]))", +] + +CURRENT_FILE_DATA_FORMAT = FILE_DATA_FORMATS[-1] + + +def json_schema_to_python_type(schema: Any) -> str: + type_ = _json_schema_to_python_type(schema, schema.get("$defs")) + return type_.replace(CURRENT_FILE_DATA_FORMAT, "filepath") + + +def _json_schema_to_python_type(schema: Any, defs) -> str: + """Convert the json schema into a python type hint""" + if schema == {}: + return "Any" + type_ = get_type(schema) + if type_ == {}: + if "json" in schema.get("description", {}): + return "str | float | bool | list | dict" + else: + return "Any" + elif type_ == "$ref": + return _json_schema_to_python_type(defs[schema["$ref"].split("/")[-1]], defs) + elif type_ == "null": + return "None" + elif type_ == "const": + return f"Literal[{schema['const']}]" + elif type_ == "enum": + return ( + "Literal[" + ", ".join(["'" + str(v) + "'" for v in schema["enum"]]) + "]" + ) + elif type_ == "integer": + return "int" + elif type_ == "string": + return "str" + elif type_ == "boolean": + return "bool" + elif type_ == "number": + return "float" + elif type_ == "array": + items = schema.get("items", []) + if "prefixItems" in items: + elements = ", ".join( + [_json_schema_to_python_type(i, defs) for i in items["prefixItems"]] + ) + return f"tuple[{elements}]" + elif "prefixItems" in schema: + elements = ", ".join( + [_json_schema_to_python_type(i, defs) for i in schema["prefixItems"]] + ) + return f"tuple[{elements}]" + else: + elements = _json_schema_to_python_type(items, defs) + return f"list[{elements}]" + elif type_ == "object": + props = schema.get("properties", {}) + if _is_file_schema(schema, defs or {}): + return "filepath" + + def get_desc(v): + return f" ({v.get('description')})" if v.get("description") else "" + + des = [ + f"{n}: {_json_schema_to_python_type(v, defs)}{get_desc(v)}" + for n, v in props.items() + if n != "$defs" + ] + + if "additionalProperties" in schema: + additional_properties = schema["additionalProperties"] + if isinstance(additional_properties, bool) and additional_properties: + des += ["str, Any"] + else: + des += [ + f"str, {_json_schema_to_python_type(additional_properties, defs)}" + ] + des = ", ".join(des) + return f"dict({des})" + elif type_ in ["oneOf", "anyOf"]: + desc = " | ".join([_json_schema_to_python_type(i, defs) for i in schema[type_]]) + return desc + elif type_ == "allOf": + data = ", ".join(_json_schema_to_python_type(i, defs) for i in schema[type_]) + desc = f"All[{data}]" + return desc + else: + raise APIInfoParseError(f"Cannot parse schema {schema}") + + +def python_type_to_json_schema(type_hint: Any) -> dict: + try: + return _python_type_to_json_schema(type_hint) + except Exception: + return {} + + +def _python_type_to_json_schema(type_hint: Any) -> dict: + """Convert a Python type hint to a JSON schema.""" + if type_hint is type(None): + return {"type": "null"} + if type_hint is Any: + return {} + if type_hint is str: + return {"type": "string"} + if type_hint is int: + return {"type": "integer"} + if type_hint is float: + return {"type": "number"} + if type_hint is bool: + return {"type": "boolean"} + if type_hint is dict: + return {"type": "object", "additionalProperties": {}} + if type_hint is list: + return {"type": "array", "items": {}} + if type_hint is tuple: + return {"type": "array"} + if type_hint is set or type_hint is frozenset: + return {"type": "array", "uniqueItems": True} + if type_hint is bytes or type_hint is bytearray: + return {"type": "string", "format": "byte"} + + origin = get_origin(type_hint) + + if origin is Literal: + literal_values = get_args(type_hint) + if len(literal_values) == 1: + return {"const": literal_values[0]} + return {"enum": list(literal_values)} + + if ( + origin is Union + or (hasattr(origin, "__name__") and origin.__name__ == "UnionType") + or str(origin) == "|" + ): + types = get_args(type_hint) + if len(types) == 2 and type(None) in types: + other_type = next(t for t in types if t is not type(None)) + schema = _python_type_to_json_schema(other_type) + return {"oneOf": [{"type": "null"}, schema]} + return {"anyOf": [_python_type_to_json_schema(t) for t in types]} + + if origin is list: + args = get_args(type_hint) + if not args: + return {"type": "array", "items": {}} + item_type = args[0] + return {"type": "array", "items": _python_type_to_json_schema(item_type)} + if origin is tuple: + types = get_args(type_hint) + if not types: + return {"type": "array"} + if len(types) == 2 and types[1] is ...: + return {"type": "array", "items": _python_type_to_json_schema(types[0])} + return { + "type": "array", + "prefixItems": [_python_type_to_json_schema(t) for t in types], + "minItems": len(types), + "maxItems": len(types), + } + if origin is set or origin is frozenset: + args = get_args(type_hint) + if not args: + return {"type": "array", "uniqueItems": True} + item_type = args[0] + return { + "type": "array", + "uniqueItems": True, + "items": _python_type_to_json_schema(item_type), + } + + if origin is dict: + args = get_args(type_hint) + if not args: + return {"type": "object", "additionalProperties": {}} + key_type, value_type = args + if key_type is not str: + raise ValueError("JSON Schema only supports string keys in objects") + schema = { + "type": "object", + "additionalProperties": _python_type_to_json_schema(value_type), + } + return schema + + if inspect.isclass(type_hint) and issubclass(type_hint, Enum): + enum_values = [item.value for item in type_hint] + return {"enum": enum_values} + + if inspect.isclass(type_hint) and hasattr(type_hint, "__annotations__"): + properties = {} + required = [] + + hints = get_type_hints(type_hint) + for field_name, field_type in hints.items(): + properties[field_name] = _python_type_to_json_schema(field_type) + if hasattr(type_hint, "__total__"): + if type_hint.__total__: + required.append(field_name) + elif ( + not hasattr(type_hint, "__dataclass_fields__") + or not type_hint.__dataclass_fields__[field_name].default + ): + required.append(field_name) + + schema = {"type": "object", "properties": properties} + if required: + schema["required"] = required + return schema + + return {} + + +def traverse(json_obj: Any, func: Callable, is_root: Callable[..., bool]) -> Any: + """ + Traverse a JSON object and apply a function to each element that satisfies the is_root condition. + """ + if is_root(json_obj): + return func(json_obj) + elif isinstance(json_obj, dict): + new_obj = {} + for key, value in json_obj.items(): + new_obj[key] = traverse(value, func, is_root) + return new_obj + elif isinstance(json_obj, (list, tuple)): + new_obj = [] + for item in json_obj: + new_obj.append(traverse(item, func, is_root)) + return new_obj + else: + return json_obj + + +async def async_traverse( + json_obj: Any, + func: Callable[..., Coroutine[Any, Any, Any]], + is_root: Callable[..., bool], +) -> Any: + """ + Traverse a JSON object and apply a async function to each element that satisfies the is_root condition. + """ + if is_root(json_obj): + return await func(json_obj) + elif isinstance(json_obj, dict): + new_obj = {} + for key, value in json_obj.items(): + new_obj[key] = await async_traverse(value, func, is_root) + return new_obj + elif isinstance(json_obj, (list, tuple)): + new_obj = [] + for item in json_obj: + new_obj.append(await async_traverse(item, func, is_root)) + return new_obj + else: + return json_obj + + +def value_is_file(api_info: dict) -> bool: + return _schema_contains_file(api_info, api_info.get("$defs", {})) + + +def _resolve_ref(schema: dict, defs: dict) -> dict: + """Resolve a $ref to its definition.""" + if "$ref" in schema: + ref_name = schema["$ref"].split("/")[-1] + if ref_name in defs: + return defs[ref_name] + return schema + + +def _is_file_schema(schema: dict, defs: dict | None = None) -> bool: + """Check if a schema directly represents a file type (has path + meta with gradio.FileData).""" + if defs is None: + defs = schema.get("$defs", {}) + props = schema.get("properties", {}) + if "path" not in props or "meta" not in props: + return False + meta = _resolve_ref(props["meta"], defs) + meta_props = meta.get("properties", {}) + if "_type" in meta_props: + type_schema = meta_props["_type"] + return type_schema.get("const") == "gradio.FileData" + meta_default = meta.get("default", {}) + if isinstance(meta_default, dict): + return meta_default.get("_type") == "gradio.FileData" + return False + + +def _schema_contains_file(schema, defs: dict) -> bool: + """Recursively check if a JSON schema contains a file type anywhere.""" + if not isinstance(schema, dict): + if isinstance(schema, list): + return any(_schema_contains_file(item, defs) for item in schema) + return False + if "$ref" in schema: + ref_name = schema["$ref"].split("/")[-1] + if ref_name in defs: + return _schema_contains_file(defs[ref_name], defs) + return False + if _is_file_schema(schema, defs): + return True + return any( + _schema_contains_file(v, defs) + for k, v in schema.items() + if k != "$defs" and isinstance(v, (dict, list)) + ) + + +def is_filepath(s) -> bool: + """ + Check if the given value is a valid str or Path filepath on the local filesystem, e.g. "path/to/file". + """ + return isinstance(s, (str, Path)) and Path(s).exists() and Path(s).is_file() + + +def is_file_obj(d) -> bool: + """ + Check if the given value is a valid FileData object dictionary in versions of Gradio<=4.20, e.g. + { + "path": "path/to/file", + } + """ + return isinstance(d, dict) and "path" in d and isinstance(d["path"], str) + + +def is_file_obj_with_meta(d) -> bool: + """ + Check if the given value is a valid FileData object dictionary in newer versions of Gradio + where the file objects include a specific "meta" key, e.g. + { + "path": "path/to/file", + "meta": {"_type: "gradio.FileData"} + } + """ + return ( + isinstance(d, dict) + and "path" in d + and isinstance(d["path"], str) + and "meta" in d + and d["meta"].get("_type", "") == "gradio.FileData" + ) + + +def is_file_obj_with_url(d) -> bool: + """ + Check if the given value is a valid FileData object dictionary in newer versions of Gradio + where the file objects include a specific "meta" key, and ALSO include a "url" key, e.g. + { + "path": "path/to/file", + "url": "/file=path/to/file", + "meta": {"_type: "gradio.FileData"} + } + """ + return is_file_obj_with_meta(d) and "url" in d and isinstance(d["url"], str) + + +SKIP_COMPONENTS = { + "state", + "row", + "column", + "tabs", + "tab", + "tabitem", + "box", + "form", + "accordion", + "group", + "interpretation", + "dataset", + "sidebar", +} + + +def handle_file(filepath_or_url: str | Path): + s = str(filepath_or_url) + data = {"path": s, "meta": {"_type": "gradio.FileData"}} + if is_http_url_like(s): + return {**data, "orig_name": s.rsplit("/", maxsplit=1)[-1], "url": s} + elif Path(s).exists(): + return {**data, "orig_name": Path(s).name} + else: + raise ValueError( + f"File {s} does not exist on local filesystem and is not a valid URL." + ) + + +def file(filepath_or_url: str | Path): + warnings.warn( + "file() is deprecated and will be removed in a future version. Use handle_file() instead." + ) + return handle_file(filepath_or_url) + + +def construct_args( + parameters_info: list[ParameterInfo] | None, args: tuple, kwargs: dict +) -> list: + class _Keywords(Enum): + NO_VALUE = "NO_VALUE" # Used as a sentinel to determine if nothing is provided as a parameter for an argument + + _args = list(args) + if parameters_info is None: + if kwargs: + raise ValueError( + "This endpoint does not support key-word arguments Please click on 'view API' in the footer of the Gradio app to see usage." + ) + return _args + num_args = len(args) + _args = _args + [_Keywords.NO_VALUE] * (len(parameters_info) - num_args) + + kwarg_arg_mapping = {} + kwarg_names = [] + for index, param_info in enumerate(parameters_info): + if "parameter_name" in param_info: + kwarg_arg_mapping[param_info["parameter_name"]] = index + kwarg_names.append(param_info["parameter_name"]) + else: + kwarg_names.append(f"argument {index}") + if ( + param_info.get("parameter_has_default", False) + and _args[index] == _Keywords.NO_VALUE + ): + _args[index] = param_info.get("parameter_default") + + for key, value in kwargs.items(): + if key in kwarg_arg_mapping: + if kwarg_arg_mapping[key] < num_args: + raise TypeError( + f"Parameter `{key}` is already set as a positional argument. Please click on 'view API' in the footer of the Gradio app to see usage." + ) + else: + _args[kwarg_arg_mapping[key]] = value + else: + raise TypeError( + f"Parameter `{key}` is not a valid key-word argument. Please click on 'view API' in the footer of the Gradio app to see usage." + ) + + if _Keywords.NO_VALUE in _args: + raise TypeError( + f"No value provided for required argument: {kwarg_names[_args.index(_Keywords.NO_VALUE)]}" + ) + + return _args + + +def extract_validation_message(req: httpx.Response) -> str | None: + """ + If the request is a 422 error and the detail contains a validation error message, return the message. Otherwise, return None. + """ + if req.status_code == 422: + detail = req.json().get("detail", []) + validation_messages = [] + for index, error_info in enumerate(detail): + if ( + error_info.get("__type__", "") == "validate" + and error_info.get("is_valid") is False + ): + param_name = error_info.get("parameter_name", f"parameter_{index}") + validation_messages.append( + f"- {param_name}: {error_info.get('message', '')}" + ) + validation_messages.insert( + 0, f"{len(validation_messages)} parameter(s) failed validation:" + ) + if validation_messages: + return "\n".join(validation_messages) diff --git a/client/python/pyproject.toml b/client/python/pyproject.toml new file mode 100644 index 0000000..c7dcf08 --- /dev/null +++ b/client/python/pyproject.toml @@ -0,0 +1,70 @@ +[build-system] +requires = ["hatchling", "hatch-requirements-txt", "hatch-fancy-pypi-readme>=22.5.0"] +build-backend = "hatchling.build" + +[project] +name = "gradio_client" +dynamic = ["version", "dependencies", "readme"] +description = "Python library for easily interacting with trained machine learning models" +license = "Apache-2.0" +requires-python = ">=3.10" +authors = [ + { name = "Abubakar Abid", email = "gradio-team@huggingface.co" }, + { name = "Ali Abid", email = "gradio-team@huggingface.co" }, + { name = "Ali Abdalla", email = "gradio-team@huggingface.co" }, + { name = "Dawood Khan", email = "gradio-team@huggingface.co" }, + { name = "Ahsen Khaliq", email = "gradio-team@huggingface.co" }, + { name = "Pete Allen", email = "gradio-team@huggingface.co" }, + { name = "Freddy Boulton", email = "gradio-team@huggingface.co" }, +] +keywords = ["machine learning", "client", "API"] + +classifiers = [ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Topic :: Scientific/Engineering', + 'Topic :: Scientific/Engineering :: Artificial Intelligence', + 'Topic :: Software Development :: User Interfaces', +] + +[project.urls] +Homepage = "https://github.com/gradio-app/gradio" + +[tool.hatch.version] +path = "gradio_client/package.json" +pattern = ".*\"version\":\\s*\"(?P[^\"]+)\"" + +[tool.hatch.metadata.hooks.requirements_txt] +filename = "requirements.txt" + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" +fragments = [ + { path = "README.md" }, +] + +[tool.hatch.build.targets.sdist] +include = [ + "/gradio_client", + "/README.md", + "/requirements.txt", +] + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.ruff.lint.isort] +known-first-party = [ + "gradio_client" +] + +[tool.pytest.ini_options] +GRADIO_ANALYTICS_ENABLED = "False" +HF_HUB_DISABLE_TELEMETRY = "1" diff --git a/client/python/requirements.txt b/client/python/requirements.txt new file mode 100644 index 0000000..12b5994 --- /dev/null +++ b/client/python/requirements.txt @@ -0,0 +1,5 @@ +fsspec +httpx>=0.24.1 +huggingface_hub>=0.19.3,<2.0 +packaging +typing_extensions~=4.0 \ No newline at end of file diff --git a/client/python/scripts/test.sh b/client/python/scripts/test.sh new file mode 100755 index 0000000..12c480d --- /dev/null +++ b/client/python/scripts/test.sh @@ -0,0 +1,6 @@ +#!/bin/bash -eu + +cd "$(dirname ${0})/.." + +echo "Testing..." +python -m pytest test diff --git a/client/python/test/conftest.py b/client/python/test/conftest.py new file mode 100644 index 0000000..4e467da --- /dev/null +++ b/client/python/test/conftest.py @@ -0,0 +1,515 @@ +import inspect +import random +import time + +import gradio as gr +import pytest +from pydub import AudioSegment + + +def pytest_configure(config): + config.addinivalue_line( + "markers", "flaky: mark test as flaky. Failure will not cause te" + ) + config.addinivalue_line("markers", "serial: mark test as serial") + + +@pytest.fixture +def calculator_demo(): + 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", + api_name="predict", + examples=[ + [5, "add", 3], + [4, "divide", 2], + [-4, "multiply", 2.5], + [0, "subtract", 1.2], + ], + ) + return demo + + +@pytest.fixture +def hello_world_demo(): + def greet(name, punctuation): + return "Hello " + name + punctuation + + demo = gr.Interface( + fn=greet, + inputs=[gr.Textbox(label="Name"), gr.Textbox(label="Punctuation")], + outputs=gr.Textbox(label="Greeting"), + api_name="greet", + ) + return demo + + +@pytest.fixture +def calculator_demo_with_defaults(): + def calculator(num1, operation=None, num2=100): + if operation is None or 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, + [ + gr.Number(value=10), + gr.Radio(["add", "subtract", "multiply", "divide"]), + gr.Number(), + ], + "number", + examples=[ + [5, "add", 3], + [4, "divide", 2], + [-4, "multiply", 2.5], + [0, "subtract", 1.2], + ], + api_name="predict", + ) + return demo + + +@pytest.fixture +def state_demo(): + state = gr.State(delete_callback=lambda x: print("STATE DELETED")) + demo = gr.Interface( + lambda x, y: (x, y), + ["textbox", state], + ["textbox", state], + api_name="predict", + ) + return demo + + +@pytest.fixture +def increment_demo(): + with gr.Blocks() as demo: + btn1 = gr.Button("Increment") + btn2 = gr.Button("Increment") + btn3 = gr.Button("Increment") + numb = gr.Number() + + state = gr.State(0) + + btn1.click( + lambda x: (x + 1, x + 1), + state, + [state, numb], + api_name="increment_with_queue", + ) + btn2.click( + lambda x: (x + 1, x + 1), + state, + [state, numb], + queue=False, + api_name="increment_without_queue", + ) + btn3.click( + lambda x: (x + 1, x + 1), + state, + [state, numb], + api_visibility="private", + ) + + return demo + + +@pytest.fixture +def progress_demo(): + def my_function(x, progress=gr.Progress()): + progress(0, desc="Starting...") + for _ in progress.tqdm(range(20)): + time.sleep(0.1) + return x + + return gr.Interface(my_function, gr.Textbox(), gr.Textbox(), api_name="predict") + + +@pytest.fixture +def yield_demo(): + def spell(x): + for i in range(len(x)): + time.sleep(0.5) + yield x[:i] + + return gr.Interface(spell, "textbox", "textbox", api_name="predict") + + +@pytest.fixture +def cancel_from_client_demo(): + def iteration(): + for i in range(20): + print(f"i: {i}") + yield i + time.sleep(0.5) + + def long_process(): + time.sleep(10) + print("DONE!") + return 10 + + with gr.Blocks() as demo: + num = gr.Number() + + btn = gr.Button(value="Iterate") + btn.click(iteration, None, num, api_name="iterate") + btn2 = gr.Button(value="Long Process") + btn2.click(long_process, None, num, api_name="long") + + return demo + + +@pytest.fixture +def sentiment_classification_demo(): + def classifier(text): # noqa: ARG001 + time.sleep(1) + return {label: random.random() for label in ["POSITIVE", "NEGATIVE", "NEUTRAL"]} + + def sleep_for_test(): + time.sleep(10) + return 2 + + with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + input_text = gr.Textbox(label="Input Text") + with gr.Row(): + classify = gr.Button("Classify Sentiment") + with gr.Column(): + label = gr.Label(label="Predicted Sentiment") + number = gr.Number() + btn = gr.Button("Sleep then print") + classify.click(classifier, input_text, label, api_name="classify") + btn.click(sleep_for_test, None, number, api_name="sleep") + + return demo + + +@pytest.fixture +def count_generator_demo(): + def count(n): + for i in range(int(n)): + time.sleep(0.5) + yield i + + def show(n): + return str(list(range(int(n)))) + + with gr.Blocks() as demo: + with gr.Column(): + num = gr.Number(value=10) + with gr.Row(): + count_btn = gr.Button("Count") + list_btn = gr.Button("List") + with gr.Column(): + out = gr.Textbox() + + count_btn.click(count, num, out) + list_btn.click(show, num, out) + + return demo + + +@pytest.fixture +def count_generator_no_api(): + def count(n): + for i in range(int(n)): + time.sleep(0.5) + yield i + + def show(n): + return str(list(range(int(n)))) + + with gr.Blocks() as demo: + with gr.Column(): + num = gr.Number(value=10) + with gr.Row(): + count_btn = gr.Button("Count") + list_btn = gr.Button("List") + with gr.Column(): + out = gr.Textbox() + + count_btn.click(count, num, out, api_visibility="private") + list_btn.click(show, num, out, api_visibility="private") + + return demo + + +@pytest.fixture +def count_generator_demo_exception(): + def count(n): + for i in range(int(n)): + time.sleep(0.01) + if i == 5: + raise ValueError("Oh no!") + yield i + + def show(n): + return str(list(range(int(n)))) + + with gr.Blocks() as demo: + with gr.Column(): + num = gr.Number(value=10) + with gr.Row(): + count_btn = gr.Button("Count") + with gr.Column(): + out = gr.Textbox() + + count_btn.click(count, num, out, api_name="count") + return demo + + +@pytest.fixture +def file_io_demo(): + demo = gr.Interface( + lambda _: print("foox"), + [gr.File(file_count="multiple"), "file"], + [gr.File(file_count="multiple"), "file"], + api_name="predict", + ) + + return demo + + +@pytest.fixture +def stateful_chatbot(): + with gr.Blocks() as demo: + chatbot = gr.Chatbot() + msg = gr.Textbox() + clear = gr.Button("Clear") + st = gr.State([1, 2, 3]) + + def respond(message, st, chat_history): + assert st[0] == 1 and st[1] == 2 and st[2] == 3 + bot_message = "I love you" + chat_history.append({"role": "user", "content": message}) + chat_history.append({"role": "assistant", "content": bot_message}) + return "", chat_history + + msg.submit(respond, [msg, st, chatbot], [msg, chatbot], api_name="submit") + clear.click(lambda: None, None, chatbot, queue=False) + return demo + + +@pytest.fixture +def hello_world_with_group(): + with gr.Blocks() as demo: + name = gr.Textbox(label="name") + output = gr.Textbox(label="greeting") + greet = gr.Button("Greet") + show_group = gr.Button("Show group") + with gr.Group(visible=False) as group: + gr.Textbox("Hello!") + + def greeting(name): + return f"Hello {name}", gr.Group(visible=True) + + greet.click( + greeting, inputs=[name], outputs=[output, group], api_name="greeting" + ) + show_group.click( + lambda: gr.Group(visible=False), None, group, api_name="show_group" + ) + return demo + + +@pytest.fixture +def hello_world_with_state_and_accordion(): + with gr.Blocks() as demo: + with gr.Row(): + name = gr.Textbox(label="name") + output = gr.Textbox(label="greeting") + num = gr.Number(label="count") + with gr.Row(): + n_counts = gr.State(value=0) + greet = gr.Button("Greet") + open_acc = gr.Button("Open acc") + close_acc = gr.Button("Close acc") + with gr.Accordion(label="Extra stuff", open=False) as accordion: + gr.Textbox("Hello!") + + def greeting(name, state): + """This is a greeting function.""" + state += 1 + return state, f"Hello {name}", state, gr.Accordion(open=False) + + greet.click( + greeting, + inputs=[name, n_counts], + outputs=[n_counts, output, num, accordion], + api_name="greeting", + ) + open_acc.click( + lambda state: (state + 1, state + 1, gr.Accordion(open=True)), + [n_counts], + [n_counts, num, accordion], + api_name="open", + ) + close_acc.click( + lambda state: (state + 1, state + 1, gr.Accordion(open=False)), + [n_counts], + [n_counts, num, accordion], + api_name="close", + ) + return demo + + +@pytest.fixture +def stream_audio(): + import pathlib + import tempfile + + def _stream_audio(audio_file): + audio = AudioSegment.from_mp3(audio_file) + i = 0 + chunk_size = 3000 + + while chunk_size * i < len(audio): + chunk = audio[chunk_size * i : chunk_size * (i + 1)] + i += 1 + if chunk: + file = str(pathlib.Path(tempfile.gettempdir()) / f"{i}.wav") + chunk.export(file, format="wav") + yield file + + return gr.Interface( + fn=_stream_audio, + inputs=gr.Audio(type="filepath", label="Audio file to stream"), + outputs=gr.Audio(autoplay=True, streaming=True), + api_name="predict", + ) + + +@pytest.fixture +def video_component(): + return gr.Interface( + fn=lambda x: x, inputs=gr.Video(), outputs=gr.Video(), api_name="predict" + ) + + +@pytest.fixture +def all_components(): + classes_to_check = gr.components.Component.__subclasses__() + subclasses = [] + + while classes_to_check: + subclass = classes_to_check.pop() + children = subclass.__subclasses__() + + if children: + classes_to_check.extend(children) + if ( + "value" in inspect.signature(subclass).parameters + and subclass != gr.components.Component + and not getattr(subclass, "is_template", False) + ): + subclasses.append(subclass) + + return subclasses + + +@pytest.fixture(autouse=True) +def gradio_temp_dir(monkeypatch, tmp_path): + """tmp_path is unique to each test function. + It will be cleared automatically according to pytest docs: https://docs.pytest.org/en/6.2.x/reference.html#tmp-path + """ + monkeypatch.setenv("GRADIO_TEMP_DIR", str(tmp_path)) + return tmp_path + + +@pytest.fixture +def long_response_with_info(): + def long_response(_): + gr.Info("Beginning long response") + time.sleep(17) + gr.Info("Done!") + return "\ta\nb" * 90000 + + return gr.Interface( + long_response, None, gr.Textbox(label="Output"), api_name="predict" + ) + + +@pytest.fixture +def many_endpoint_demo(): + with gr.Blocks() as demo: + + def noop(x): + return x + + n_elements = 1000 + for _ in range(n_elements): + msg2 = gr.Textbox() + msg2.submit(noop, msg2, msg2) + butn2 = gr.Button() + butn2.click(noop, msg2, msg2) + + return demo + + +@pytest.fixture +def max_file_size_demo(): + with gr.Blocks() as demo: + file_1b = gr.File() + upload_status = gr.Textbox() + + file_1b.upload( + lambda x: "Upload successful", file_1b, upload_status, api_name="upload_1b" + ) + + return demo + + +@pytest.fixture +def chatbot_message_format(): + with gr.Blocks() as demo: + chatbot = gr.Chatbot() + msg = gr.Textbox() + + def respond(message, chat_history: list): + bot_message = random.choice( + ["How are you?", "I love you", "I'm very hungry"] + ) + chat_history.extend( + [ + {"role": "user", "content": message}, + {"role": "assistant", "content": bot_message}, + ] + ) + return "", chat_history + + msg.submit(respond, [msg, chatbot], [msg, chatbot], api_name="chat") + + return demo + + +@pytest.fixture +def media_data(): + import sys + from pathlib import Path + + sys.path.append(Path(".").resolve().as_posix()) + from client.python.test import media_data + + return media_data diff --git a/client/python/test/files/alphabet.txt b/client/python/test/files/alphabet.txt new file mode 100644 index 0000000..e85d5b4 --- /dev/null +++ b/client/python/test/files/alphabet.txt @@ -0,0 +1 @@ +abcdefghijklmnopqrstuvwxyz \ No newline at end of file diff --git a/client/python/test/media_data.py b/client/python/test/media_data.py new file mode 100644 index 0000000..693f6c0 --- /dev/null +++ b/client/python/test/media_data.py @@ -0,0 +1,8668 @@ +from typing import cast + +try: + from gradio.media import get_audio, get_video +except Exception: + + def get_video(filename: str | None): + return cast(str, filename) + + def get_audio(filename: str | None): + return cast(str, filename) + + +BASE64_IMAGE = ( # test/test_files/bus.png + "data:image/png;base64," + "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" +) +BASE64_AUDIO = { + "path": get_audio("audio_sample.wav"), + "data": "data:audio/wav;base64,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gwobAWwAoQTr+LgFs/1Q7/sCcPj0AJ72QQC9/Qr4NgOE+NYFt/hRABEDm/xzAH/9DQLyAyT/PwQVAiP/wwUdAtb/DAFnATn/owdP/1UJe/pSDFEGUPt/Ezz4GAVj/BX7nP+P9W0CB/ti8ogAp/OF/yr3e/hJ/5z4egiV/uQEswHyAaEEmPpMCMEDwQKBAlL/Ygc78fwCQwVN9sr+KAXbAMgEyPwOAuQB5fnZDOv3mQrbCSP7yAlg94MHpvVc+LIIr/izAcb+pgKMAY75vfz3/Lf30foJ/bj5k/vBBcvytARRBOcE4AMVAFoJzfnMAVYADwC7ABsDtgRCCB4CgwtoAmn/cAX+/0AAMgWiAdP9JQIF+oEDHPYZAaD6x/VCAWvwUQG88hcAIgFp+8oC5v63BKD9pgKI/a8ErvxHBx4DgwGnBF4EN/w3BQH8ff86ASj8qQdr9JgLz/mwCI74EgOvAtn+cwaw/o4GK/1eAZEJ8/diAAYDjfX4CInqdBIE87YBjwFu9A0H2/Y1/3b3M/nF+SgAH/nQCt787Qa3/68E7wQG/sQA8gSv/dYBsgsQ+ogLtABkBxwDSAcLBXADVP4LBDcA6vPZBQr1NwLV+zn8IwKt9Kz88PzO7QsHa/wz/r0HqPi/Cn35yASb/7MBuv0cDPsCYQMiAD75GwVk830GfflZ/3MI3wFH+MkH0/xpBT37ZQadBgv8DAlO+7gDCPyTBrr3awvc+AMDDP+n+gcF0/fj/Mn7cwFM//787fTeA0/z3wLn9HX/uQSb/dwDcf1QAMsEDAKL/oAJO/vBB9cFuf5D/1EDZAEBBs7+qQof/hgNAwO4/dcDm/zUBw/4Gv+m9nX9wvbl9RT22//D/HwCPfnF/7/7oQJXA6D5ywdRAUIHMgA+Ayf9FwQBBi39M/6YAxX97ACJ/Zb73QAsAaMF2v/8AnADgwMpAj//SvyNB2UBl/tMBGT8ggVD+4MHQPzC/2gDCv1p+ov9Zv9x85cF/PJt+p4BCP1n/eb8x/ypCiXzgAqT/xX7jAhq+tYFN/tACMAA3QL8BDAK+P6LBuIE6ATBBL8DegTMBOT6WQbx/ED1UQS07z3/cvdE/Ib76fppAfj4jfdMSVNUYgAAAElORk9JTkFNEAAAAEltcGFjdCBNb2RlcmF0bwBJUFJEFgAAAFlvdVR1YmUgQXVkaW8gTGlicmFyeQBJQVJUDgAAAEtldmluIE1hY0xlb2QASUdOUgoAAABDaW5lbWF0aWMAaWQzIHAAAABJRDMDAAAAAABmVElUMgAAABAAAABJbXBhY3QgTW9kZXJhdG9UQUxCAAAAFgAAAFlvdVR1YmUgQXVkaW8gTGlicmFyeVRQRTEAAAAOAAAAS2V2aW4gTWFjTGVvZFRDT04AAAAKAAAAQ2luZW1hdGlj", +} + +BASE64_AUDIO_DUPLICATE = { + "path": get_audio("audio_sample.wav"), + "data": "data:audio/wav;base64,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gwobAWwAoQTr+LgFs/1Q7/sCcPj0AJ72QQC9/Qr4NgOE+NYFt/hRABEDm/xzAH/9DQLyAyT/PwQVAiP/wwUdAtb/DAFnATn/owdP/1UJe/pSDFEGUPt/Ezz4GAVj/BX7nP+P9W0CB/ti8ogAp/OF/yr3e/hJ/5z4egiV/uQEswHyAaEEmPpMCMEDwQKBAlL/Ygc78fwCQwVN9sr+KAXbAMgEyPwOAuQB5fnZDOv3mQrbCSP7yAlg94MHpvVc+LIIr/izAcb+pgKMAY75vfz3/Lf30foJ/bj5k/vBBcvytARRBOcE4AMVAFoJzfnMAVYADwC7ABsDtgRCCB4CgwtoAmn/cAX+/0AAMgWiAdP9JQIF+oEDHPYZAaD6x/VCAWvwUQG88hcAIgFp+8oC5v63BKD9pgKI/a8ErvxHBx4DgwGnBF4EN/w3BQH8ff86ASj8qQdr9JgLz/mwCI74EgOvAtn+cwaw/o4GK/1eAZEJ8/diAAYDjfX4CInqdBIE87YBjwFu9A0H2/Y1/3b3M/nF+SgAH/nQCt787Qa3/68E7wQG/sQA8gSv/dYBsgsQ+ogLtABkBxwDSAcLBXADVP4LBDcA6vPZBQr1NwLV+zn8IwKt9Kz88PzO7QsHa/wz/r0HqPi/Cn35yASb/7MBuv0cDPsCYQMiAD75GwVk830GfflZ/3MI3wFH+MkH0/xpBT37ZQadBgv8DAlO+7gDCPyTBrr3awvc+AMDDP+n+gcF0/fj/Mn7cwFM//787fTeA0/z3wLn9HX/uQSb/dwDcf1QAMsEDAKL/oAJO/vBB9cFuf5D/1EDZAEBBs7+qQof/hgNAwO4/dcDm/zUBw/4Gv+m9nX9wvbl9RT22//D/HwCPfnF/7/7oQJXA6D5ywdRAUIHMgA+Ayf9FwQBBi39M/6YAxX97ACJ/Zb73QAsAaMF2v/8AnADgwMpAj//SvyNB2UBl/tMBGT8ggVD+4MHQPzC/2gDCv1p+ov9Zv9x85cF/PJt+p4BCP1n/eb8x/ypCiXzgAqT/xX7jAhq+tYFN/tACMAA3QL8BDAK+P6LBuIE6ATBBL8DegTMBOT6WQbx/ED1UQS07z3/cvdE/Ib76fppAfj4jfdMSVNUYgAAAElORk9JTkFNEAAAAEltcGFjdCBNb2RlcmF0bwBJUFJEFgAAAFlvdVR1YmUgQXVkaW8gTGlicmFyeQBJQVJUDgAAAEtldmluIE1hY0xlb2QASUdOUgoAAABDaW5lbWF0aWMAaWQzIHAAAABJRDMDAAAAAABmVElUMgAAABAAAABJbXBhY3QgTW9kZXJhdG9UQUxCAAAAFgAAAFlvdVR1YmUgQXVkaW8gTGlicmFyeVRQRTEAAAAOAAAAS2V2aW4gTWFjTGVvZFRDT04AAAAKAAAAQ2luZW1hdGlj", +} +BASE64_VIDEO = { + "is_file": True, + "path": get_video("b.mp4"), + "data": "data:video/mp4;base64,AAAAHGZ0eXBtcDQyAAAAAWlzb21tcDQxbXA0MgAAAAFtZGF0AAAAAAAD8BohEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8AAAC4gYF///e3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE0NiByMTFNIDEyMTM5NmMgLSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDE1IC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9uczogY2FiYWM9MCByZWY9MyBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgxOjB4MTExIG1lPWhleCBzdWJtZT03IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0xIDh4OGRjdD0wIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJvbWFfcXBfb2Zmc2V0PS0yIHRocmVhZHM9NDggbG9va2FoZWFkX3RocmVhZHM9MiBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIHN0aXRjaGFibGU9MSBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MCB3ZWlnaHRwPTAga2V5aW50PWluZmluaXRlIGtleWludF9taW49MzAgc2NlbmVjdXQ9NDAgaW50cmFfcmVmcmVzaD0wIHJjX2xvb2thaGVhZD00MCByYz0ycGFzcyBtYnRyZWU9MSBiaXRyYXRlPTMwMCByYXRldG9sPTEuMCBxY29tcD0wLjYwIHFwbWluPTUgcXBtYXg9NjkgcXBzdGVwPTQgY3BseGJsdXI9MjAuMCBxYmx1cj0wLjUgdmJ2X21heHJhdGU9MzMwIHZi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PcnL10/Cvr6+kSq/FeyoRHS4/u8MZclfrWoI+7zQyT+v/Vf30X/SJ9WLDgsz4EeAIRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0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WTvfBNyaQO5hYw6x0WPO5hLvd7+T3wrkgo7vbLO61+CLe8XW7363d/666KmR39iUCK7u/ZPTT+pgQ3dp5fIzjGfcLrr6ybr19fS+b6cN+SrX+HvjYCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8AAACikGbz0pAK+CvzDs1pfN40Wnza3vfNx9xq+Xbthj0RKpf/sXLJmSb3FH6nRNn/wqWcCpsRFPp7v8v3vbLCBnw3EZ3Dt0eatetoFrbUvD8tD7cJHqfpy5yz3P6k2vxdN9ByxeX/6fhpfF8s/mMliFPFDOE3xWhePCdl/u3GRlewuE2LpvQJElJqtwf2n3eT3Utz6D2+4KvRuC/7vhzV3jHf4jaH97B3Dy6buEi3Ve8od+EtBFQQzLnE+qxMEk4KvPayfy96tAolg/d3On5CnFQy9/vy5CfmCXDtya2i9deX/dr6mX8KPfG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WlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpa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ClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpa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4hx2Hyks/Td+yUnQff4T56bv6cKF838hnvvlk7E9dSmE8nS/21k9NGufPSiu73ffXC2SMhCpnK/Oa7O0ab5h5HGJXpXy+XPJ3Xspdmrk9pRMi/X5io5ojcRWZiIi7uXHb39Qtk/oh0rJVIksVyFW019z4/Xw98XAIRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpa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4kAna7SgYQIeh8uUdvH6PYUf4yN1fjd32rR/fL8E5N07rb1QtF5eXLr67GwREl+UGvSe69if0JK++rVeZip/uHbJf5MLLbwmIu7yZ3q+tpKnrQIT7uRPMyFS3tVckVu7gSIhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gYhS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeBiFLS0tLS0tLS0tLS0tLS0tLwhEUUAFFABRv/xClpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWl3gghS0tLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4GIUtLS0tLS0tLS0tLS0tLS0vCERRQAUUAFG//EKWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaXeCCFLS0tLS0tLS0tLS0tLS0tLS8IRFFABRQAUb/8QpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpd4IIUtLS0tLS0tLS0tLS0tLS0tLwAADdCZYiCAV8mKAAOKMnJycnJycnJycnJycnJycnJycnJycnJycnJycnJ11111111111///wXeAAmFEcIGm63wChQOU1oQEeEj84Ba2lPP/u7vw6IgATyCOF2cOKFpqPMANIgQwqMkPMjTYR88zMDUXZnDIT/2wMBVFRFyQ8o8AmLIC7aUBdUP35c9jxADMgEdpYBJkHUp2iKlZXx94+K8ARMxEOpH4OpLDpP8aQdXIO5Ma+G8bVvsdADZ7iv8LXiuvHTinnkOOSBqz8zMW1xGr/eFB8gIenZ4C5x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AAAGAAAAIQAAAAEAAAAHAAAADgAAAAEAAAAIAAAAIQAAAAEAAAAJAAAADgAAAAEAAAAKAAAAAQAAAAEAAAPEc3RzegAAAAAAAAAAAAAA7AAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAABKwAAASsAAAEqAAAAOHN0Y28AAAAAAAAACgAAACwAADXXAABrgQAAwYwAARKNAAFhWwABztsAAiToAAKY1gADEjk=", +} +BASE64_FILE = { + "path": "test/test_files/sample_file.pdf", + "data": "data:@file/pdf;base64,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", +} +BINARY_IMAGE = ( + b'GIF89a=\x00D\x00\xf7\xa8\x00\x9a,3\xff\xc0\xc0\xef\xc0\xc0uXg\xfc\xf9\xf7\x993\x00\xff\xec\xec\xff\xa0\xa0\xe5\xcc\xbf\xcf\x9f\x87\x0f\xef\xef\x7f\x7f\x7f\xef\x0f\x0f\xdf\x1f\x1f\xff&&_\x9f\x9f\xffYY\xbf??5\xa5\xc2\xff\xff\xff\xac\x16\x19\xb2&\x00\xf8\x13\x10\xc2& \xdf`PP\x84\x9b\xf8\x03\x00\xb5\x0b\x0c\xdf\x0f\x00>\x9a\xb5\x87BM\x7f`P\xd2\xa5\x8f\xcc\x19\x00\xa5,\x00\xec\xd9\xcf\xe5\x0c\x00\xeb\t\x00\xff\xd9\xd9\xc7\x0c\x0c\x0f\x0f\x0f\xffyy~MZ\xfb\t\x08\xe5M@\xfb__\xff33\xcf\x90x\xf2\xe5\xdf\xc3\x06\x06\xbf\t\x08\xff\xb3\xb3\xd9\xb2\x9f\xff\x06\x06\xac)\x00\xff\xc6\xc6\x0c\t\x08\xf9\xf2\xef\xc9s`\xb8#\x00\x9f/\x00\xff__\xff\x8c\x8c\xc5\x1c\x00\xdf33\xffpp\xcf\x19\x19\xc0\x13\x10\xbf\x90x\xf7YY\xff\xf6\xf6\xe7??\xd7&&\xefLL2& \xdf\xbf\xaf\xbf\xbf\xbf???\xc5M@cn\x81_\x00\x00___\xcb00\xd8\x13\x00YC8\x80\x80\x80\xf3RRsVH\xc490\x10\x10\x10\x917@\xf2\x06\x00\xcf@@\xca\x86pooo\xa3!&\xc1\x1d\x18\xcf//\x1f\x1f\x1f\xdf\x00\x00\xd2\x16\x00\xcb\x90x\xbf\x1f\x00\x19\x13\x10\xf3\xd0\xd0\xe399&\x1d\x18Yy\x8e\x8f\x8f\x8f\xff\xa9\xa9\xcb\x13\x13\xbf00SF@\xb6& >\x1d\x18\xfb\xdd\xdd@@@\x99\x93\x90\xff\xbc\xbc\x7fPP\xaf\xaf\xaf\xc6VHzsp\x93& \xb7pp\xb3\x86ptPP|pp\xafOO\xd0\xd0\xd0\xef\xef\xefL90\xbc\xa9\xa0o0(\xeb\xb0\xb0\xff\xe0\xe0\xff\xd0\xd0\x870(K0(\xc9|h\x9f__lct\xebFF\xcf\xcf\xcf\xe0\xe0\xe0b& \xff },(@0(\xa9\x93\x88\xa6|h\x1f\xdf\xdf\xd5\xac\x97\xe2\xc5\xb7\xc7`POOO\x9cyhppp\xff\x80\x80\xff\x96\x96\xd7``\xcc\x99\x7f,\xb0\xcf\xbf\x00\x00\x00\x00\x00\x00\xff\xff\xff\x00\x00\xffff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\xa8\x00,\x00\x00\x00\x00=\x00D\x00\x00\x08\xff\x00Q\t\x1cH\xb0\xa0\xc1\x83\x08\x13*\\\xc8\xb0\xa1\xc0\x1b\x07\x0c8\x9cHq\xa1\x89\x14\xa72F\xac\xc8\xb1\xa2\t\x1f\x19Cj\x94\xd8\xb1$B\x03\x07D\xaa\x1ci\xb2%*#3V\xcad\xe9\xb2\xa2\x9d 3s\x9e\xdaX\x93!"\x8c:\x83\xf2\xeci\xf0c\xd0\xa3!\x87\x12E\x89\xb4iR\x92.a:\x9d\xfa\xb4\xe5\x0c\x9cT\xb3\xee\x84:\xf1\x06P\xad`\x95*4\n\xb6l\xd5\x84\x06>\x99]\x1b\xb2\xc5\x9c\x83F\xda\xb0\x9d{\xe4\x84\x00\x83W\xe7\xaeM\xe2f\xd4\xa8\xbb\x03\xbd\xea5kE\x88_\xbf\x80\x0fy\x1a\\\xb6\x08\x92\xc3\x87\x01\x070\xe5\x00\x02\xe3\xa9-\x80\xc4\x80\x1cY\xe0dS\x94-_\x0ezd3\xe7\xce\xa8>\x83\x0e=Zf\x92\x13\xa7Gm\x18 \xe1\xaf\xe7\xd5\xb8+\xb7\xceX8\xf6(\xda\xa2D\xd9N\x8d\xbb\xb8n\xc6\x8e}\x8f\xfa\x12<\xf8\xf0\xcf\x11\x1a\x14\x07}|mf\xdf\x00\x9elP\xd1\\\xb8dSaJ\x95\xffz }zu\xadiLs\xa6\xb0&8\x80\x01\xdd\x9f\x9b\x8a ^<\xf9\xe9\xac\xa9:\x82\x1d{\x83\x84\xe6\xef\xc5\xf7\x1d}\xf5\xd9W\x9eq\xa2\x1d\x95\x84a\xb1\xa9\xb0\x01\x00\xdd\x05\xd8\x9c|\x04\x16X\x8a\x02\x0b0\x80\x9f\x0b=\xe8\x94\\l\x1et \n\x00\x10\x02\x08\xdf\x84\x03ZX \x86\x1a\x16W\x03\x87+]\xe7[\x06\x00\x96\xe8\xde\x89\xce\xa5\xa8\xe2\x8a\x19N\xf7b\x87\x19\xa5\x17\x1b\x05\xa3P\x10\xa1\x8d#\xe2X\x9b\x8e;\xf2\xd8"n/\xd6\xd5\xdf\x13\xa2x\x80$\x89\x11\x9e\xd8\x81\x16\x146\xb9#\x8b\xd3\xf9\xe6\xc1\x7f\xa2\x0cp\xe5\x99\x12\xa8\x80\xdad\x15zi!\x98\xab\xf9Ff\x99gvG$g\xdf1\xa0\x80\x9bM\xc2\t\x19\x00\x19p\xd9\x9d\x99G6\xd7Hl\xdf\x99\xc2\xc8\x9e|~\t\x88)~Q@c\x99\xa3\x0cZg\x06\x00\xf8\x96\xa8)\x0c,\xc0h\xa3\x05^\x02\xe9(\x93Rji\x84\xcb)\'\x1fn\x9d~\nj)\xa3\x0e\xffZis\x84\x06\xd7\x81\xaak\xae\xc6\x01\x07\xa0\xb5\xfa*\xac~\xc9z\xaa\x04\x03l\x80+b\xb7\x81V@\x01$\xac\xd6\xe9\xab\xb1\xd2:kpj\x0ep\xe7\xb1\xab\x9aRA\x01!\x14\xd7\xc0\x03\x8dF\x1b\xdc\x00\xd3\x8ar-\xb6\xc8\x12\x07Z\t\x15\xf0:\xdd\xb7n\x8ak\xaa(\x1ddz\xac\x14\x86\x80\x92+~\xf8\xc1\xbb\xa3\xbc\xe4\xae\xe1\x01\xbaR\xfcAG\'\\\xa4\xab\x1a\xbf\xef\x82k\xa1\xbc\x03\xa3\xeb\xd7\x1d\xa4T\xcc\x87\xc2\xc5qP\x02\xc3\xab\xf9+\x9e\xb8OH\xec\xd7\x1bYTL\x8a\x1f~\xa1\x91\xecj"\xd8\xc01n\xfe\x8e\xdaA\x06\xe7\xa2;\t)Q\xb0AJ\x15\\\xa8\xbc2h!\x14\xe0\xee\xcb\xa05\x10\xc6\xa8"s&\x07\n\x13L\xb0sA\x0b\x9b\xa2\x81\x08"h\xf02\x0f\x15\xe0\x964g2\xa8\xd1D\xd3\xa4\xe8\x01\xf5t\x1c\x14`\xc6\xcb\xcbN\x11\xe7\xd6\x87]@\xca\xd7\x8f\x90\xf2\x01\x08#\x10t\x80$\xc5\x99\xc1-\xc7?\x14\xff@\xc6\xdal\x8f\xe2\x04)b0\xb1\t)}\x84\x12J&\x04\x05\x02\xc5\x18\xb8\xd9P\xc0\x0f\x1c\x93`5h\x81_\xb0H(j\x98\xacD( \xc0`P\xc5\x8f\x83\xa6\xc1\xb6;l1\x9d\x06\x1bk\x9d4\x18:(\x1e\n\x15&sR\xb7A9\xc0Q\xf1 \x18X\x00Z\xdf<\x84\xa0:h$H^\x1cgC\\\xa0\xdc\x10\x9a\xc8\xae8\x11gdQ\x07\x01\x07!\x10\n\x11W| {\xef\xa6\x90\xb0m\x01"T B\x01<\xa8\xed\xba_X|pE\x1e\xa7\xc9\xe0D\x19\xce\xcb\xbe\x04\xf5\x08\x11\x80@\x02\xf1+\xce}\t!\xecP\xc1\x0ed\xb8\xdc\xf9\x86\xa0\x88\x8aQA\x06\x90\xc1\x02\xfc\xf2G\x83\x1c4\xc4~\xf8\xcb\x1f\xf7^v\x98D\x98\x0c\x07\xca\x1b\xc5\x05\xba\x90\xbfP`Bt\x14\x81`\x07\'\xc8/\xbf\xc8@\toC\x01)\x9c\x00\xbb\x0e\xd2\xcd$"\x94\xa0\xef\xf0\xe3\x978\xe0l\x02^ \x05\x07\xf3\x97\x00\x04\xd0\xaf%1t\xde\x0b|X\xb0\x820\x8db\x0f\xa4`\xc2\x04\x16@\x8a\x0e\xce\x8f(\x02\t\xa2\xec\x86X\xc4\xb5\x15"\x898\xc4A\xfc\x1a\x08\xc5\x82HQqT\xc4\xdc("A\n<\x08\x02\x05\x94\x90\x1d\r@\xd8E\x83|1\x14T\xbc\x80\x0e>@\n\x14\x88An\xa0\xbb]\x1b\x13\xf2F\xd9Y\xc2dg\xe8\xe1\x1e\x1d\xd2\xc7P\xa0\x10\x07\x84\xf8\xe1 \x1fx\xbf\xfc\x11\xa1\x12\x90XdG\x82\xb8FI\x02q\t/\xb4\xa4&[\x12\x10\x00;', + "png", +) +ARRAY_TO_BASE64_IMAGE = ( + "data:image/png;base64," + "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" +) +BASE64_MODEL3D = { + "path": "Box.gltf", + "data": "data:;base64,ewogICAgImFzc2V0IjogewogICAgICAgICJnZW5lcmF0b3IiOiAiQ09MTEFEQTJHTFRGIiwKICAgICAgICAidmVyc2lvbiI6ICIyLjAiCiAgICB9LAogICAgInNjZW5lIjogMCwKICAgICJzY2VuZXMiOiBbCiAgICAgICAgewogICAgICAgICAgICAibm9kZXMiOiBbCiAgICAgICAgICAgICAgICAwCiAgICAgICAgICAgIF0KICAgICAgICB9CiAgICBdLAogICAgIm5vZGVzIjogWwogICAgICAgIHsKICAgICAgICAgICAgImNoaWxkcmVuIjogWwogICAgICAgICAgICAgICAgMQogICAgICAgICAgICBdLAogICAgICAgICAgICAibWF0cml4IjogWwogICAgICAgICAgICAgICAgMS4wLAogICAgICAgICAgICAgICAgMC4wLAogICAgICAgICAgICAgICAgMC4wLAogICAgICAgICAgICAgICAgMC4wLAogICAgICAgICAgICAgICAgMC4wLAogICAgICAgICAgICAgICAgMC4wLAogICAgICAgICAgICAgICAgLTEuMCwKICAgICAgICAgICAgICAgIDAuMCwKICAgICAgICAgICAgICAgIDAuMCwKICAgICAgICAgICAgICAgIDEuMCwKICAgICAgICAgICAgICAgIDAuMCwKICAgICAgICAgICAgICAgIDAuMCwKICAgICAgICAgICAgICAgIDAuMCwKICAgICAgICAgICAgICAgIDAuMCwKICAgICAgICAgICAgICAgIDAuMCwKICAgICAgICAgICAgICAgIDEuMAogICAgICAgICAgICBdCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJtZXNoIjogMAogICAgICAgIH0KICAgIF0sCiAgICAibWVzaGVzIjogWwogICAgICAgIHsKICAgICAgICAgICAgInByaW1pdGl2ZXMiOiBbCiAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICAgImF0dHJpYnV0ZXMiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICJOT1JNQUwiOiAxLAogICAgICAgICAgICAgICAgICAgICAgICAiUE9TSVRJT04iOiAyCiAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICAiaW5kaWNlcyI6IDAsCiAgICAgICAgICAgICAgICAgICAgIm1vZGUiOiA0LAogICAgICAgICAgICAgICAgICAgICJtYXRlcmlhbCI6IDAKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgXSwKICAgICAgICAgICAgIm5hbWUiOiAiTWVzaCIKICAgICAgICB9CiAgICBdLAogICAgImFjY2Vzc29ycyI6IFsKICAgICAgICB7CiAgICAgICAgICAgICJidWZmZXJWaWV3IjogMCwKICAgICAgICAgICAgImJ5dGVPZmZzZXQiOiAwLAogICAgICAgICAgICAiY29tcG9uZW50VHlwZSI6IDUxMjMsCiAgICAgICAgICAgICJjb3VudCI6IDM2LAogICAgICAgICAgICAibWF4IjogWwogICAgICAgICAgICAgICAgMjMKICAgICAgICAgICAgXSwKICAgICAgICAgICAgIm1pbiI6IFsKICAgICAgICAgICAgICAgIDAKICAgICAgICAgICAgXSwKICAgICAgICAgICAgInR5cGUiOiAiU0NBTEFSIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAiYnVmZmVyVmlldyI6IDEsCiAgICAgICAgICAgICJieXRlT2Zmc2V0IjogMCwKICAgICAgICAgICAgImNvbXBvbmVudFR5cGUiOiA1MTI2LAogICAgICAgICAgICAiY291bnQiOiAyNCwKICAgICAgICAgICAgIm1heCI6IFsKICAgICAgICAgICAgICAgIDEuMCwKICAgICAgICAgICAgICAgIDEuMCwKICAgICAgICAgICAgICAgIDEuMAogICAgICAgICAgICBdLAogICAgICAgICAgICAibWluIjogWwogICAgICAgICAgICAgICAgLTEuMCwKICAgICAgICAgICAgICAgIC0xLjAsCiAgICAgICAgICAgICAgICAtMS4wCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ0eXBlIjogIlZFQzMiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJidWZmZXJWaWV3IjogMSwKICAgICAgICAgICAgImJ5dGVPZmZzZXQiOiAyODgsCiAgICAgICAgICAgICJjb21wb25lbnRUeXBlIjogNTEyNiwKICAgICAgICAgICAgImNvdW50IjogMjQsCiAgICAgICAgICAgICJtYXgiOiBbCiAgICAgICAgICAgICAgICAwLjUsCiAgICAgICAgICAgICAgICAwLjUsCiAgICAgICAgICAgICAgICAwLjUKICAgICAgICAgICAgXSwKICAgICAgICAgICAgIm1pbiI6IFsKICAgICAgICAgICAgICAgIC0wLjUsCiAgICAgICAgICAgICAgICAtMC41LAogICAgICAgICAgICAgICAgLTAuNQogICAgICAgICAgICBdLAogICAgICAgICAgICAidHlwZSI6ICJWRUMzIgogICAgICAgIH0KICAgIF0sCiAgICAibWF0ZXJpYWxzIjogWwogICAgICAgIHsKICAgICAgICAgICAgInBick1ldGFsbGljUm91Z2huZXNzIjogewogICAgICAgICAgICAgICAgImJhc2VDb2xvckZhY3RvciI6IFsKICAgICAgICAgICAgICAgICAgICAwLjgwMDAwMDAxMTkyMDkyOSwKICAgICAgICAgICAgICAgICAgICAwLjAsCiAgICAgICAgICAgICAgICAgICAgMC4wLAogICAgICAgICAgICAgICAgICAgIDEuMAogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJtZXRhbGxpY0ZhY3RvciI6IDAuMAogICAgICAgICAgICB9LAogICAgICAgICAgICAibmFtZSI6ICJSZWQiCiAgICAgICAgfQogICAgXSwKICAgICJidWZmZXJWaWV3cyI6IFsKICAgICAgICB7CiAgICAgICAgICAgICJidWZmZXIiOiAwLAogICAgICAgICAgICAiYnl0ZU9mZnNldCI6IDU3NiwKICAgICAgICAgICAgImJ5dGVMZW5ndGgiOiA3MiwKICAgICAgICAgICAgInRhcmdldCI6IDM0OTYzCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJidWZmZXIiOiAwLAogICAgICAgICAgICAiYnl0ZU9mZnNldCI6IDAsCiAgICAgICAgICAgICJieXRlTGVuZ3RoIjogNTc2LAogICAgICAgICAgICAiYnl0ZVN0cmlkZSI6IDEyLAogICAgICAgICAgICAidGFyZ2V0IjogMzQ5NjIKICAgICAgICB9CiAgICBdLAogICAgImJ1ZmZlcnMiOiBbCiAgICAgICAgewogICAgICAgICAgICAiYnl0ZUxlbmd0aCI6IDY0OCwKICAgICAgICAgICAgInVyaSI6ICJkYXRhOmFwcGxpY2F0aW9uL29jdGV0LXN0cmVhbTtiYXNlNjQsQUFBQUFBQUFBQUFBQUlBL0FBQUFBQUFBQUFBQUFJQS9BQUFBQUFBQUFBQUFBSUEvQUFBQUFBQUFBQUFBQUlBL0FBQUFBQUFBZ0w4QUFBQUFBQUFBQUFBQWdMOEFBQUFBQUFBQUFBQUFnTDhBQUFBQUFBQUFBQUFBZ0w4QUFBQUFBQUNBUHdBQUFBQUFBQUFBQUFDQVB3QUFBQUFBQUFBQUFBQ0FQd0FBQUFBQUFBQUFBQUNBUHdBQUFBQUFBQUFBQUFBQUFBQUFnRDhBQUFBQUFBQUFBQUFBZ0Q4QUFBQUFBQUFBQUFBQWdEOEFBQUFBQUFBQUFBQUFnRDhBQUFBQUFBQ0F2d0FBQUFBQUFBQUFBQUNBdndBQUFBQUFBQUFBQUFDQXZ3QUFBQUFBQUFBQUFBQ0F2d0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBSUMvQUFBQUFBQUFBQUFBQUlDL0FBQUFBQUFBQUFBQUFJQy9BQUFBQUFBQUFBQUFBSUMvQUFBQXZ3QUFBTDhBQUFBL0FBQUFQd0FBQUw4QUFBQS9BQUFBdndBQUFEOEFBQUEvQUFBQVB3QUFBRDhBQUFBL0FBQUFQd0FBQUw4QUFBQS9BQUFBdndBQUFMOEFBQUEvQUFBQVB3QUFBTDhBQUFDL0FBQUF2d0FBQUw4QUFBQy9BQUFBUHdBQUFEOEFBQUEvQUFBQVB3QUFBTDhBQUFBL0FBQUFQd0FBQUQ4QUFBQy9BQUFBUHdBQUFMOEFBQUMvQUFBQXZ3QUFBRDhBQUFBL0FBQUFQd0FBQUQ4QUFBQS9BQUFBdndBQUFEOEFBQUMvQUFBQVB3QUFBRDhBQUFDL0FBQUF2d0FBQUw4QUFBQS9BQUFBdndBQUFEOEFBQUEvQUFBQXZ3QUFBTDhBQUFDL0FBQUF2d0FBQUQ4QUFBQy9BQUFBdndBQUFMOEFBQUMvQUFBQXZ3QUFBRDhBQUFDL0FBQUFQd0FBQUw4QUFBQy9BQUFBUHdBQUFEOEFBQUMvQUFBQkFBSUFBd0FDQUFFQUJBQUZBQVlBQndBR0FBVUFDQUFKQUFvQUN3QUtBQWtBREFBTkFBNEFEd0FPQUEwQUVBQVJBQklBRXdBU0FCRUFGQUFWQUJZQUZ3QVdBQlVBIgogICAgICAgIH0KICAgIF0KfQo=", +} +SUM_PIXELS_INTERPRETATION = { + "scores": [ + [ + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.9217332561281606, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.8478093032233159, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + 0.7775525960239336, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.28228141285466124, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.7110596409959468, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.5717043041883806, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.17004439297432927, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.6232387569967188, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.349160393746381, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + 0.37415556842308434, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + [ + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.4147847905809689, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.21617448369040726, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.4393939393939394, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + 0.8667245705462266, + ], + ] + ], + "alternative_outputs": [ + [ + [1793106], + [1795539], + [1797837], + [1800021], + [1815417], + [1802088], + [1806420], + [1824192], + [1818906], + [1804818], + [1813338], + [1812561], + [1811298], + [1817472], + [1810533], + [1797249], + ] + ], +} +SUM_PIXELS_SHAP_INTERPRETATION = { + "scores": [ + [ + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.36599426908032084, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.9044030984144017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.5780729041010304, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.03706410007949775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.4724172299368354, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + 0.5148839775509372, + ], + ] + ], + "alternative_outputs": [[]], +} + +FILE_TEMPLATE_CONTEXT = { + "file_count": "single", + "value": { + "path": "sample_file.pdf", + "size": 10558, + "data": "data:application/pdf;base64,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", + }, + "path": "file", + "label": None, + "show_label": True, + "style": {}, + "elem_id": None, + "interactive": None, + "visible": True, +} +BASE64_MICROPHONE = { + "path": "/var/folders/t1/j7cmtcgd0mx43jh9nj_r9mmw0000gn/T/audiovb4gqjpc.wav", + "data": "data:audio/wav;base64,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", +} diff --git a/client/python/test/requirements.txt b/client/python/test/requirements.txt new file mode 100644 index 0000000..ff528cf --- /dev/null +++ b/client/python/test/requirements.txt @@ -0,0 +1,5 @@ +pytest-asyncio +pytest==7.1.2 +pytest-xdist +ty==0.0.2 +pydub==0.25.1 diff --git a/client/python/test/test_client.py b/client/python/test/test_client.py new file mode 100644 index 0000000..78c6c60 --- /dev/null +++ b/client/python/test/test_client.py @@ -0,0 +1,1224 @@ +from __future__ import annotations + +import pathlib +import tempfile +import threading +import time +import uuid +from concurrent.futures import CancelledError, TimeoutError, wait +from contextlib import contextmanager +from datetime import datetime, timedelta +from pathlib import Path +from unittest.mock import MagicMock, patch + +import gradio as gr +import httpx +import huggingface_hub +import pytest +from huggingface_hub.utils import RepositoryNotFoundError + +from gradio_client import Client, handle_file +from gradio_client.client import DEFAULT_TEMP_DIR +from gradio_client.exceptions import AuthenticationError +from gradio_client.utils import ( + Communicator, + ProgressUnit, + QueueError, + Status, + StatusUpdate, +) + +HF_TOKEN = huggingface_hub.get_token() + + +@contextmanager +def connect( + demo: gr.Blocks, + download_files: str = DEFAULT_TEMP_DIR, + client_kwargs: dict | None = None, + **kwargs, +): + _, local_url, _ = demo.launch(prevent_thread_lock=True, **kwargs) + if client_kwargs is None: + client_kwargs = {} + try: + yield Client(local_url, download_files=download_files, **client_kwargs) + finally: + # A more verbose version of .close() because we should set a timeout + # the tests that call .cancel() can get stuck waiting for the thread to join + demo.close() + + +class TestClientInitialization: + def test_headers_constructed_correctly(self, increment_demo): + if not HF_TOKEN: + pytest.skip("HF_TOKEN is not set, skipping test") + _, local_url, _ = increment_demo.launch(prevent_thread_lock=True) + try: + client = Client(local_url, token=HF_TOKEN) + assert { + "x-hf-authorization": f"Bearer {HF_TOKEN}" + }.items() <= client.headers.items() + client = Client( + local_url, + token=HF_TOKEN, + headers={"additional": "value"}, + ) + assert { + "x-hf-authorization": f"Bearer {HF_TOKEN}", + "additional": "value", + }.items() <= client.headers.items() + client = Client( + local_url, + token=HF_TOKEN, + headers={"authorization": "Bearer abcde"}, + ) + assert {"authorization": "Bearer abcde"}.items() <= client.headers.items() + finally: + increment_demo.close() + + @pytest.mark.serial + def test_many_endpoint_demo_loads_quickly(self, many_endpoint_demo): + import datetime + + start = datetime.datetime.now() + with connect(many_endpoint_demo): + pass + assert (datetime.datetime.now() - start).seconds < 5 + + @pytest.mark.parametrize( + "cookies,expected", + [ + (None, {}), # Falsy values coherced to empty dict + ({}, {}), # Empty does not make any difference + ({"test-cookie": "abc"}, {"test-cookie": "abc"}), # Well-formed cookies + ], + ) + def test_httpx_cookies_kwarg_is_used_by_client( + self, cookies, expected, monkeypatch + ): + monkeypatch.setattr(threading, "Thread", lambda *_, **__: MagicMock()) + monkeypatch.setattr(Client, "_space_name_to_src", lambda _, src: src) + monkeypatch.setattr( + Client, "_get_space_state", lambda _: huggingface_hub.SpaceStage.RUNNING + ) + + with patch("httpx.get") as mocked: + mocked.return_value = httpx.Response( + 200, + json={ + "version": "3.36.2", # Force recent version branch + "dependencies": [], + "named_endpoints": {}, + "unnamed_endpoints": {}, + }, + request=httpx.Request("GET", "https://fake/space"), + ) + client = Client("fake/space", httpx_kwargs={"cookies": cookies}) + for call in mocked.call_args_list: + assert call.kwargs["cookies"] == expected, ( + "Client instantiation missing cookies" + ) + + # _login overrides cookies + response = httpx.Response(200) + response._cookies = httpx.Cookies(cookies) + with patch("httpx.post", return_value=response) as mocked: + client._login(("user", "pass")) + mocked.assert_called_once() + call = mocked.call_args + assert "cookies" not in call.kwargs, "_login call incorporate cookies" + assert client.cookies == expected, "_login does not set client cookies" + + +class TestClientPredictions: + @pytest.mark.flaky + def test_raise_error_invalid_state(self): + with pytest.raises(ValueError, match="invalid state"): + Client("gradio-tests/paused-space") + + def test_raise_error_max_file_size(self, max_file_size_demo): + with connect(max_file_size_demo, max_file_size="15kb") as client: + with pytest.raises(ValueError, match="exceeds the maximum file size"): + client.predict( + handle_file( + Path(__file__).parents[3] + / "gradio" + / "media_assets" + / "images" + / "cheetah1.jpg" + ), + api_name="/upload_1b", + ) + client.predict( + handle_file(Path(__file__).parent / "files" / "alphabet.txt"), + api_name="/upload_1b", + ) + + @pytest.mark.flaky + def test_private_space(self): + space_id = "gradio-tests/not-actually-private-space" + api = huggingface_hub.HfApi() + assert api.space_info(space_id).private + client = Client(space_id, token=HF_TOKEN) + output = client.predict("abc", api_name="/predict") + assert output == "abc" + + @pytest.mark.flaky + def test_private_space_v4_sse_v1(self): + space_id = "gradio-tests/not-actually-private-spacev4-sse-v1" + api = huggingface_hub.HfApi() + assert api.space_info(space_id).private + client = Client( + space_id, + token=HF_TOKEN, + ) + output = client.predict("abc", api_name="/predict") + assert output == "abc" + + @pytest.mark.flaky + def test_space_with_files_v4_sse_v2(self): + space_id = "gradio-tests/space_with_files_v4_sse_v2" + client = Client(space_id) + payload = ( + handle_file( + "https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3" + ), + { + "video": handle_file( + "https://github.com/gradio-app/gradio/raw/main/demo/video_component/files/world.mp4" + ), + "subtitle": None, + }, + handle_file( + "https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3" + ), + ) + output = client.predict(*payload, api_name="/predict") + assert output[0].endswith(".wav") # Audio files are converted to wav + assert output[1]["video"].endswith( + "world.mp4" + ) # Video files are not converted by default + assert "sample-0.mp3" in output[2] + + def test_state(self, increment_demo): + with connect(increment_demo) as client: + output = client.predict(api_name="/increment_without_queue") + assert output == 1 + output = client.predict(api_name="/increment_without_queue") + assert output == 2 + output = client.predict(api_name="/increment_without_queue") + assert output == 3 + client.reset_session() + output = client.predict(api_name="/increment_without_queue") + assert output == 1 + output = client.predict(api_name="/increment_with_queue") + assert output == 2 + client.reset_session() + output = client.predict(api_name="/increment_with_queue") + assert output == 1 + output = client.predict(api_name="/increment_with_queue") + assert output == 2 + + def test_job_status(self, calculator_demo): + with connect(calculator_demo) as client: + statuses = [] + job = client.submit(5, "add", 4, api_name="/predict") + while not job.done(): + time.sleep(0.1) + statuses.append(job.status()) + + assert statuses + # Messages are sorted by time + assert sorted([s.time for s in statuses if s]) == [ + s.time for s in statuses if s + ] + assert sorted([s.code for s in statuses if s]) == [ + s.code for s in statuses if s + ] + + @pytest.mark.flaky + def test_intermediate_outputs(self, count_generator_demo): + with connect(count_generator_demo) as client: + job = client.submit(3, fn_index=0) + + while not job.done(): + time.sleep(0.1) + + assert job.outputs() == [str(i) for i in range(3)] + + outputs = [] + for o in client.submit(3, fn_index=0): + outputs.append(o) + assert outputs == [str(i) for i in range(3)] + + def test_break_in_loop_if_error(self, calculator_demo): + with connect(calculator_demo) as client: + job = client.submit("foo", "add", 4, fn_index=0) + output = list(job) + assert output == [] + + @pytest.mark.flaky + def test_timeout(self, sentiment_classification_demo): + with pytest.raises(TimeoutError): + with connect(sentiment_classification_demo.queue()) as client: + job = client.submit(api_name="/sleep") + job.result(timeout=0.05) + + @pytest.mark.flaky + def test_timeout_no_queue(self, sentiment_classification_demo): + with pytest.raises(TimeoutError): + with connect(sentiment_classification_demo) as client: + job = client.submit(api_name="/sleep") + job.result(timeout=0.1) + + def test_raises_exception(self, calculator_demo): + with pytest.raises(Exception): + with connect(calculator_demo) as client: + job = client.submit("foo", "add", 9, fn_index=0) + job.result() + + @pytest.mark.flaky + def test_job_output_video(self, video_component): + with connect(video_component) as client: + job = client.submit( + handle_file( + "https://huggingface.co/datasets/freddyaboulton/bucket/resolve/main/ProgressNotifications.mp4" + ), + api_name="/predict", + ) + assert Path(job.result()).exists() + assert ( + Path(DEFAULT_TEMP_DIR).resolve() in Path(job.result()).resolve().parents + ) + + temp_dir = tempfile.mkdtemp() + with connect(video_component, download_files=temp_dir) as client: + job = client.submit( + handle_file( + "https://huggingface.co/spaces/gradio/video_component/resolve/main/files/a.mp4" + ), + fn_index=0, + ) + assert Path(job.result()).exists() + assert Path(temp_dir).resolve() in Path(job.result()).resolve().parents + + def test_progress_updates(self, progress_demo): + with connect(progress_demo) as client: + job = client.submit("hello", api_name="/predict") + statuses = [] + while not job.done(): + statuses.append(job.status()) + time.sleep(0.02) + assert any(s.code == Status.PROGRESS for s in statuses) + assert any(s.progress_data is not None for s in statuses) + all_progress_data = [ + p for s in statuses if s.progress_data for p in s.progress_data + ] + count = 0 + for i in range(20): + unit = ProgressUnit( + index=i, length=20, unit="steps", progress=None, desc=None + ) + count += unit in all_progress_data + assert count + + def test_upload_and_download_with_auth(self): + demo = gr.Interface(lambda x: x, "text", "text", api_name="predict") + _, url, _ = demo.launch(auth=("user", "pass"), prevent_thread_lock=True) + with pytest.raises(AuthenticationError): + client = Client(url) + client = Client(url, auth=("user", "pass")) + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + f.write("Hello file!") + output = client.predict(f.name, api_name="/predict") + with open(output) as f: + assert f.read() == "Hello file!" + + def test_upload_preserves_orig_name(self): + demo = gr.Interface(lambda x: x, "image", "text", api_name="predict") + with connect(demo) as client: + test_file = ( + Path(__file__).parent.parent.parent.parent + / "gradio" + / "media_assets" + / "images" + / "cheetah1.jpg" + ) + output = client.endpoints[0]._upload_file({"path": test_file}, data_index=0) + assert output["orig_name"] == "cheetah1.jpg" + + output = client.endpoints[0]._upload_file( + { + "path": "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png" + }, + data_index=0, + ) + assert output["orig_name"] == "bus.png" + + @pytest.mark.flaky(reruns=5) + def test_cancel_from_client_queued(self, cancel_from_client_demo): + with connect(cancel_from_client_demo) as client: + start = time.time() + job = client.submit(api_name="/long") + while not job.done(): + if job.status().code == Status.STARTING: + job.cancel() + break + with pytest.raises(CancelledError): + job.result() + # The whole prediction takes 10 seconds to run + # and does not iterate. So this tests that we can cancel + # halfway through a prediction + assert time.time() - start < 10 + assert job.status().code == Status.CANCELLED + + job = client.submit(api_name="/iterate") + iteration_count = 0 + while not job.done(): + if job.status().code == Status.ITERATING: + iteration_count += 1 + if iteration_count == 3: + job.cancel() + break + time.sleep(0.5) + # Result for iterative jobs will raise there is an exception + with pytest.raises(Exception): + job.result() + # The whole prediction takes 10 seconds to run + # and does not iterate. So this tests that we can cancel + # halfway through a prediction + assert time.time() - start < 10 + + # Test that we did not iterate all the way to the end + assert all(o in [0, 1, 2, 3, 4, 5] for o in job.outputs()) + assert job.status().code == Status.CANCELLED + + def test_job_cancel_stops_upstream_server_if_cancel_event_defined(self): + global current_step + current_step = 0 + + def iteration_quick(): + for i in range(20): + print(f"i: {i}") + global current_step + current_step = i + yield i + time.sleep(0.1) + + with gr.Blocks() as demo: + num = gr.Number() + + btn = gr.Button(value="Iterate") + iterate_quick = btn.click( + iteration_quick, None, num, api_name="iterate_quick" + ) + btn3 = gr.Button(value="Cancel") + btn3.click(None, None, None, cancels=[iterate_quick]) + + with connect(demo) as client: + job = client.submit(api_name="/iterate_quick") + while len(job.outputs()) < 5: + time.sleep(0.1) + job.cancel() + time.sleep(2) + + assert current_step < 19 + + def test_cancel_subsequent_jobs_state_reset(self, yield_demo): + with connect(yield_demo) as client: + job1 = client.submit("abcdefefadsadfs", api_name="/predict") + time.sleep(3) + job1.cancel() + + assert len(job1.outputs()) > 0 + assert len(job1.outputs()) < len("abcdefefadsadfs") + assert job1.status().code == Status.CANCELLED + + job2 = client.submit("abcd", api_name="/predict") + assert len(job2.outputs()) == 0 + while not job2.done(): + time.sleep(0.1) + # Ran all iterations from scratch + assert job2.status().code == Status.FINISHED + assert len(job2.outputs()) == 4 + + def test_does_not_upload_dir(self, stateful_chatbot): + with connect(stateful_chatbot) as client: + initial_history = [ + {"role": "user", "content": [{"text": "", "type": "text"}]} + ] + message = "Hello" + ret = client.predict(message, initial_history, api_name="/submit") + assert ret == ( + "", + [ + { + "role": "user", + "content": [{"type": "text", "text": ""}], + "metadata": None, + "options": None, + }, + { + "role": "user", + "content": [{"type": "text", "text": "Hello"}], + "metadata": None, + "options": None, + }, + { + "role": "assistant", + "content": [{"type": "text", "text": "I love you"}], + "metadata": None, + "options": None, + }, + ], + ) + + def test_return_layout_component(self, hello_world_with_group): + with connect(hello_world_with_group) as demo: + assert demo.predict("Freddy", api_name="/greeting") == "Hello Freddy" + assert demo.predict(api_name="/show_group") == () + + def test_return_layout_and_state_components( + self, hello_world_with_state_and_accordion + ): + with connect(hello_world_with_state_and_accordion) as demo: + assert demo.predict("Freddy", api_name="/greeting") == ("Hello Freddy", 1) + assert demo.predict("Abubakar", api_name="/greeting") == ( + "Hello Abubakar", + 2, + ) + assert demo.predict(api_name="/open") == 3 + assert demo.predict(api_name="/close") == 4 + assert demo.predict("Ali", api_name="/greeting") == ("Hello Ali", 5) + + def test_long_response_time_with_gr_info_and_big_payload( + self, long_response_with_info + ): + with connect(long_response_with_info) as demo: + assert demo.predict(api_name="/predict") == "\ta\nb" * 90000 + + def test_queue_full_raises_error(self): + import time + + demo = gr.Interface( + lambda s: time.sleep(1) or f"Hello {s}", + "textbox", + "textbox", + api_name="predict", + ).queue(max_size=1) + with connect(demo) as client: + with pytest.raises(QueueError): + job1 = client.submit("Freddy", api_name="/predict") + job2 = client.submit("Abubakar", api_name="/predict") + job3 = client.submit("Pete", api_name="/predict") + wait([job1, job2, job3]) + job1.result() + job2.result() + job3.result() + + def test_json_parse_error(self): + data = ( + "Bonjour Olivier, tu as l'air bien r\u00e9veill\u00e9 ce matin. Tu veux que je te pr\u00e9pare tes petits-d\u00e9j.\n", + None, + ) + + def return_bad(): + return data + + demo = gr.Interface(return_bad, None, ["text", "text"], api_name="predict") + with connect(demo) as client: + pred = client.predict(api_name="/predict") + assert pred[0] == data[0] + + def test_state_reset_when_session_changes(self, capsys, state_demo, monkeypatch): + monkeypatch.setenv("GRADIO_IS_E2E_TEST", "1") + with connect(state_demo) as client: + client.predict("Hello", api_name="/predict") + client.reset_session() + time.sleep(5) + out = capsys.readouterr().out + assert "STATE DELETED" in out + + @pytest.mark.flaky + def test_add_zero_gpu_headers_no_gradio_context(self): + client = Client("gradio/calculator") + headers = {"existing": "header"} + new_headers = client.add_zero_gpu_headers(headers) + assert new_headers == headers # No changes when not in Gradio context + + @pytest.mark.flaky + def test_add_zero_gpu_headers_with_ip_token(self, monkeypatch): + client = Client("gradio/calculator") + headers = {"existing": "header"} + + class MockRequest: + headers = {"x-ip-token": "test-token"} + + class MockContext: + request = MagicMock() + request.get.return_value = MockRequest() + + monkeypatch.setattr("gradio.context.LocalContext", MockContext) + new_headers = client.add_zero_gpu_headers(headers) + assert new_headers == {"existing": "header", "x-ip-token": "test-token"} + + def test_multiple_newlines_in_output(self): + def test(): + return """before\x85after""" + + demo = gr.Interface(fn=test, inputs=[], outputs=["text"], api_name="predict") + with connect(demo) as client: + result = client.predict(api_name="/predict") + assert result == "before\x85after" + + +class TestClientPredictionsWithKwargs: + def test_no_default_params(self, calculator_demo): + with connect(calculator_demo) as client: + result = client.predict( + num1=3, operation="add", num2=3, api_name="/predict" + ) + assert result == 6 + + result = client.predict(33, operation="add", num2=3, api_name="/predict") + assert result == 36 + + def test_default_params(self, calculator_demo_with_defaults): + with connect(calculator_demo_with_defaults) as client: + result = client.predict(num2=10, api_name="/predict") + assert result == 20 + + result = client.predict(num2=33, operation="multiply", api_name="/predict") + assert result == 330 + + def test_missing_params(self, hello_world_demo): + with connect(hello_world_demo) as client: + with pytest.raises( + TypeError, match="No value provided for required argument: punctuation" + ): + client.predict(name="Alice", api_name="/greet") + + def test_chatbot_message_format(self, chatbot_message_format): + with connect(chatbot_message_format) as client: + _, history = client.predict("hello", [], api_name="/chat") + assert history[1]["role"] == "assistant" + assert history[1]["content"][0]["text"] in [ + "How are you?", + "I love you", + "I'm very hungry", + ] + _, history = client.predict("hi", history, api_name="/chat") + assert history[2]["role"] == "user" + assert history[2]["content"][0]["text"] == "hi" + assert history[3]["role"] == "assistant" + assert history[3]["content"][0]["text"] in [ + "How are you?", + "I love you", + "I'm very hungry", + ] + + def test_client_forwards_event_data(self): + with gr.Blocks() as demo: + button = gr.Button("Click me") + output = gr.JSON() + + def return_event_data(evt: gr.EventData): + return evt._data + + button.click( + fn=return_event_data, inputs=None, outputs=output, api_name="click" + ) + + with connect(demo) as client: + fn = client.endpoints[0].make_end_to_end_fn(client.new_helper(0)) + result = fn(event_data={"foo": "bar", "baz": 123}, api_name="/click") + assert result == {"foo": "bar", "baz": 123} + + +class TestStatusUpdates: + @patch("gradio_client.client.Endpoint.make_end_to_end_fn") + def test_messages_passed_correctly(self, mock_make_end_to_end_fn, calculator_demo): + now = datetime.now() + + messages = [ + StatusUpdate( + code=Status.STARTING, + eta=None, + rank=None, + success=None, + queue_size=None, + time=now, + progress_data=None, + ), + StatusUpdate( + code=Status.SENDING_DATA, + eta=None, + rank=None, + success=None, + queue_size=None, + time=now + timedelta(seconds=1), + progress_data=None, + ), + StatusUpdate( + code=Status.IN_QUEUE, + eta=3, + rank=2, + queue_size=2, + success=None, + time=now + timedelta(seconds=2), + progress_data=None, + ), + StatusUpdate( + code=Status.IN_QUEUE, + eta=2, + rank=1, + queue_size=1, + success=None, + time=now + timedelta(seconds=3), + progress_data=None, + ), + StatusUpdate( + code=Status.ITERATING, + eta=None, + rank=None, + queue_size=None, + success=None, + time=now + timedelta(seconds=3), + progress_data=None, + ), + StatusUpdate( + code=Status.FINISHED, + eta=None, + rank=None, + queue_size=None, + success=True, + time=now + timedelta(seconds=4), + progress_data=None, + ), + ] + + class MockEndToEndFunction: + def __init__(self, communicator: Communicator): + self.communicator = communicator + + def __call__(self, *args, **kwargs): + for m in messages: + with self.communicator.lock: + self.communicator.job.latest_status = m + time.sleep(0.1) + + mock_make_end_to_end_fn.side_effect = MockEndToEndFunction + + with connect(calculator_demo) as client: + job = client.submit(5, "add", 6, api_name="/predict") + + statuses = [] + while not job.done(): + statuses.append(job.status()) + time.sleep(0.09) + + assert all(s in messages for s in statuses) + + @pytest.mark.flaky + @patch("gradio_client.client.Endpoint.make_end_to_end_fn") + def test_messages_correct_two_concurrent( + self, mock_make_end_to_end_fn, calculator_demo + ): + now = datetime.now() + + messages_1 = [ + StatusUpdate( + code=Status.STARTING, + eta=None, + rank=None, + success=None, + queue_size=None, + time=now, + progress_data=None, + ), + StatusUpdate( + code=Status.FINISHED, + eta=None, + rank=None, + queue_size=None, + success=True, + time=now + timedelta(seconds=4), + progress_data=None, + ), + ] + + messages_2 = [ + StatusUpdate( + code=Status.IN_QUEUE, + eta=3, + rank=2, + queue_size=2, + success=None, + time=now + timedelta(seconds=2), + progress_data=None, + ), + StatusUpdate( + code=Status.IN_QUEUE, + eta=2, + rank=1, + queue_size=1, + success=None, + time=now + timedelta(seconds=3), + progress_data=None, + ), + ] + + class MockEndToEndFunction: + n_counts = 0 + + def __init__(self, communicator: Communicator): + self.communicator = communicator + self.messages = ( + messages_1 if MockEndToEndFunction.n_counts == 0 else messages_2 + ) + MockEndToEndFunction.n_counts += 1 + + def __call__(self, *args, **kwargs): + for m in self.messages: + with self.communicator.lock: + print(f"here: {m}") + self.communicator.job.latest_status = m + time.sleep(0.1) + + mock_make_end_to_end_fn.side_effect = MockEndToEndFunction + + with connect(calculator_demo) as client: + job_1 = client.submit(5, "add", 6, api_name="/predict") + job_2 = client.submit(11, "subtract", 1, api_name="/predict") + + statuses_1 = [] + statuses_2 = [] + while not (job_1.done() and job_2.done()): + statuses_1.append(job_1.status()) + statuses_2.append(job_2.status()) + time.sleep(0.05) + + assert all(s in messages_1 for s in statuses_1) + + +class TestAPIInfo: + @pytest.mark.flaky + @pytest.mark.parametrize("trailing_char", ["/", ""]) + def test_test_endpoint_src(self, trailing_char): + src = "https://gradio-tests-image-identity-new.hf.space" + trailing_char + client = Client(src=src) + assert ( + client.endpoints[0].root_url + == "https://gradio-tests-image-identity-new.hf.space/gradio_api/" + ) + + def test_state_does_not_appear(self, state_demo): + with connect(state_demo) as client: + api_info = client.view_api(return_format="dict") + assert isinstance(api_info, dict) + for parameter in api_info["named_endpoints"]["/predict"]["parameters"]: + assert parameter["component"] != "State" + + @pytest.mark.flaky + def test_private_space(self): + client = Client( + "gradio-tests/not-actually-private-space", + token=HF_TOKEN, + ) + assert "/predict" in client.view_api(return_format="dict")["named_endpoints"] + + def test_api_info_of_local_demo(self, calculator_demo): + with connect(calculator_demo) as client: + api_info = client.view_api(return_format="dict") + assert isinstance(api_info, dict) + assert api_info["named_endpoints"]["/predict"] == { + "parameters": [ + { + "label": "num1", + "parameter_name": "num1", + "parameter_has_default": False, + "parameter_default": None, + "type": {"type": "number"}, + "python_type": {"type": "float", "description": ""}, + "component": "Number", + "example_input": 3, + }, + { + "label": "operation", + "parameter_name": "operation", + "parameter_has_default": False, + "parameter_default": None, + "type": { + "enum": ["add", "subtract", "multiply", "divide"], + "title": "Radio", + "type": "string", + }, + "python_type": { + "type": "Literal['add', 'subtract', 'multiply', 'divide']", + "description": "", + }, + "component": "Radio", + "example_input": "add", + }, + { + "label": "num2", + "parameter_name": "num2", + "parameter_has_default": False, + "parameter_default": None, + "type": {"type": "number"}, + "python_type": {"type": "float", "description": ""}, + "component": "Number", + "example_input": 3, + }, + ], + "returns": [ + { + "label": "output", + "type": {"type": "number"}, + "python_type": {"type": "float", "description": ""}, + "component": "Number", + } + ], + "description": "", + } + assert api_info["unnamed_endpoints"] == {} + + def test_unnamed_endpoints_use_fn_index(self, count_generator_demo): + with connect(count_generator_demo) as client: + info = client.view_api(return_format="str") + assert "fn_index" not in info + assert "api_name" in info + + def test_api_false_endpoints_do_not_appear(self, count_generator_no_api): + with connect(count_generator_no_api) as client: + info = client.view_api(return_format="dict") + assert len(info["named_endpoints"]) == 0 + + def test_api_false_endpoints_cannot_be_accessed_with_fn_index(self, increment_demo): + with connect(increment_demo) as client: + with pytest.raises(ValueError): + client.submit(1, fn_index=2) + + def test_file_io(self, file_io_demo): + with connect(file_io_demo) as client: + info = client.view_api(return_format="dict") + inputs = info["named_endpoints"]["/predict"]["parameters"] + outputs = info["named_endpoints"]["/predict"]["returns"] + + assert inputs[0]["type"]["type"] == "array" + assert inputs[0]["python_type"]["type"] == "list[filepath]" + + assert isinstance(inputs[0]["example_input"], list) + assert isinstance(inputs[0]["example_input"][0], dict) + + assert inputs[1]["python_type"]["type"] == "filepath" + assert isinstance(inputs[1]["example_input"], dict) + + assert outputs[0]["python_type"]["type"] == "list[filepath]" + assert outputs[0]["type"]["type"] == "array" + + assert outputs[1]["python_type"]["type"] == "filepath" + + def test_layout_components_in_output(self, hello_world_with_group): + with connect(hello_world_with_group) as client: + info = client.view_api(return_format="dict") + assert info == { + "named_endpoints": { + "/greeting": { + "parameters": [ + { + "label": "name", + "parameter_name": "name", + "parameter_has_default": False, + "parameter_default": None, + "type": {"type": "string"}, + "python_type": {"type": "str", "description": ""}, + "component": "Textbox", + "example_input": "Hello!!", + } + ], + "returns": [ + { + "label": "greeting", + "type": {"type": "string"}, + "python_type": {"type": "str", "description": ""}, + "component": "Textbox", + } + ], + "description": "", + }, + "/show_group": {"parameters": [], "returns": [], "description": ""}, + }, + "unnamed_endpoints": {}, + } + + def test_layout_and_state_components_in_output( + self, hello_world_with_state_and_accordion + ): + with connect(hello_world_with_state_and_accordion) as client: + info = client.view_api(return_format="dict") + assert info == { + "named_endpoints": { + "/greeting": { + "parameters": [ + { + "label": "name", + "parameter_name": "name", + "parameter_has_default": False, + "parameter_default": None, + "type": {"type": "string"}, + "python_type": {"type": "str", "description": ""}, + "component": "Textbox", + "example_input": "Hello!!", + } + ], + "returns": [ + { + "label": "greeting", + "type": {"type": "string"}, + "python_type": {"type": "str", "description": ""}, + "component": "Textbox", + }, + { + "label": "count", + "type": {"type": "number"}, + "python_type": {"type": "float", "description": ""}, + "component": "Number", + }, + ], + "description": "This is a greeting function.", + }, + "/open": { + "parameters": [], + "returns": [ + { + "label": "count", + "type": {"type": "number"}, + "python_type": {"type": "float", "description": ""}, + "component": "Number", + } + ], + "description": "", + }, + "/close": { + "parameters": [], + "returns": [ + { + "label": "count", + "type": {"type": "number"}, + "python_type": {"type": "float", "description": ""}, + "component": "Number", + } + ], + "description": "", + }, + }, + "unnamed_endpoints": {}, + } + + +class TestEndpoints: + @pytest.mark.flaky + def test_upload(self): + client = Client( + src="gradio-tests/not-actually-private-file-upload", + token=HF_TOKEN, + ) + response = MagicMock(status_code=200) + response.json.return_value = [ + "file1", + "file2", + "file3", + "file4", + "file5", + "file6", + "file7", + ] + with patch("httpx.post", MagicMock(return_value=response)): + with patch("builtins.open", MagicMock()): + with patch.object(pathlib.Path, "name") as mock_name: + mock_name.side_effect = lambda x: x + results = client.endpoints[0]._upload_file( + handle_file(__file__), data_index=0 + ) + assert results["path"] == "file1" + + @pytest.mark.flaky + def test_download_private_file(self, gradio_temp_dir): + client = Client( + src="gradio/zip_files", + ) + url_path = handle_file( + "https://gradio-tests-not-actually-private-spacev4-sse.hf.space/file=lion.jpg" + ) + file = client.endpoints[0]._upload_file(url_path, 0) # type: ignore + assert file["path"].endswith(".jpg") + + @pytest.mark.flaky + def test_download_tmp_copy_of_file_does_not_save_errors( + self, monkeypatch, gradio_temp_dir + ): + client = Client( + src="gradio/zip_files", + ) + error_response = httpx.Response(status_code=404) + monkeypatch.setattr(httpx, "get", lambda *args, **kwargs: error_response) + with pytest.raises(httpx.HTTPStatusError): + client.endpoints[0]._download_file({"path": "https://example.com/foo"}) # type: ignore + + @pytest.mark.flaky + def test_download_stream_file_uses_url_directly(self, monkeypatch, gradio_temp_dir): + """Test that stream files use the URL directly instead of constructing from path.""" + client = Client( + src="gradio/zip_files", + ) + + # Mock response for stream file + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.iter_bytes.return_value = [b"test content"] + + def mock_stream(*args, **kwargs): + # Verify that the URL is used directly for streams + called_url: str = args[1] # Second argument is the URL + assert called_url.endswith("api/stream/test_file.txt"), ( + f"Expected stream URL, got: {called_url}" + ) + return mock_response + + monkeypatch.setattr(httpx, "stream", mock_stream) + + # Test stream file with URL + stream_file_data = { + "path": "some/wrong/path", # This path should be ignored for streams + "url": "/api/stream/test_file.txt", # This URL should be used directly + "is_stream": True, + } + + with patch("pathlib.Path.resolve", return_value="/tmp/test_file.txt"): + client.endpoints[0]._download_file(stream_file_data) # type: ignore + + # Test non-stream file still uses path-based URL construction + regular_file_data = {"path": "regular/file.txt", "is_stream": False} + + def mock_stream_regular(*args, **kwargs): + called_url = args[1] + assert called_url.endswith("file=regular/file.txt"), ( + f"Expected path-based URL, got: {called_url}" + ) + return mock_response + + monkeypatch.setattr(httpx, "stream", mock_stream_regular) + + with patch("pathlib.Path.resolve", return_value="/tmp/regular_file.txt"): + client.endpoints[0]._download_file(regular_file_data) # type: ignore + + +cpu = huggingface_hub.SpaceHardware.CPU_BASIC + + +class TestDuplication: + @pytest.mark.flaky + @patch("huggingface_hub.get_space_runtime", return_value=MagicMock(hardware=cpu)) + @patch("gradio_client.client.Client.__init__", return_value=None) + def test_new_space_id(self, mock_init, mock_runtime): + Client.duplicate( + "gradio/calculator", + "test", + token=HF_TOKEN, + ) + mock_runtime.assert_any_call("gradio/calculator", token=HF_TOKEN) + mock_init.assert_called() + Client.duplicate( + "gradio/calculator", + "gradio-tests/test", + token=HF_TOKEN, + ) + mock_runtime.assert_any_call("gradio/calculator", token=HF_TOKEN) + mock_init.assert_called() + + @pytest.mark.flaky + @patch("gradio_client.utils.set_space_timeout") + @patch("huggingface_hub.get_space_runtime", return_value=MagicMock(hardware=cpu)) + @patch("gradio_client.client.Client.__init__", return_value=None) + def test_dont_set_timeout_if_default_hardware( + self, mock_init, mock_runtime, mock_set_timeout + ): + Client.duplicate( + "gradio/calculator", + "test", + ) + mock_set_timeout.assert_not_called() + + @pytest.mark.flaky + @patch("huggingface_hub.request_space_hardware") + @patch("gradio_client.utils.set_space_timeout") + @patch( + "huggingface_hub.get_space_runtime", + return_value=MagicMock(hardware=huggingface_hub.SpaceHardware.CPU_UPGRADE), + ) + @patch("gradio_client.client.Client.__init__", return_value=None) + def test_set_timeout_if_not_default_hardware( + self, mock_init, mock_runtime, mock_set_timeout, mock_request_hardware + ): + Client.duplicate( + "gradio/calculator", + "test", + hardware="cpu-upgrade", + sleep_timeout=15, + token=HF_TOKEN, + ) + assert mock_set_timeout.call_count == 1 + _, called_kwargs = mock_set_timeout.call_args + assert called_kwargs["timeout_in_seconds"] == 15 * 60 + + @pytest.mark.flaky + @patch("huggingface_hub.add_space_secret") + @patch("huggingface_hub.duplicate_space") + @patch("gradio_client.client.Client.__init__", return_value=None) + @patch("gradio_client.utils.set_space_timeout") + def test_add_secrets(self, mock_time, mock_init, mock_duplicate, mock_add_secret): + with pytest.raises(RepositoryNotFoundError): + name = str(uuid.uuid4()) + Client.duplicate( + "gradio/calculator", + name, + token=HF_TOKEN, + secrets={"test_key": "test_value", "test_key2": "test_value2"}, + ) + mock_add_secret.assert_called_with( + f"gradio-tests/{name}", + "test_key", + "test_value", + token=HF_TOKEN, + ) + mock_add_secret.assert_any_call( + f"gradio-tests/{name}", + "test_key2", + "test_value2", + token=HF_TOKEN, + ) + + +def test_httpx_kwargs(increment_demo): + with connect( + increment_demo, client_kwargs={"httpx_kwargs": {"timeout": 5}} + ) as client: + with patch("httpx.post", MagicMock()) as mock_post: + with pytest.raises(Exception): + client.predict(1, api_name="/increment_with_queue") + assert mock_post.call_args.kwargs["timeout"] == 5 + + +def test_x_gradio_user_header(): + def fn(name: str, request: gr.Request) -> str: + return f"Hello, {name}! Your x-gradio-user is {request.headers.get('x-gradio-user', 'not provided')}" + + app = gr.Interface(fn, "text", "text") + + with connect(app) as client: + res = client.submit( + "Gradio", headers={"x-gradio-user": "test-user"}, api_name="/fn" + ).result() + assert res == "Hello, Gradio! Your x-gradio-user is api" diff --git a/client/python/test/test_documentation.py b/client/python/test/test_documentation.py new file mode 100644 index 0000000..d73a9e1 --- /dev/null +++ b/client/python/test/test_documentation.py @@ -0,0 +1,7 @@ +from gradio_client import documentation + + +class TestDocumentation: + def test_website_documentation(self): + docs = documentation.generate_documentation() + assert len(docs) > 0 diff --git a/client/python/test/test_snippet.py b/client/python/test/test_snippet.py new file mode 100644 index 0000000..a7f6567 --- /dev/null +++ b/client/python/test/test_snippet.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import datetime +from contextlib import contextmanager + +import gradio as gr + +from gradio_client import Client +from gradio_client.snippet import _stringify_py, generate_code_snippets + + +@contextmanager +def connect(demo: gr.Blocks, **kwargs): + _, local_url, _ = demo.launch(prevent_thread_lock=True, **kwargs) + try: + yield Client(local_url) + finally: + demo.close() + + +class TestSnippetExecution: + def test_python_snippet_runs_for_simple_demo(self): + def greet(name): + return "Hello " + name + "!" + + demo = gr.Interface( + fn=greet, + inputs=gr.Textbox(label="Name"), + outputs=gr.Textbox(label="Greeting"), + api_name="greet", + ) + + with connect(demo) as client: + api_info = client.view_api(print_info=False, return_format="dict") + endpoint_info = api_info["named_endpoints"]["/greet"] + snippets = generate_code_snippets("/greet", endpoint_info, client.src) + + python_snippet = snippets["python"] + assert "client.predict(" in python_snippet + assert 'api_name="/greet"' in python_snippet + + namespace = {} + exec(python_snippet, namespace) + assert namespace["result"] == "Hello Hello!!!" + + def test_python_snippet_runs_for_calculator(self): + 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": + return num1 / num2 + + demo = gr.Interface( + calculator, + [ + "number", + gr.Radio(["add", "subtract", "multiply", "divide"]), + "number", + ], + "number", + api_name="predict", + ) + + with connect(demo) as client: + api_info = client.view_api(print_info=False, return_format="dict") + endpoint_info = api_info["named_endpoints"]["/predict"] + snippets = generate_code_snippets("/predict", endpoint_info, client.src) + + python_snippet = snippets["python"] + namespace = {} + exec(python_snippet, namespace) + assert namespace["result"] == 6.0 + + def test_python_snippet_runs_with_default_params(self): + def add(a, b=10): + return a + b + + demo = gr.Interface( + add, + [gr.Number(label="a"), gr.Number(label="b", value=10)], + gr.Number(label="result"), + api_name="add", + ) + + with connect(demo) as client: + api_info = client.view_api(print_info=False, return_format="dict") + endpoint_info = api_info["named_endpoints"]["/add"] + snippets = generate_code_snippets("/add", endpoint_info, client.src) + + python_snippet = snippets["python"] + namespace = {} + exec(python_snippet, namespace) + assert isinstance(namespace["result"], (int, float)) + + +class TestStringifyPy: + def test_datetime_in_nested_structure(self): + """Non-JSON-native types like datetime should not raise TypeError.""" + value = { + "headers": ["t", "x"], + "data": [[datetime.datetime(2026, 1, 1, 0, 0), 1]], + } + result = _stringify_py(value) + assert "2026-01-01" in result + assert isinstance(result, str) + + def test_date_serialization(self): + value = [datetime.date(2026, 6, 15)] + result = _stringify_py(value) + assert "2026-06-15" in result diff --git a/client/python/test/test_utils.py b/client/python/test/test_utils.py new file mode 100644 index 0000000..5ffa067 --- /dev/null +++ b/client/python/test/test_utils.py @@ -0,0 +1,334 @@ +import importlib.resources +import json +import tempfile +from copy import deepcopy +from enum import Enum +from pathlib import Path +from typing import Any, Literal, Optional, Union +from unittest.mock import MagicMock, patch + +import httpx +import pytest +from huggingface_hub import get_token + +from gradio_client import utils + +types = json.loads(importlib.resources.read_text("gradio_client", "types.json")) +types["MultipleFile"] = { + "type": "array", + "items": {"type": "string", "description": "filepath or URL to file"}, +} +types["SingleFile"] = {"type": "string", "description": "filepath or URL to file"} +types["FileWithAdditionalProperties"] = {"type": "object", "additionalProperties": True} +HF_TOKEN = get_token() + + +class TestEnum(Enum): + VALUE1 = "option1" + VALUE2 = "option2" + VALUE3 = 42 + + +def test_encode_url_or_file_to_base64(media_data): + output_base64 = utils.encode_url_or_file_to_base64( + Path(__file__).parents[3] / "gradio" / "test_data" / "test_image.png" + ) + assert output_base64 == deepcopy(media_data.BASE64_IMAGE) + + +def test_encode_file_to_base64(media_data): + output_base64 = utils.encode_file_to_base64( + Path(__file__).parents[3] / "gradio" / "test_data" / "test_image.png" + ) + assert output_base64 == deepcopy(media_data.BASE64_IMAGE) + + +@pytest.mark.flaky +def test_encode_url_to_base64(media_data): + output_base64 = utils.encode_url_to_base64( + "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/test_data/test_image.png" + ) + assert output_base64 == deepcopy(media_data.BASE64_IMAGE) + + +def test_encode_url_to_base64_doesnt_encode_errors(monkeypatch): + request = httpx.Request("GET", "https://example.com/foo") + error_response = httpx.Response(status_code=404, request=request) + monkeypatch.setattr(httpx, "get", lambda *args, **kwargs: error_response) + with pytest.raises(httpx.HTTPStatusError): + utils.encode_url_to_base64("https://example.com/foo") + + +def test_decode_base64_to_binary(media_data): + binary = utils.decode_base64_to_binary(deepcopy(media_data.BASE64_IMAGE)) + assert deepcopy(media_data.BINARY_IMAGE) == binary + + b64_img_without_header = deepcopy(media_data.BASE64_IMAGE).split(",")[1] + binary_without_header, extension = utils.decode_base64_to_binary( + b64_img_without_header + ) + + assert binary[0] == binary_without_header + assert extension is None + + +def test_decode_base64_to_file(media_data): + temp_file = utils.decode_base64_to_file(deepcopy(media_data.BASE64_IMAGE)) + assert isinstance(temp_file, tempfile._TemporaryFileWrapper) + + +@pytest.mark.parametrize( + "path_or_url, file_types, expected_result", + [ + ("/home/user/documents/example.pdf", [".json", "text", ".mp3", ".pdf"], True), + ("C:\\Users\\user\\documents\\example.png", [".png"], True), + ("C:\\Users\\user\\documents\\example.png", ["image"], True), + ("C:\\Users\\user\\documents\\example.png", ["file"], True), + ("/home/user/documents/example.pdf", [".json", "text", ".mp3"], False), + ("https://example.com/avatar/xxxx.mp4", ["audio", ".png", ".jpg"], False), + # WebP support - case insensitive + ("/home/user/images/photo.webp", ["image"], True), + ("/home/user/images/photo.WEBP", ["image"], True), + ("/home/user/images/photo.WebP", ["image"], True), + ("C:\\Users\\user\\images\\photo.webp", ["image", "video"], True), + ("C:\\Users\\user\\images\\photo.WEBP", ["image", "video"], True), + ], +) +def test_is_valid_file_type(path_or_url, file_types, expected_result): + assert utils.is_valid_file(path_or_url, file_types) is expected_result + + +@pytest.mark.parametrize( + "filename, expected_mimetype", + [ + ("photo.webp", "image/webp"), + ("photo.WEBP", "image/webp"), + ("photo.WebP", "image/webp"), + ("video.vtt", "text/vtt"), + ("video.VTT", "text/vtt"), + ("image.png", "image/png"), + ], +) +def test_get_mimetype(filename, expected_mimetype): + assert utils.get_mimetype(filename) == expected_mimetype + + +@pytest.mark.parametrize( + "orig_filename, new_filename", + [ + ("abc", "abc"), + ("$$AAabc&3", "AAabc&3"), + ("$$AAa&..b-c3_", "AAa&..b-c3_"), + ("#.txt", "#.txt"), + ("###.pdf", "###.pdf"), + ("@!$.csv", "@.csv"), + ("a#.txt", "a#.txt"), + # Path traversal characters are stripped + ("a/b\\c.txt", "abc.txt"), + ('ac:"d.txt', "abcd.txt"), + ("a\x00b.txt", "ab.txt"), + # Shell-dangerous characters ($, !, {, }) are stripped; parentheses and brackets preserved + ("[{(Hunting's Shadowsl!)}].epub", "[(Hunting's Shadowsl)].epub"), + ("l!)}]test[{(.txt", "l)]test[(.txt"), + ( + "ゆかりです。私、こんなかわいい服は初めて着ました…。なんだかうれしくって、楽しいです。歌いたくなる気分って、初めてです。これがアイドルってことなのかもしれませんね", + "ゆかりです。私、こんなかわいい服は初めて着ました…。なんだかうれしくって、楽しいです。歌いたくなる気分って、初めてです。これがアイト", + ), + ( + "Bringing-computational-thinking-into-classrooms-a-systematic-review-on-supporting-teachers-in-integrating-computational-thinking-into-K12-classrooms_2024_Springer-Science-and-Business-Media-Deutschland-GmbH.pdf", + "Bringing-computational-thinking-into-classrooms-a-systematic-review-on-supporting-teachers-in-integrating-computational-thinking-into-K12-classrooms_2024_Springer-Science-and-Business-Media-Deutsc.pdf", + ), + ], +) +def test_strip_invalid_filename_characters(orig_filename, new_filename): + assert utils.strip_invalid_filename_characters(orig_filename) == new_filename + + +class AsyncMock(MagicMock): + async def __call__(self, *args, **kwargs): + return super().__call__(*args, **kwargs) + + +@patch("httpx.post") +def test_sleep_successful(mock_post): + utils.set_space_timeout("gradio/calculator") + + +@patch( + "httpx.post", + side_effect=httpx.HTTPStatusError("error", request=None, response=None), +) +def test_sleep_unsuccessful(mock_post): + with pytest.raises(utils.SpaceDuplicationError): + utils.set_space_timeout("gradio/calculator") + + +@pytest.mark.parametrize("schema", types) +def test_json_schema_to_python_type(schema): + if schema == "SimpleSerializable": + answer = "Any" + elif schema == "StringSerializable": + answer = "str" + elif schema == "ListStringSerializable": + answer = "list[str]" + elif schema == "BooleanSerializable": + answer = "bool" + elif schema == "NumberSerializable": + answer = "float" + elif schema == "ImgSerializable": + answer = "str" + elif schema == "FileSerializable": + answer = "str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file)) | list[str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))]" + elif schema == "JSONSerializable": + answer = "str | float | bool | list | dict" + elif schema == "GallerySerializable": + answer = "tuple[dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file)), str | None]" + elif schema == "SingleFileSerializable": + answer = "str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))" + elif schema == "MultipleFileSerializable": + answer = "list[str | dict(name: str (name of file), data: str (base64 representation of file), size: int (size of image in bytes), is_file: bool (true if the file has been uploaded to the server), orig_name: str (original name of the file))]" + elif schema == "SingleFile": + answer = "str" + elif schema == "MultipleFile": + answer = "list[str]" + elif schema == "FileWithAdditionalProperties": + answer = "dict(str, Any)" + else: + raise ValueError(f"This test has not been modified to check {schema}") + assert utils.json_schema_to_python_type(types[schema]) == answer + + +@pytest.mark.parametrize( + "type_hint, expected_schema", + [ + (str, {"type": "string"}), + (int, {"type": "integer"}), + (float, {"type": "number"}), + (bool, {"type": "boolean"}), + (type(None), {"type": "null"}), + (Any, {}), + (Union[str, int], {"anyOf": [{"type": "string"}, {"type": "integer"}]}), + (Optional[str], {"oneOf": [{"type": "null"}, {"type": "string"}]}), + (str | None, {"oneOf": [{"type": "null"}, {"type": "string"}]}), + (dict, {"type": "object", "additionalProperties": {}}), + (list, {"type": "array", "items": {}}), + (tuple, {"type": "array"}), + (set, {"type": "array", "uniqueItems": True}), + (frozenset, {"type": "array", "uniqueItems": True}), + (bytes, {"type": "string", "format": "byte"}), + (bytearray, {"type": "string", "format": "byte"}), + (TestEnum, {"enum": ["option1", "option2", 42]}), + ], +) +def test_python_type_to_json_schema(type_hint, expected_schema): + schema = utils.python_type_to_json_schema(type_hint) + assert schema == expected_schema + + +@pytest.mark.parametrize( + "type_hint, expected_schema", + [ + (tuple[int, ...], {"type": "array", "items": {"type": "integer"}}), + ( + tuple[str, int], + { + "type": "array", + "prefixItems": [{"type": "string"}, {"type": "integer"}], + "minItems": 2, + "maxItems": 2, + }, + ), + (set[str], {"type": "array", "uniqueItems": True, "items": {"type": "string"}}), + ( + list[str | None], + { + "type": "array", + "items": {"oneOf": [{"type": "null"}, {"type": "string"}]}, + }, + ), + (Literal["a", "b", "c"], {"enum": ["a", "b", "c"]}), + (Literal["single"], {"const": "single"}), + ], +) +def test_python_type_to_json_schema_complex_nested_types(type_hint, expected_schema): + assert utils.python_type_to_json_schema(type_hint) == expected_schema + + +class TestConstructArgs: + def test_no_parameters_empty_args(self): + assert utils.construct_args(None, (), {}) == [] + + def test_no_parameters_with_args(self): + assert utils.construct_args(None, (1, 2), {}) == [1, 2] + + def test_no_parameters_with_kwargs(self): + with pytest.raises( + ValueError, match="This endpoint does not support key-word arguments" + ): + utils.construct_args(None, (), {"a": 1}) + + def test_parameters_no_args_kwargs(self): + parameters_info = [ + { + "label": "param1", + "parameter_name": "a", + "parameter_has_default": True, + "parameter_default": 10, + } + ] + assert utils.construct_args(parameters_info, (), {"a": 1}) == [1] + + def test_parameters_with_args_no_kwargs(self): + parameters_info = [{"label": "param1", "parameter_name": "a"}] + assert utils.construct_args(parameters_info, (1,), {}) == [1] + + def test_parameter_with_default_no_args_no_kwargs(self): + parameters_info = [ + {"label": "param1", "parameter_has_default": True, "parameter_default": 10} + ] + assert utils.construct_args(parameters_info, (), {}) == [10] + + def test_args_filled_parameters_with_defaults(self): + parameters_info = [ + {"label": "param1", "parameter_has_default": True, "parameter_default": 10}, + {"label": "param2", "parameter_has_default": True, "parameter_default": 20}, + ] + assert utils.construct_args(parameters_info, (1,), {}) == [1, 20] + + def test_kwargs_filled_parameters_with_defaults(self): + parameters_info = [ + { + "label": "param1", + "parameter_name": "a", + "parameter_has_default": True, + "parameter_default": 10, + }, + { + "label": "param2", + "parameter_name": "b", + "parameter_has_default": True, + "parameter_default": 20, + }, + ] + assert utils.construct_args(parameters_info, (), {"a": 1, "b": 2}) == [1, 2] + + def test_positional_arg_and_kwarg_for_same_parameter(self): + parameters_info = [{"label": "param1", "parameter_name": "a"}] + with pytest.raises( + TypeError, match="Parameter `a` is already set as a positional argument." + ): + utils.construct_args(parameters_info, (1,), {"a": 2}) + + def test_invalid_kwarg(self): + parameters_info = [{"label": "param1", "parameter_name": "a"}] + with pytest.raises( + TypeError, match="Parameter `b` is not a valid key-word argument." + ): + utils.construct_args(parameters_info, (), {"b": 1}) + + def test_required_arg_missing(self): + parameters_info = [{"label": "param1", "parameter_name": "a"}] + with pytest.raises( + TypeError, match="No value provided for required argument: a" + ): + utils.construct_args(parameters_info, (), {}) diff --git a/cs.js b/cs.js new file mode 100644 index 0000000..e496318 --- /dev/null +++ b/cs.js @@ -0,0 +1,265 @@ +import { readFileSync, writeFileSync, readdirSync } from "fs"; +import path from "path"; +import { execSync } from "child_process"; +import { getPackages } from "@manypkg/get-packages"; + +/** + * @typedef {"patch" | "minor" | "major"} VersionBump + * @typedef {{ feat: string; fix: string; highlight: string; [key: string]: string }} ReadmeContent + * @typedef {{ version: VersionBump; reamde_content: ReadmeContent }} ChangedPackage + * @typedef {{ name: string; dir: string; version: string; packge_json: Record; changelog: string }} PackageToWrite + */ + +const changsetsFolder = path.join(process.cwd(), ".changeset"); +const files = readdirSync(changsetsFolder); +const mdFiles = files.filter( + (file) => file.endsWith(".md") && !file.startsWith("README.md") +); + +/** @param {string} filePath */ +const getGitInfo = (filePath) => { + const gitInfo = execSync( + `git log -n 1 --pretty=format:"%H -bingboong- %s" -- ${filePath}`, + { + encoding: "utf8" + } + ); + return gitInfo; +}; + +const changsets = mdFiles + .map((file) => { + const filePath = path.join(changsetsFolder, file); + const fileContent = readFileSync(filePath, "utf8"); + const [sha, message] = getGitInfo( + path.join(process.cwd(), ".changeset", file) + ).split(" -bingboong- "); + return { + file, + sha, + short_sha: sha.slice(0, 7), + pr: message.match(/(#\d+)/)?.[1], + content: fileContent + }; + }) + .filter((c) => c.pr); + +const changedPackages = changsets.reduce( + (acc, { content, sha, short_sha, pr }) => { + const [, frontmatter, body] = content.split("---"); + const type_index = body.indexOf(":"); + const type = body.slice(0, type_index).trim(); + const _content = body.slice(type_index + 1).trim(); + if (frontmatter) { + const packages = frontmatter.split("\n").filter((line) => !!line.trim()); + + if (packages) { + packages.forEach((_package) => { + const [name, version] = _package.split(":"); + if (!name || !version || !pr) return; + + if (!acc[name]) { + acc[name] = { + version: /** @type {VersionBump} */ (version.trim()), + reamde_content: { + feat: "", + fix: "", + highlight: "", + [type]: format_readme_content(pr, short_sha, sha, _content) + } + }; + } else { + acc[name].version = getMaximumBump( + /** @type {VersionBump} */ (version.trim()), + acc[name].version + ); + acc[name].reamde_content[type] += `\n${format_readme_content( + pr, + short_sha, + sha, + _content + )}`; + } + }); + } + } + return acc; + }, + /** @type {Record} */ ({}) +); + +/** + * @param {VersionBump} newVersion + * @param {VersionBump} oldVersion + * @returns {VersionBump} + */ +function getMaximumBump(newVersion, oldVersion) { + const versionOrder = ["patch", "minor", "major"]; + const newVersionIndex = versionOrder.indexOf(newVersion); + const oldVersionIndex = versionOrder.indexOf(oldVersion); + + return /** @type {VersionBump} */ ( + versionOrder[Math.max(newVersionIndex, oldVersionIndex)] + ); +} + +/** + * @param {string} pr + * @param {string} short_sha + * @param {string} sha + * @param {string} _content + */ +function format_readme_content(pr, short_sha, sha, _content) { + return `- [${pr}](https://github.com/gradio-app/gradio/pull/${pr.replace( + "#", + "" + )}) [\`${short_sha}\`](https://github.com/gradio-app/gradio/commit/${sha}) - ${_content.trim()}`; +} + +const { packages } = await getPackages(process.cwd()); + +/** @type {PackageToWrite[]} */ +const packages_to_write = []; + +for (const pkg of packages) { + if (`"${pkg.packageJson.name}"` in changedPackages) { + const current_version = pkg.packageJson.version; + const bump = changedPackages[`"${pkg.packageJson.name}"`].version; + + const new_version = get_new_version(current_version, bump); + const new_package_json = { + ...pkg.packageJson, + version: new_version + }; + + /** @type {[string, string][]} */ + let deps_updated = []; + for (const dep in new_package_json.dependencies) { + if (`"${dep}"` in changedPackages) { + console.log( + dep, + changedPackages[`"${dep}"`].version, + new_package_json.dependencies[dep] + ); + + const dep_version = packages.find((p) => p.packageJson.name === dep) + ?.packageJson.version; + + deps_updated.push([ + dep, + get_new_version( + dep_version ?? "0.0.0", + changedPackages[`"${dep}"`].version + ) + ]); + } + } + + if ( + deps_updated.length > 0 && + !(`"${pkg.packageJson.name}"` in changedPackages) + ) { + new_package_json.version = get_new_version(current_version, "patch"); + packages_to_write.push({ + name: pkg.packageJson.name, + dir: pkg.dir, + version: new_version, + packge_json: new_package_json, + changelog: readFileSync(`${pkg.dir}/CHANGELOG.md`, "utf8").replace( + `# ${pkg.packageJson.name}`, + + make_changelog( + pkg.packageJson.name, + new_version, + changedPackages[`"${pkg.packageJson.name}"`].reamde_content, + deps_updated + ) + ) + }); + } else if (`"${pkg.packageJson.name}"` in changedPackages) { + new_package_json.version = new_version; + packages_to_write.push({ + name: pkg.packageJson.name, + dir: pkg.dir, + version: new_version, + packge_json: new_package_json, + changelog: readFileSync(`${pkg.dir}/CHANGELOG.md`, "utf8").replace( + `# ${pkg.packageJson.name}`, + + make_changelog( + pkg.packageJson.name, + new_version, + changedPackages[`"${pkg.packageJson.name}"`].reamde_content, + deps_updated + ) + ) + }); + } + } +} + +/** + * @param {string} name + * @param {string} version + * @param {ReadmeContent} changes + * @param {[string, string][]} deps_updated + */ +function make_changelog(name, version, changes, deps_updated) { + const { feat, fix, highlight } = changes; + let changelog = `# ${name} +## ${version}`; + + if (highlight) { + changelog += `\n\n### Highlights\n\n${highlight}`; + } + if (feat) { + changelog += `\n\n### Features\n\n${feat}`; + } + if (fix) { + changelog += `\n\n### Fixes\n\n${fix}`; + } + if (deps_updated.length > 0) { + changelog += `\n\n### Dependencies\n\n${deps_updated + .map(([dep, version]) => `- ${dep}@${version}`) + .join("\n")}`; + } + + return changelog; +} + +/** + * @param {string} version + * @param {VersionBump | string} bump + */ +function get_new_version(version, bump) { + const [_major, _minor, _patch, prerelease] = version.split("."); + + const major = parseInt(_major); + const minor = parseInt(_minor); + const patch = parseInt(_patch); + + if (prerelease) { + return `${major}.${minor}.${patch}`; + } + + switch (bump) { + case "major": + return `${major + 1}.0.0`; + case "minor": + return `${major}.${minor + 1}.0`; + case "patch": + return `${major}.${minor}.${patch + 1}`; + default: + return version; + } +} + +console.log(packages_to_write); + +for (const pkg of packages_to_write) { + writeFileSync( + `${pkg.dir}/package.json`, + JSON.stringify(pkg.packge_json, null, "\t") + ); + writeFileSync(`${pkg.dir}/CHANGELOG.md`, pkg.changelog); +} diff --git a/demo/__init__.py b/demo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/accordion_tab_switch/run.py b/demo/accordion_tab_switch/run.py new file mode 100644 index 0000000..fc1dde3 --- /dev/null +++ b/demo/accordion_tab_switch/run.py @@ -0,0 +1,27 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Tabs() as tabs: + with gr.Tab("Tab 1", id="t1"): + with gr.Accordion("Accordion", open=False) as acc: + name = gr.Textbox(label="Name") + + accordion_open = gr.Checkbox(label="Accordion Open", value=False) + + accordion_open.change( + fn=lambda is_open: gr.update(open=is_open), + inputs=accordion_open, + outputs=acc, + ) + with gr.Tab("Tab 2", id="t2"): + gr.Markdown("This is Tab 2 content.") + + swith_tabs_btn = gr.Button("Switch to Tab 2") + swith_tabs_btn.click( + fn=lambda: gr.Tabs(selected="t2"), + inputs=None, + outputs=tabs, + ) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/agent_chatbot/requirements.txt b/demo/agent_chatbot/requirements.txt new file mode 100644 index 0000000..9ed70ee --- /dev/null +++ b/demo/agent_chatbot/requirements.txt @@ -0,0 +1 @@ +transformers>=4.47.0 \ No newline at end of file diff --git a/demo/agent_chatbot/run.py b/demo/agent_chatbot/run.py new file mode 100644 index 0000000..7e6d5a4 --- /dev/null +++ b/demo/agent_chatbot/run.py @@ -0,0 +1,44 @@ +import gradio as gr +from dataclasses import asdict +from transformers import Tool, ReactCodeAgent # type: ignore +from transformers.agents import stream_to_gradio, HfApiEngine # type: ignore + +# Import tool from Hub +image_generation_tool = Tool.from_space( # type: ignore + space_id="black-forest-labs/FLUX.1-schnell", + name="image_generator", + description="Generates an image following your prompt. Returns a PIL Image.", + api_name="/infer", +) + +llm_engine = HfApiEngine("Qwen/Qwen2.5-Coder-32B-Instruct") +# Initialize the agent with both tools and engine +agent = ReactCodeAgent(tools=[image_generation_tool], llm_engine=llm_engine) + + +def interact_with_agent(prompt, history): + messages = [] + yield messages + for msg in stream_to_gradio(agent, prompt): + messages.append(asdict(msg)) # type: ignore + yield messages + yield messages + + +demo = gr.ChatInterface( + interact_with_agent, + chatbot= gr.Chatbot( + label="Agent", + avatar_images=( + None, + "https://em-content.zobj.net/source/twitter/53/robot-face_1f916.png", + ), + ), + examples=[ + ["Generate an image of an astronaut riding an alligator"], + ["I am writing a children's book for my daughter. Can you help me with some illustrations?"], + ], +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/all_demos/image.png b/demo/all_demos/image.png new file mode 100644 index 0000000..dc392ea Binary files /dev/null and b/demo/all_demos/image.png differ diff --git a/demo/all_demos/run.py b/demo/all_demos/run.py new file mode 100644 index 0000000..05f0c90 --- /dev/null +++ b/demo/all_demos/run.py @@ -0,0 +1,46 @@ +import importlib +import gradio as gr +import os +import sys +import copy +import pathlib +from gradio.media import MEDIA_ROOT + +os.environ["GRADIO_ANALYTICS_ENABLED"] = "False" + +demo_dir = pathlib.Path(__file__).parent / "demos" + +names = sorted(os.listdir("./demos")) + +all_demos = [] +demo_module = None +for p in sorted(os.listdir("./demos")): + old_path = copy.deepcopy(sys.path) + sys.path = [os.path.join(demo_dir, p)] + sys.path + try: # Some demos may not be runnable because of 429 timeouts, etc. + if demo_module is None: + demo_module = importlib.import_module("run") + else: + demo_module = importlib.reload(demo_module) + all_demos.append((p, demo_module.demo, False)) # type: ignore + except Exception as e: + with gr.Blocks() as demo: + gr.Markdown(f"Error loading demo: {e}") + all_demos.append((p, demo, True)) + +app = gr.Blocks() + +with app: + gr.Markdown(""" +# Deployed Demos +## Click through demos to test them out! +""") + +for demo_name, demo, _ in all_demos: + with app.route(demo_name): + demo.render() + + # app = gr.mount_gradio_app(app, demo, f"/demo/{demo_name}") + +if __name__ == "__main__": + app.launch(allowed_paths=[str(MEDIA_ROOT)]) diff --git a/demo/animeganv2/DESCRIPTION.md b/demo/animeganv2/DESCRIPTION.md new file mode 100644 index 0000000..e66cbc0 --- /dev/null +++ b/demo/animeganv2/DESCRIPTION.md @@ -0,0 +1 @@ +Recreate the viral AnimeGAN image transformation demo. \ No newline at end of file diff --git a/demo/animeganv2/gongyoo.jpeg b/demo/animeganv2/gongyoo.jpeg new file mode 100644 index 0000000..8f09a41 Binary files /dev/null and b/demo/animeganv2/gongyoo.jpeg differ diff --git a/demo/animeganv2/requirements.txt b/demo/animeganv2/requirements.txt new file mode 100644 index 0000000..45d786f --- /dev/null +++ b/demo/animeganv2/requirements.txt @@ -0,0 +1,9 @@ +torch +torchvision +Pillow +gdown +numpy +scipy +cmake +onnxruntime-gpu +opencv-python-headless \ No newline at end of file diff --git a/demo/animeganv2/run.py b/demo/animeganv2/run.py new file mode 100644 index 0000000..2e22f6b --- /dev/null +++ b/demo/animeganv2/run.py @@ -0,0 +1,38 @@ +import gradio as gr +import torch + +model2 = torch.hub.load( + "AK391/animegan2-pytorch:main", + "generator", + pretrained=True, + progress=False +) +model1 = torch.hub.load("AK391/animegan2-pytorch:main", "generator", pretrained="face_paint_512_v1") +face2paint = torch.hub.load( + 'AK391/animegan2-pytorch:main', 'face2paint', + size=512,side_by_side=False +) + +def inference(img, ver): + if ver == 'version 2 (🔺 robustness,🔻 stylization)': + out = face2paint(model2, img) + else: + out = face2paint(model1, img) + return out + +title = "AnimeGANv2" +description = "Gradio Demo for AnimeGanv2 Face Portrait. To use it, simply upload your image, or click one of the examples to load them. Read more at the links below. Please use a cropped portrait picture for best results similar to the examples below." +article = "

Github Repo Pytorch

visitor badge

" +examples=[['groot.jpeg','version 2 (🔺 robustness,🔻 stylization)'],['gongyoo.jpeg','version 1 (🔺 stylization, 🔻 robustness)']] + +demo = gr.Interface( + fn=inference, + inputs=[gr.Image(type="pil"),gr.Radio(['version 1 (🔺 stylization, 🔻 robustness)','version 2 (🔺 robustness,🔻 stylization)'], type="value", value='version 2 (🔺 robustness,🔻 stylization)', label='version')], + outputs=gr.Image(type="pil"), + title=title, + description=description, + article=article, + examples=examples, + api_name="predict") + +demo.launch() diff --git a/demo/annotatedimage_component/requirements.txt b/demo/annotatedimage_component/requirements.txt new file mode 100644 index 0000000..6dd520b --- /dev/null +++ b/demo/annotatedimage_component/requirements.txt @@ -0,0 +1,3 @@ +numpy +requests +Pillow diff --git a/demo/annotatedimage_component/run.py b/demo/annotatedimage_component/run.py new file mode 100644 index 0000000..ad3a878 --- /dev/null +++ b/demo/annotatedimage_component/run.py @@ -0,0 +1,17 @@ +import gradio as gr +import numpy as np +import requests +from io import BytesIO +from PIL import Image + +base_image = "https://gradio-docs-json.s3.us-west-2.amazonaws.com/base.png" +building_image = requests.get("https://gradio-docs-json.s3.us-west-2.amazonaws.com/buildings.png") +building_image = np.asarray(Image.open(BytesIO(building_image.content)))[:, :, -1] > 0 + +with gr.Blocks() as demo: + gr.AnnotatedImage( + value=(base_image, [(building_image, "buildings")]), + height=500, + ) + +demo.launch() \ No newline at end of file diff --git a/demo/asr/requirements.txt b/demo/asr/requirements.txt new file mode 100644 index 0000000..0175df5 --- /dev/null +++ b/demo/asr/requirements.txt @@ -0,0 +1,3 @@ +torch +torchaudio +transformers \ No newline at end of file diff --git a/demo/asr/run.py b/demo/asr/run.py new file mode 100644 index 0000000..aacba46 --- /dev/null +++ b/demo/asr/run.py @@ -0,0 +1,27 @@ +import gradio as gr +from transformers import pipeline +import numpy as np + +transcriber = pipeline("automatic-speech-recognition", model="openai/whisper-base.en") + +def transcribe(audio): + sr, y = audio + + # Convert to mono if stereo + if y.ndim > 1: + y = y.mean(axis=1) + + y = y.astype(np.float32) + y /= np.max(np.abs(y)) + + return transcriber({"sampling_rate": sr, "raw": y})["text"] # type: ignore + +demo = gr.Interface( + transcribe, + gr.Audio(sources="microphone"), + "text", + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/audio_component/run.py b/demo/audio_component/run.py new file mode 100644 index 0000000..17508a7 --- /dev/null +++ b/demo/audio_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Audio() + +demo.launch() diff --git a/demo/audio_component_events/run.py b/demo/audio_component_events/run.py new file mode 100644 index 0000000..c8a69c3 --- /dev/null +++ b/demo/audio_component_events/run.py @@ -0,0 +1,43 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + input_audio = gr.Audio(type="filepath", label="Input Audio", sources=["upload", "microphone"]) + with gr.Column(): + output_audio = gr.Audio(label="Output Audio", sources=["upload", "microphone"]) + + with gr.Row(): + with gr.Column(): + input_num_change = gr.Number(label="# Input Change Events", value=0) + input_num_input = gr.Number(label="# Input Input Events", value=0) + input_num_load = gr.Number(label="# Input Upload Events", value=0) + input_num_play = gr.Number(label="# Input Play Events", value=0) + input_num_pause = gr.Number(label="# Input Pause Events", value=0) + + with gr.Column(): + input_record = gr.Number(label="# Input Start Recording Events", value=0) + input_pause = gr.Number(label="# Input Pause Recording Events", value=0) + input_stop = gr.Number(label="# Input Stop Recording Events", value=0) + + with gr.Column(): + output_num_play = gr.Number(label="# Output Play Events", value=0) + output_num_pause = gr.Number(label="# Output Pause Events", value=0) + output_num_stop = gr.Number(label="# Output Stop Events", value=0) + + input_audio.upload(lambda s, n: (s, n + 1), [input_audio, input_num_load], [output_audio, input_num_load]) + input_audio.change(lambda n: n + 1, input_num_change, input_num_change) + input_audio.play(lambda n: n + 1, input_num_play, input_num_play) + input_audio.pause(lambda n: n + 1, input_num_pause, input_num_pause) + input_audio.input(lambda n: n + 1, input_num_input, input_num_input) + + input_audio.start_recording(lambda n: n + 1, input_record, input_record) + input_audio.pause_recording(lambda n: n + 1, input_pause, input_pause) + input_audio.stop_recording(lambda n: n + 1, input_stop, input_stop) + + output_audio.play(lambda n: n + 1, output_num_play, output_num_play) + output_audio.pause(lambda n: n + 1, output_num_pause, output_num_pause) + output_audio.stop(lambda n: n + 1, output_num_stop, output_num_stop) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/audio_debugger/run.py b/demo/audio_debugger/run.py new file mode 100644 index 0000000..e515d50 --- /dev/null +++ b/demo/audio_debugger/run.py @@ -0,0 +1,43 @@ +import gradio as gr +from gradio.media import get_audio, MEDIA_PATHS + +# get_audio returns the path to the audio file +audio_file = get_audio("cantina.wav") + +with gr.Blocks() as demo: + with gr.Tab("Audio"): + gr.Audio(audio_file, buttons=["download"]) + with gr.Tab("Interface"): + gr.Interface( + lambda x: x, + gr.Audio(), + gr.Audio(), + examples=[audio_file], + cache_examples=True, + api_name="predict", + ) + with gr.Tab("Streaming"): + gr.Interface( + lambda x: x, + gr.Audio(streaming=True), + "audio", + examples=[audio_file], + cache_examples=True, + api_name="predict", + ) + with gr.Tab("console"): + ip = gr.Textbox(label="User IP Address") + gr.Interface( + lambda cmd: f"You typed this command: {cmd}", + "text", + "text", + api_name="predict", + ) + + def get_ip(request: gr.Request): + return request.client.host + + demo.load(get_ip, None, ip) + +if __name__ == "__main__": + demo.launch(allowed_paths=MEDIA_PATHS) diff --git a/demo/audio_mixer/run.py b/demo/audio_mixer/run.py new file mode 100644 index 0000000..5a02a08 --- /dev/null +++ b/demo/audio_mixer/run.py @@ -0,0 +1,43 @@ +import gradio as gr + +with gr.Blocks() as demo: + track_count = gr.State(1) + add_track_btn = gr.Button("Add Track") + + add_track_btn.click(lambda count: count + 1, track_count, track_count) + + @gr.render(inputs=track_count) + def render_tracks(count): + audios = [] + volumes = [] + with gr.Row(): + for i in range(count): + with gr.Column(variant="panel", min_width=200): + gr.Textbox(placeholder="Track Name", key=f"name-{i}", show_label=False) + track_audio = gr.Audio(label=f"Track {i}", key=f"track-{i}") + track_volume = gr.Slider(0, 100, value=100, label="Volume", key=f"volume-{i}") + audios.append(track_audio) + volumes.append(track_volume) + + def merge(data): + sr, output = None, None + for audio, volume in zip(audios, volumes): + sr, audio_val = data[audio] + volume_val = data[volume] + final_track = audio_val * (volume_val / 100) + if output is None: + output = final_track + else: + min_shape = tuple(min(s1, s2) for s1, s2 in zip(output.shape, final_track.shape)) + trimmed_output = output[:min_shape[0], ...][:, :min_shape[1], ...] if output.ndim > 1 else output[:min_shape[0]] + trimmed_final = final_track[:min_shape[0], ...][:, :min_shape[1], ...] if final_track.ndim > 1 else final_track[:min_shape[0]] + output += trimmed_output + trimmed_final + return (sr, output) + + merge_btn.click(merge, set(audios + volumes), output_audio) + + merge_btn = gr.Button("Merge Tracks") + output_audio = gr.Audio(label="Output", interactive=False) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/audio_subtitle/run.py b/demo/audio_subtitle/run.py new file mode 100644 index 0000000..bfd477b --- /dev/null +++ b/demo/audio_subtitle/run.py @@ -0,0 +1,33 @@ +import gradio as gr +from gradio.media import get_audio, get_file, MEDIA_PATHS + + +a = get_audio("cate_blanch.mp3") +b = get_audio("cate_blanch_2.mp3") +s1 = get_file("s1.srt") +s2 = get_file("s2.vtt") + + +def add_subtitles_to_audio(audio, subtitles=None): + if subtitles is None: + return audio + if subtitles is not None: + return gr.Audio(label="Out", value=audio, subtitles=subtitles.name) + + +demo = gr.Interface( + fn=add_subtitles_to_audio, + inputs=[ + gr.Audio(label="In", interactive=True), + gr.File(label="Subtitle", file_types=[".srt", ".vtt"]), + ], + outputs=gr.Audio(label="Out"), + examples=[ + [a, s1], + [b, s2], + ], + api_name="predict", +) + +if __name__ == "__main__": + demo.launch(allowed_paths=[MEDIA_PATHS]) # type: ignore diff --git a/demo/autocomplete/DESCRIPTION.md b/demo/autocomplete/DESCRIPTION.md new file mode 100644 index 0000000..1ba2417 --- /dev/null +++ b/demo/autocomplete/DESCRIPTION.md @@ -0,0 +1 @@ +This text generation demo works like autocomplete. There's only one textbox and it's used for both the input and the output. The demo loads the model as an interface, and uses that interface as an API. It then uses blocks to create the UI. All of this is done in less than 10 lines of code. \ No newline at end of file diff --git a/demo/autocomplete/run.py b/demo/autocomplete/run.py new file mode 100644 index 0000000..0e450fd --- /dev/null +++ b/demo/autocomplete/run.py @@ -0,0 +1,21 @@ +import gradio as gr +import os + +# save your HF API token from https:/hf.co/settings/tokens as an env variable to avoid rate limiting +hf_token = os.getenv("hf_token") + +# load a model from https://hf.co/models as an interface, then use it as an api +# you can remove the hf_token parameter if you don't care about rate limiting. +api = gr.load("huggingface/gpt2-xl", hf_token=hf_token) + +def complete_with_gpt(text): + return text[:-50] + api(text[-50:]) + +with gr.Blocks() as demo: + textbox = gr.Textbox(placeholder="Type here...", lines=4) + btn = gr.Button("Autocomplete") + + # define what will run when the button is clicked, here the textbox is used as both an input and an output + btn.click(fn=complete_with_gpt, inputs=textbox, outputs=textbox, queue=False) + +demo.launch() diff --git a/demo/automatic-speech-recognition/DESCRIPTION.md b/demo/automatic-speech-recognition/DESCRIPTION.md new file mode 100644 index 0000000..876b9b1 --- /dev/null +++ b/demo/automatic-speech-recognition/DESCRIPTION.md @@ -0,0 +1 @@ +Automatic speech recognition English. Record from your microphone and the app will transcribe the audio. \ No newline at end of file diff --git a/demo/automatic-speech-recognition/run.py b/demo/automatic-speech-recognition/run.py new file mode 100644 index 0000000..f66fe55 --- /dev/null +++ b/demo/automatic-speech-recognition/run.py @@ -0,0 +1,17 @@ +import gradio as gr +import os + +# save your HF API token from https:/hf.co/settings/tokens as an env variable to avoid rate limiting +hf_token = os.getenv("hf_token") + +# automatically load the interface from a HF model +# you can remove the hf_token parameter if you don't care about rate limiting. +demo = gr.load( + "huggingface/facebook/wav2vec2-base-960h", + title="Speech-to-text", + inputs="mic", + description="Let me try to guess what you're saying!", + hf_token=hf_token +) + +demo.launch() diff --git a/demo/bar_plot/requirements.txt b/demo/bar_plot/requirements.txt new file mode 100644 index 0000000..1411a4a --- /dev/null +++ b/demo/bar_plot/requirements.txt @@ -0,0 +1 @@ +pandas \ No newline at end of file diff --git a/demo/bar_plot/run.py b/demo/bar_plot/run.py new file mode 100644 index 0000000..5ed4f0a --- /dev/null +++ b/demo/bar_plot/run.py @@ -0,0 +1,131 @@ +import gradio as gr +import pandas as pd +import random + +simple = pd.DataFrame( + { + "a": ["A", "B", "C", "D", "E", "F", "G", "H", "I"], + "b": [28, 55, 43, 91, 81, 53, 19, 87, 52], + } +) + +fake_barley = pd.DataFrame( + { + "site": [ + random.choice( + [ + "University Farm", + "Waseca", + "Morris", + "Crookston", + "Grand Rapids", + "Duluth", + ] + ) + for _ in range(120) + ], + "yield": [random.randint(25, 75) for _ in range(120)], + "variety": [ + random.choice( + [ + "Manchuria", + "Wisconsin No. 38", + "Glabron", + "No. 457", + "No. 462", + "No. 475", + ] + ) + for _ in range(120) + ], + "year": [ + random.choice( + [ + "1931", + "1932", + ] + ) + for _ in range(120) + ], + } +) + +def bar_plot_fn(display): + if display == "simple": + return gr.BarPlot( + simple, + x="a", + y="b", + title="Simple Bar Plot with made up data", + tooltip=["a", "b"], + y_lim=[20, 100], + ) + elif display == "stacked": + return gr.BarPlot( + fake_barley, + x="variety", + y="yield", + color="site", + title="Barley Yield Data", + tooltip=["variety", "site"], + ) + elif display == "grouped": + return gr.BarPlot( + fake_barley.astype({"year": str}), + x="year", + y="yield", + color="year", + title="Barley Yield by Year and Site", + tooltip=["yield", "site", "year"], + ) + elif display == "simple-horizontal": + return gr.BarPlot( + simple, + x="a", + y="b", + x_title="Variable A", + y_title="Variable B", + title="Simple Bar Plot with made up data", + tooltip=["a", "b"], + y_lim=[20, 100], + ) + elif display == "stacked-horizontal": + return gr.BarPlot( + fake_barley, + x="variety", + y="yield", + color="site", + title="Barley Yield Data", + tooltip=["variety", "site"], + ) + elif display == "grouped-horizontal": + return gr.BarPlot( + fake_barley.astype({"year": str}), + x="year", + y="yield", + color="year", + title="Barley Yield by Year and Site", + tooltip=["yield", "site", "year"], + ) + +with gr.Blocks() as bar_plot: + with gr.Row(): + with gr.Column(): + display = gr.Dropdown( + choices=[ + "simple", + "stacked", + "grouped", + "simple-horizontal", + "stacked-horizontal", + "grouped-horizontal", + ], + value="simple", + label="Type of Bar Plot", + ) + with gr.Column(): + plot = gr.BarPlot() + display.change(bar_plot_fn, inputs=display, outputs=plot) + bar_plot.load(fn=bar_plot_fn, inputs=display, outputs=plot) + +bar_plot.launch() diff --git a/demo/bar_plot_demo/requirements.txt b/demo/bar_plot_demo/requirements.txt new file mode 100644 index 0000000..fb6c7ed --- /dev/null +++ b/demo/bar_plot_demo/requirements.txt @@ -0,0 +1 @@ +pandas diff --git a/demo/bar_plot_demo/run.py b/demo/bar_plot_demo/run.py new file mode 100644 index 0000000..ed25096 --- /dev/null +++ b/demo/bar_plot_demo/run.py @@ -0,0 +1,94 @@ +import pandas as pd +from random import randint, random +import gradio as gr + + +temp_sensor_data = pd.DataFrame( + { + "time": pd.date_range("2021-01-01", end="2021-01-05", periods=200), + "temperature": [randint(50 + 10 * (i % 2), 65 + 15 * (i % 2)) for i in range(200)], + "humidity": [randint(50 + 10 * (i % 2), 65 + 15 * (i % 2)) for i in range(200)], + "location": ["indoor", "outdoor"] * 100, + } +) + +food_rating_data = pd.DataFrame( + { + "cuisine": [["Italian", "Mexican", "Chinese"][i % 3] for i in range(100)], + "rating": [random() * 4 + 0.5 * (i % 3) for i in range(100)], + "price": [randint(10, 50) + 4 * (i % 3) for i in range(100)], + "wait": [random() for i in range(100)], + } +) + +with gr.Blocks() as bar_plots: + with gr.Row(): + start = gr.DateTime("2021-01-01 00:00:00", label="Start") + end = gr.DateTime("2021-01-05 00:00:00", label="End") + apply_btn = gr.Button("Apply", scale=0) + with gr.Row(): + group_by = gr.Radio(["None", "30m", "1h", "4h", "1d"], value="None", label="Group by") + aggregate = gr.Radio(["sum", "mean", "median", "min", "max"], value="sum", label="Aggregation") + + temp_by_time = gr.BarPlot( + temp_sensor_data, + x="time", + y="temperature", + ) + temp_by_time_location = gr.BarPlot( + temp_sensor_data, + x="time", + y="temperature", + color="location", + ) + + time_graphs = [temp_by_time, temp_by_time_location] + group_by.change( + lambda group: [gr.BarPlot(x_bin=None if group == "None" else group)] * len(time_graphs), + group_by, + time_graphs + ) + aggregate.change( + lambda aggregate: [gr.BarPlot(y_aggregate=aggregate)] * len(time_graphs), + aggregate, + time_graphs + ) + + def rescale(select: gr.SelectData): + return select.index + rescale_evt = gr.on([plot.select for plot in time_graphs], rescale, None, [start, end]) + + for trigger in [apply_btn.click, rescale_evt.then]: + trigger( + lambda start, end: [gr.BarPlot(x_lim=[start, end])] * len(time_graphs), [start, end], time_graphs + ) + + with gr.Row(): + price_by_cuisine = gr.BarPlot( + food_rating_data, + x="cuisine", + y="price", + ) + with gr.Column(scale=0): + gr.Button("Sort $ > $$$").click(lambda: gr.BarPlot(sort="y"), None, price_by_cuisine) + gr.Button("Sort $$$ > $").click(lambda: gr.BarPlot(sort="-y"), None, price_by_cuisine) + gr.Button("Sort A > Z").click(lambda: gr.BarPlot(sort=["Chinese", "Italian", "Mexican"]), None, price_by_cuisine) + + with gr.Row(): + price_by_rating = gr.BarPlot( + food_rating_data, + x="rating", + y="price", + x_bin=1, + ) + price_by_rating_color = gr.BarPlot( + food_rating_data, + x="rating", + y="price", + color="cuisine", + x_bin=1, + color_map={"Italian": "red", "Mexican": "green", "Chinese": "blue"}, + ) + +if __name__ == "__main__": + bar_plots.launch() diff --git a/demo/barplot_component/requirements.txt b/demo/barplot_component/requirements.txt new file mode 100644 index 0000000..fb6c7ed --- /dev/null +++ b/demo/barplot_component/requirements.txt @@ -0,0 +1 @@ +pandas diff --git a/demo/barplot_component/run.py b/demo/barplot_component/run.py new file mode 100644 index 0000000..d8e135b --- /dev/null +++ b/demo/barplot_component/run.py @@ -0,0 +1,21 @@ +import gradio as gr +import pandas as pd + +simple = pd.DataFrame( + { + "item": ["A", "B", "C", "D", "E", "F", "G", "H", "I"], + "inventory": [28, 55, 43, 91, 81, 53, 19, 87, 52], + } +) + +with gr.Blocks() as demo: + gr.BarPlot( + value=simple, + x="item", + y="inventory", + title="Simple Bar Plot", + container=False, + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/big_complex_demo/run.py b/demo/big_complex_demo/run.py new file mode 100644 index 0000000..6464a14 --- /dev/null +++ b/demo/big_complex_demo/run.py @@ -0,0 +1,67 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Tabs(): + with gr.Tab("Components", id="components"): + with gr.Row(): + with gr.Column(): + gr.Textbox(label="Textbox") + gr.Number(label="Number") + gr.Slider(minimum=0, maximum=100, label="Slider") + gr.Dropdown(choices=["A", "B", "C"], label="Dropdown") + gr.Radio(choices=["X", "Y", "Z"], label="Radio") + gr.Checkbox(label="Checkbox") + gr.CheckboxGroup( + choices=["1", "2", "3"], label="CheckboxGroup" + ) + gr.ColorPicker(label="ColorPicker") + with gr.Column(): + gr.Image(label="Image") + gr.Audio(label="Audio") + gr.Video(label="Video") + gr.File(label="File") + gr.Gallery(label="Gallery") + gr.Dataframe(label="Dataframe", headers=["A", "B", "C"]) + gr.JSON(label="JSON", value={"key": "value"}) + gr.Code(label="Code", language="python") + + with gr.Tab("Chatbot", id="chatbot"): + gr.Chatbot(label="Chatbot") + gr.Textbox(label="Message") + + with gr.Tab("Media", id="media"): + with gr.Row(): + gr.Image(label="Image Upload") + gr.Image(label="Image Output") + gr.Audio(label="Audio Player") + gr.Video(label="Video Player") + + with gr.Tab("Layout", id="layout"): + with gr.Accordion("Accordion 1", open=True): + gr.Markdown("## Content inside accordion 1") + gr.Textbox(label="Accordion Input 1") + with gr.Accordion("Accordion 2", open=False): + gr.Markdown("## Content inside accordion 2") + gr.Slider(minimum=0, maximum=50, label="Accordion Slider") + with gr.Accordion("Accordion 3", open=False): + gr.Dataframe(headers=["Col1", "Col2"], label="Accordion Table") + with gr.Row(): + with gr.Column(): + gr.Markdown("### Column 1") + gr.Textbox(label="Col1 Input") + with gr.Column(): + gr.Markdown("### Column 2") + gr.Number(label="Col2 Input") + + with gr.Tab("More", id="more"): + gr.HighlightedText( + label="HighlightedText", + value=[("Hello ", None), ("world", "POS")], + ) + gr.Label(label="Label", value={"cat": 0.7, "dog": 0.3}) + gr.Plot(label="Plot") + gr.HTML(value="
HTML content
") + gr.Markdown("### Markdown content") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_chained_events/run.py b/demo/blocks_chained_events/run.py new file mode 100644 index 0000000..4dbd2af --- /dev/null +++ b/demo/blocks_chained_events/run.py @@ -0,0 +1,46 @@ +import gradio as gr + +def failure(): + raise gr.Error("This should fail!") + +def exception(): + raise ValueError("Something went wrong") + +def success(): + return True + +def warning_fn(): + gr.Warning("This is a warning!") + +def info_fn(): + gr.Info("This is some info") + +with gr.Blocks() as demo: + gr.Markdown("Used in E2E tests of success event trigger. The then event covered in chatbot E2E tests." + " Also testing that the status modals show up.") + with gr.Row(): + result = gr.Textbox(label="Result") + result_2 = gr.Textbox(label="Consecutive Event") + result_failure = gr.Textbox(label="Failure Event") + with gr.Row(): + success_btn = gr.Button(value="Trigger Success") + success_btn_2 = gr.Button(value="Trigger Consecutive Success") + failure_btn = gr.Button(value="Trigger Failure") + failure_exception = gr.Button(value="Trigger Failure With ValueError") + with gr.Row(): + trigger_warning = gr.Button(value="Trigger Warning") + trigger_info = gr.Button(value="Trigger Info") + + success_btn_2.click(success, None, None).success(lambda: "First Event Trigered", None, result).success(lambda: "Consecutive Event Triggered", None, result_2) + success_event = success_btn.click(success, None, None) + success_event.success(lambda: "Success event triggered", inputs=None, outputs=result) + success_event.failure(lambda: "Should not be triggered", inputs=None, outputs=result_failure) + failure_event = failure_btn.click(failure, None, None) + failure_event.success(lambda: "Should not be triggered", inputs=None, outputs=result) + failure_event.failure(lambda: "Failure event triggered", inputs=None, outputs=result_failure) + failure_exception.click(exception, None, None) + trigger_warning.click(warning_fn, None, None) + trigger_info.click(info_fn, None, None) + +if __name__ == "__main__": + demo.launch(show_error=True) diff --git a/demo/blocks_essay/run.py b/demo/blocks_essay/run.py new file mode 100644 index 0000000..a522c34 --- /dev/null +++ b/demo/blocks_essay/run.py @@ -0,0 +1,69 @@ +import gradio as gr + +countries_cities_dict = { + "USA": ["New York", "Los Angeles", "Chicago"], + "Canada": ["Toronto", "Montreal", "Vancouver"], + "Pakistan": ["Karachi", "Lahore", "Islamabad"], +} + +def change_textbox(choice): + if choice == "short": + return gr.Textbox(lines=2, visible=True), gr.Button(interactive=True) + elif choice == "long": + return gr.Textbox(lines=8, visible=True, value="Lorem ipsum dolor sit amet"), gr.Button(interactive=True) + else: + return gr.Textbox(visible=False), gr.Button(interactive=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"], elem_id="essay-textbox") + + with gr.Row(): + num = gr.Number(minimum=0, maximum=100, label="input") + out = gr.Number(label="output") + minimum_slider = gr.Slider(0, 100, 0, label="min") + maximum_slider = gr.Slider(0, 100, 100, label="max") + submit_btn = gr.Button("Submit", variant="primary") + + with gr.Row(): + country = gr.Dropdown(list(countries_cities_dict.keys()), label="Country") + cities = gr.Dropdown([], label="Cities") + @country.change(inputs=country, outputs=cities) + def update_cities(country): + cities = list(countries_cities_dict[country]) + return gr.Dropdown(choices=cities, value=cities[0], interactive=True) + + def reset_bounds(minimum, maximum): + return gr.Number(minimum=minimum, maximum=maximum) + + radio.change(fn=change_textbox, inputs=radio, outputs=[text, submit_btn]) + gr.on( + [minimum_slider.change, maximum_slider.change], + reset_bounds, + [minimum_slider, maximum_slider], + outputs=num, + ) + num.submit(lambda x: x, num, out) + + with gr.Row(): + with gr.Column(elem_id="test-column") as test_col: + gr.Textbox("Content inside column", label="Column Content") + gr.Markdown("This column should hide/show when button is clicked") + toggle_btn = gr.Button("Toggle Column Visibility", elem_id="toggle-col-btn") + + col_visible = gr.State(True) + + def toggle_column(visible): + new_visible = not visible + return gr.Column(visible=new_visible), new_visible + + toggle_btn.click( + toggle_column, + inputs=col_visible, + outputs=[test_col, col_visible] + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_essay_simple/run.py b/demo/blocks_essay_simple/run.py new file mode 100644 index 0000000..236eaab --- /dev/null +++ b/demo/blocks_essay_simple/run.py @@ -0,0 +1,19 @@ +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) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_flag/requirements.txt b/demo/blocks_flag/requirements.txt new file mode 100644 index 0000000..296d654 --- /dev/null +++ b/demo/blocks_flag/requirements.txt @@ -0,0 +1 @@ +numpy \ No newline at end of file diff --git a/demo/blocks_flag/run.py b/demo/blocks_flag/run.py new file mode 100644 index 0000000..630e314 --- /dev/null +++ b/demo/blocks_flag/run.py @@ -0,0 +1,33 @@ +import numpy as np +import gradio as gr + +def sepia(input_img, strength): + sepia_filter = strength * np.array( + [[0.393, 0.769, 0.189], [0.349, 0.686, 0.168], [0.272, 0.534, 0.131]] + ) + (1-strength) * np.identity(3) + sepia_img = input_img.dot(sepia_filter.T) + sepia_img /= sepia_img.max() + return sepia_img + +callback = gr.CSVLogger() + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + img_input = gr.Image() + strength = gr.Slider(0, 1, 0.5) + img_output = gr.Image() + with gr.Row(): + btn = gr.Button("Flag") + + # This needs to be called at some point prior to the first call to callback.flag() + callback.setup([img_input, strength, img_output], "flagged_data_points") + + img_input.change(sepia, [img_input, strength], img_output) + strength.change(sepia, [img_input, strength], img_output) + + # We can choose which components to flag -- in this case, we'll flag all of them + btn.click(lambda *args: callback.flag(list(args)), [img_input, strength, img_output], None, preprocess=False) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_flashcards/run.py b/demo/blocks_flashcards/run.py new file mode 100644 index 0000000..9e4f4d1 --- /dev/null +++ b/demo/blocks_flashcards/run.py @@ -0,0 +1,106 @@ +import random + +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown( + "Load the flashcards in the table below, then use the Practice tab to practice." + ) + + with gr.Tabs() as tabs: + with gr.Tab("Word Bank"): + flashcards_table = gr.Dataframe(headers=["front", "back"], type="array") + flashcards_table.change(fn=lambda: print(flashcards_table.value)) + practice_btn = gr.Button("Start Practice") + with gr.Tab("Practice", interactive=False, id=1) as practice_tab: + with gr.Row(): + with gr.Column(): + front = gr.Textbox(label="Prompt") + with gr.Row(): + new_btn = gr.Button("New Card") + flip_btn = gr.Button("Flip Card") + with gr.Column(visible=False) as answer_col: + back = gr.Textbox(label="Answer") + selected_card = gr.State() + with gr.Row(): + correct_btn = gr.Button("Correct") + incorrect_btn = gr.Button("Incorrect") + + def start_practice(flashcards): + # if no cards entered into dataframe yet, return + if len(flashcards) == 0: + practice_tab = gr.Tab("Practice", interactive=False, id=1) + raise gr.Error("Please enter word prompts into the table.") + return [practice_tab, tabs] + else: + practice_tab = gr.Tab("Practice", interactive=True, id=1) + new_tabs = gr.Tabs(selected=1) + return [practice_tab, new_tabs] + + with gr.Tab("Results", visible=False) as results_tab: + results = gr.State(value={}) + correct_field = gr.Markdown("# Correct: 0") + incorrect_field = gr.Markdown("# Incorrect: 0") + gr.Markdown("Card Statistics: ") + results_table = gr.Dataframe(headers=["Card", "Correct", "Incorrect"]) + practice_btn.click(start_practice, inputs=[flashcards_table], outputs=[practice_tab, tabs]) + + def load_new_card(flashcards): + card = random.choice(flashcards) + return ( + card, + card[0], + gr.Column(visible=False), + ) + + new_btn.click( + load_new_card, + [flashcards_table], + [selected_card, front, answer_col], + ) + + def flip_card(card): + return card[1], gr.Column(visible=True) + + flip_btn.click(flip_card, [selected_card], [back, answer_col]) + + def mark_correct(card, results): + if card[0] not in results: + results[card[0]] = [0, 0] + results[card[0]][0] += 1 + correct_count = sum(result[0] for result in results.values()) + return ( + results, + f"# Correct: {correct_count}", + [[front, scores[0], scores[1]] for front, scores in results.items()], + ) + + def mark_incorrect(card, results): + if card[0] not in results: + results[card[0]] = [ + 0, 0] + results[card[0]][1] += 1 + incorrect_count = sum(result[1] for result in results.values()) + return ( + results, + f"# Inorrect: {incorrect_count}", + [[front, scores[0], scores[1]] for front, scores in results.items()], + ) + + def toggle_results_tab(): + return gr.Tab("Results", visible=True) + + correct_btn.click( + mark_correct, + [selected_card, results], + [results, correct_field, results_table], + ) + + incorrect_btn.click(mark_incorrect, [selected_card, results], [results, incorrect_field, results_table]) + + # set results tab to visible when correct or incorrect button is clicked + correct_btn.click(fn=toggle_results_tab, outputs=[results_tab]) + incorrect_btn.click(fn=toggle_results_tab, outputs=[results_tab]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_flipper/requirements.txt b/demo/blocks_flipper/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/blocks_flipper/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/blocks_flipper/run.py b/demo/blocks_flipper/run.py new file mode 100644 index 0000000..09cd34a --- /dev/null +++ b/demo/blocks_flipper/run.py @@ -0,0 +1,36 @@ +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) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_flipper/screenshot.gif b/demo/blocks_flipper/screenshot.gif new file mode 100644 index 0000000..2b38ac0 Binary files /dev/null and b/demo/blocks_flipper/screenshot.gif differ diff --git a/demo/blocks_form/run.py b/demo/blocks_form/run.py new file mode 100644 index 0000000..6a1a282 --- /dev/null +++ b/demo/blocks_form/run.py @@ -0,0 +1,28 @@ +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], + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_gpt/run.py b/demo/blocks_gpt/run.py new file mode 100644 index 0000000..0fd7a4c --- /dev/null +++ b/demo/blocks_gpt/run.py @@ -0,0 +1,16 @@ +import gradio as gr + +api = gr.load("huggingface/gpt2-xl") + +def complete_with_gpt(text): + # Use the last 50 characters of the text as context + return text[:-50] + api(text[-50:]) + +with gr.Blocks() as demo: + textbox = gr.Textbox(placeholder="Type here and press enter...", lines=4) + btn = gr.Button("Generate") + + btn.click(complete_with_gpt, textbox, textbox) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_group/run.py b/demo/blocks_group/run.py new file mode 100644 index 0000000..b91d4bc --- /dev/null +++ b/demo/blocks_group/run.py @@ -0,0 +1,110 @@ +import gradio as gr + +def greet(name): + return "Hello " + name + "!" + +with gr.Blocks() as demo: + gr.Markdown("### This is a couple of elements without any gr.Group. Form elements naturally group together anyway.") + gr.Textbox("A") + gr.Number(3) + gr.Button() + gr.Image() + gr.Slider() + + gr.Markdown("### This is the same set put in a gr.Group.") + with gr.Group(): + gr.Textbox("A") + gr.Number(3) + gr.Button() + gr.Image() + gr.Slider() + + gr.Markdown("### Now in a Row, no group.") + with gr.Row(): + gr.Textbox("A") + gr.Number(3) + gr.Button() + gr.Image() + gr.Slider() + + gr.Markdown("### Now in a Row in a group.") + with gr.Group(): + with gr.Row(): + gr.Textbox("A") + gr.Number(3) + gr.Button() + gr.Image() + gr.Slider() + + gr.Markdown("### Several rows grouped together.") + with gr.Group(): + with gr.Row(): + gr.Textbox("A") + gr.Number(3) + gr.Button() + with gr.Row(): + gr.Image() + gr.Audio() + + gr.Markdown("### Several columns grouped together. If columns are uneven, there is a gray group background.") + with gr.Group(): + with gr.Row(): + with gr.Column(): + name = gr.Textbox(label="Name") + btn = gr.Button("Hello") + gr.Dropdown(["a", "b", "c"], interactive=True) + gr.Number() + gr.Textbox() + with gr.Column(): + gr.Image() + gr.Dropdown(["a", "b", "c"], interactive=True) + with gr.Row(): + gr.Number(scale=2) + gr.Textbox() + + gr.Markdown("### container=False removes label, padding, and block border, placing elements 'directly' on background.") + gr.Radio([1,2,3], container=False) + gr.Textbox(container=False) + gr.Image("https://picsum.photos/id/237/200/300", container=False, height=200) + + gr.Markdown("### Textbox, Dropdown, and Number input boxes takes up full space when within a group without a container.") + + with gr.Group(): + name = gr.Textbox(label="Name") + output = gr.Textbox(show_label=False, container=False) + greet_btn = gr.Button("Greet") + with gr.Row(): + gr.Dropdown(["a", "b", "c"], interactive=True, container=False) + gr.Textbox(container=False) + gr.Number(container=False) + gr.Image(height=100) + greet_btn.click(fn=greet, inputs=name, outputs=output, api_name="greet") + + gr.Markdown("### More examples") + + with gr.Group(): + gr.Chatbot() + with gr.Row(): + name = gr.Textbox(label="Prompot", container=False) + go = gr.Button("go", scale=0) + + with gr.Column(): + gr.Radio([1,2,3], container=False) + gr.Slider(0, 20, container=False) + + with gr.Group(): + with gr.Row(): + gr.Dropdown(["a", "b", "c"], interactive=True, container=False, elem_id="here2") + gr.Number(container=False) + gr.Textbox(container=False) + + with gr.Row(): + with gr.Column(): + gr.Dropdown(["a", "b", "c"], interactive=True, container=False, elem_id="here2") + with gr.Column(): + gr.Number(container=False) + with gr.Column(): + gr.Textbox(container=False) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_hello/run.py b/demo/blocks_hello/run.py new file mode 100644 index 0000000..6d5b8d9 --- /dev/null +++ b/demo/blocks_hello/run.py @@ -0,0 +1,17 @@ +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) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_inputs/run.py b/demo/blocks_inputs/run.py new file mode 100644 index 0000000..6f5a511 --- /dev/null +++ b/demo/blocks_inputs/run.py @@ -0,0 +1,43 @@ +import gradio as gr +import os + +def combine(a, b): + return a + " " + b + +def mirror(x): + return x + +with gr.Blocks() as demo: + + txt = gr.Textbox(label="Input", lines=2) + txt_2 = gr.Textbox(label="Input 2") + txt_3 = gr.Textbox(value="", label="Output") + btn = gr.Button(value="Submit") + btn.click(combine, inputs=[txt, txt_2], outputs=[txt_3]) + + with gr.Row(): + im = gr.Image() + im_2 = gr.Image() + + btn = gr.Button(value="Mirror Image") + btn.click(mirror, inputs=[im], outputs=[im_2]) + + gr.Markdown("## Text Examples") + gr.Examples( + [["hi", "Adam"], ["hello", "Eve"]], + [txt, txt_2], + txt_3, + combine, + cache_examples=True, + ) + gr.Markdown("## Image Examples") + gr.Examples( + examples=[gr.get_image("lion.jpg")], + inputs=im, + outputs=im_2, + fn=mirror, + cache_examples=True, + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_joined/run.py b/demo/blocks_joined/run.py new file mode 100644 index 0000000..a3e91f3 --- /dev/null +++ b/demo/blocks_joined/run.py @@ -0,0 +1,47 @@ +from time import sleep +import gradio as gr +from gradio.media import get_image + +# get_image() returns file paths to sample media included with Gradio +cheetah = get_image("cheetah1.jpg") + +def img(text): + sleep(3) + return [ + cheetah, + cheetah, + cheetah, + cheetah, + cheetah, + cheetah, + cheetah, + cheetah, + cheetah, + ] + +with gr.Blocks() as demo: + gr.Markdown("

DALL·E mini

") + gr.Markdown( + "DALL·E mini is an AI model that generates images from any prompt you give!" + ) + with gr.Group(): + with gr.Row(equal_height=True): + text = gr.Textbox( + label="Enter your prompt", + max_lines=1, + container=False, + ) + btn = gr.Button("Run", scale=0) + gallery = gr.Gallery( + label="Generated images", + show_label=False, + columns=1, + height="auto", + ) + btn.click(img, inputs=text, outputs=gallery) + +if __name__ == "__main__": + demo.launch(css=".container { max-width: 800px; margin: auto; }") + +# margin = (TOP, RIGHT, BOTTOM, LEFT) +# rounded = (TOPLEFT, TOPRIGHT, BOTTOMRIGHT, BOTTOMLEFT) diff --git a/demo/blocks_js_load/run.py b/demo/blocks_js_load/run.py new file mode 100644 index 0000000..afdb27b --- /dev/null +++ b/demo/blocks_js_load/run.py @@ -0,0 +1,48 @@ +import gradio as gr + +def welcome(name): + return f"Welcome to Gradio, {name}!" + +js = """ +function createGradioAnimation() { + var container = document.createElement('div'); + container.id = 'gradio-animation'; + container.style.fontSize = '2em'; + container.style.fontWeight = 'bold'; + container.style.textAlign = 'center'; + container.style.marginBottom = '20px'; + + var text = 'Welcome to Gradio!'; + for (var i = 0; i < text.length; i++) { + (function(i){ + setTimeout(function(){ + var letter = document.createElement('span'); + letter.style.opacity = '0'; + letter.style.transition = 'opacity 0.5s'; + letter.innerText = text[i]; + + container.appendChild(letter); + + setTimeout(function() { + letter.style.opacity = '1'; + }, 50); + }, i * 250); + })(i); + } + + var gradioContainer = document.querySelector('.gradio-container'); + gradioContainer.insertBefore(container, gradioContainer.firstChild); + + return 'Animation created'; +} + +createGradioAnimation(); +""" + +with gr.Blocks() as demo: + inp = gr.Textbox(placeholder="What is your name?") + out = gr.Textbox() + inp.change(welcome, inp, out) + +if __name__ == "__main__": + demo.launch(js=js) diff --git a/demo/blocks_js_methods/run.py b/demo/blocks_js_methods/run.py new file mode 100644 index 0000000..e6b3236 --- /dev/null +++ b/demo/blocks_js_methods/run.py @@ -0,0 +1,41 @@ +import gradio as gr + +blocks = gr.Blocks() + +with blocks as demo: + subject = gr.Textbox(placeholder="subject") + verb = gr.Radio(["ate", "loved", "hated"]) + object = gr.Textbox(placeholder="object") + + with gr.Row(): + btn = gr.Button("Create sentence.") + reverse_btn = gr.Button("Reverse sentence.") + foo_bar_btn = gr.Button("Append foo") + reverse_then_to_the_server_btn = gr.Button( + "Reverse sentence and send to server." + ) + + def sentence_maker(w1, w2, w3): + return f"{w1} {w2} {w3}" + + output1 = gr.Textbox(label="output 1") + output2 = gr.Textbox(label="verb") + output3 = gr.Textbox(label="verb reversed") + output4 = gr.Textbox(label="front end process and then send to backend") + + btn.click(sentence_maker, [subject, verb, object], output1) + reverse_btn.click( + None, [subject, verb, object], output2, js="(s, v, o) => o + ' ' + v + ' ' + s" + ) + verb.change(None, verb, output3, js="(x) => [...x].reverse().join('')") + foo_bar_btn.click(None, [], subject, js="(x) => x + ' foo'") + + reverse_then_to_the_server_btn.click( + None, + [subject, verb, object], + output4, + js="(s, v, o) => [s, v, o].map(x => [...x].reverse().join('')).join(' ')", + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_kinematics/requirements.txt b/demo/blocks_kinematics/requirements.txt new file mode 100644 index 0000000..5da331c --- /dev/null +++ b/demo/blocks_kinematics/requirements.txt @@ -0,0 +1,2 @@ +numpy +pandas diff --git a/demo/blocks_kinematics/run.py b/demo/blocks_kinematics/run.py new file mode 100644 index 0000000..61cf7ba --- /dev/null +++ b/demo/blocks_kinematics/run.py @@ -0,0 +1,39 @@ +import pandas as pd +import numpy as np + +import gradio as gr + +def plot(v, a): + g = 9.81 + theta = a / 180 * 3.14 + tmax = ((2 * v) * np.sin(theta)) / g + timemat = tmax * np.linspace(0, 1, 40) + + x = (v * timemat) * np.cos(theta) + y = ((v * timemat) * np.sin(theta)) - ((0.5 * g) * (timemat**2)) + df = pd.DataFrame({"x": x, "y": y}) + return df + +demo = gr.Blocks() + +with demo: + gr.Markdown( + r"Let's do some kinematics! Choose the speed and angle to see the trajectory. Remember that the range $R = v_0^2 \cdot \frac{\sin(2\theta)}{g}$" + ) + + with gr.Row(): + speed = gr.Slider(1, 30, 25, label="Speed") + angle = gr.Slider(0, 90, 45, label="Angle") + output = gr.LinePlot( + x="x", + y="y", + tooltip=["x", "y"], + x_lim=[0, 100], + y_lim=[0, 60], + height=300, + ) + btn = gr.Button(value="Run") + btn.click(plot, [speed, angle], output) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_layout/run.py b/demo/blocks_layout/run.py new file mode 100644 index 0000000..3f7686f --- /dev/null +++ b/demo/blocks_layout/run.py @@ -0,0 +1,37 @@ +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"]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_multiple_event_triggers/requirements.txt b/demo/blocks_multiple_event_triggers/requirements.txt new file mode 100644 index 0000000..bbe82b5 --- /dev/null +++ b/demo/blocks_multiple_event_triggers/requirements.txt @@ -0,0 +1,3 @@ +plotly +pypistats +python-dateutil diff --git a/demo/blocks_multiple_event_triggers/run.py b/demo/blocks_multiple_event_triggers/run.py new file mode 100644 index 0000000..ff32287 --- /dev/null +++ b/demo/blocks_multiple_event_triggers/run.py @@ -0,0 +1,36 @@ +import gradio as gr +import pypistats # type: ignore +from datetime import date +from dateutil.relativedelta import relativedelta +import pandas as pd + +def get_plot(lib, time): + data = pypistats.overall(lib, total=True, format="pandas") + data = data.groupby("category").get_group("with_mirrors").sort_values("date") + start_date = date.today() - relativedelta(months=int(time.split(" ")[0])) + data = data[(data['date'] > str(start_date))] + data.date = pd.to_datetime(pd.to_datetime(data.date)) + return gr.LinePlot(value=data, x="date", y="downloads", + tooltip=['date', 'downloads'], + title=f"Pypi downloads of {lib} over last {time}", + height=400) + +with gr.Blocks() as demo: + gr.Markdown( + """ + ## Pypi Download Stats 📈 + See live download stats for all of Hugging Face's open-source libraries 🤗 + """) + with gr.Row(): + lib = gr.Dropdown(["transformers", "datasets", "huggingface-hub", "gradio", "accelerate"], + value="gradio", label="Library") + time = gr.Dropdown(["3 months", "6 months", "9 months", "12 months"], + value="3 months", label="Downloads over the last...") + + plt = gr.LinePlot() + # You can add multiple event triggers in 2 lines like this + for event in [lib.change, time.change, demo.load]: + event(get_plot, [lib, time], [plt]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_neural_instrument_coding/flute.wav b/demo/blocks_neural_instrument_coding/flute.wav new file mode 100644 index 0000000..17430f9 Binary files /dev/null and b/demo/blocks_neural_instrument_coding/flute.wav differ diff --git a/demo/blocks_neural_instrument_coding/new-sax-1.mp3 b/demo/blocks_neural_instrument_coding/new-sax-1.mp3 new file mode 100644 index 0000000..a323cae Binary files /dev/null and b/demo/blocks_neural_instrument_coding/new-sax-1.mp3 differ diff --git a/demo/blocks_neural_instrument_coding/new-sax-1.wav b/demo/blocks_neural_instrument_coding/new-sax-1.wav new file mode 100644 index 0000000..1c92620 Binary files /dev/null and b/demo/blocks_neural_instrument_coding/new-sax-1.wav differ diff --git a/demo/blocks_neural_instrument_coding/new-sax.wav b/demo/blocks_neural_instrument_coding/new-sax.wav new file mode 100644 index 0000000..ec61ad2 Binary files /dev/null and b/demo/blocks_neural_instrument_coding/new-sax.wav differ diff --git a/demo/blocks_neural_instrument_coding/run.py b/demo/blocks_neural_instrument_coding/run.py new file mode 100644 index 0000000..df429ec --- /dev/null +++ b/demo/blocks_neural_instrument_coding/run.py @@ -0,0 +1,138 @@ +# A Blocks implementation of https://erlj.notion.site/Neural-Instrument-Cloning-from-very-few-samples-2cf41d8b630842ee8c7eb55036a1bfd6 + +import datetime +import os +import random + +import gradio as gr +from gradio.components import Markdown as m + +def get_time(): + now = datetime.datetime.now() + return now.strftime("%m/%d/%Y, %H:%M:%S") + +def generate_recording(): + return random.choice(["new-sax-1.mp3", "new-sax-1.wav"]) + +def reconstruct(audio): + return random.choice(["new-sax-1.mp3", "new-sax-1.wav"]) + +io1 = gr.Interface( + lambda x, y, z: os.path.join(os.path.dirname(__file__),"sax.wav"), + [ + gr.Slider(label="pitch"), + gr.Slider(label="loudness"), + gr.Audio(label="base audio file (optional)"), + ], + gr.Audio(), +) + +io2 = gr.Interface( + lambda x, y, z: os.path.join(os.path.dirname(__file__),"flute.wav"), + [ + gr.Slider(label="pitch"), + gr.Slider(label="loudness"), + gr.Audio(label="base audio file (optional)"), + ], + gr.Audio(), +) + +io3 = gr.Interface( + lambda x, y, z: os.path.join(os.path.dirname(__file__),"trombone.wav"), + [ + gr.Slider(label="pitch"), + gr.Slider(label="loudness"), + gr.Audio(label="base audio file (optional)"), + ], + gr.Audio(), +) + +io4 = gr.Interface( + lambda x, y, z: os.path.join(os.path.dirname(__file__),"sax2.wav"), + [ + gr.Slider(label="pitch"), + gr.Slider(label="loudness"), + gr.Audio(label="base audio file (optional)"), + ], + gr.Audio(), +) + +demo = gr.Blocks(title="Neural Instrument Cloning") + +with demo.clear(): + m( + """ + ## Neural Instrument Cloning from Very Few Samples +
""" + ) + m( + """ + This Blocks implementation is an adaptation [a report written](https://erlj.notion.site/Neural-Instrument-Cloning-from-very-few-samples-2cf41d8b630842ee8c7eb55036a1bfd6) by Nicolas Jonason and Bob L.T. Sturm. + + I've implemented it in Blocks to show off some cool features, such as embedding live ML demos. More on that ahead... + + ### What does this machine learning model do? + It combines techniques from neural voice cloning with musical instrument synthesis. This makes it possible to produce neural instrument synthesisers from just seconds of target instrument audio. + + ### Audio Examples + Here are some **real** 16 second saxophone recordings: + """ + ) + gr.Audio(os.path.join(os.path.dirname(__file__),"sax.wav"), label="Here is a real 16 second saxophone recording:") + gr.Audio(os.path.join(os.path.dirname(__file__),"sax.wav")) + + m( + """\n + Here is a **generated** saxophone recordings:""" + ) + a = gr.Audio(os.path.join(os.path.dirname(__file__),"new-sax.wav")) + + gr.Button("Generate a new saxophone recording") + + m( + """ + ### Inputs to the model + The inputs to the model are: + * pitch + * loudness + * base audio file + """ + ) + + m( + """ + Try the model live! + """ + ) + + gr.TabbedInterface( + [io1, io2, io3, io4], ["Saxophone", "Flute", "Trombone", "Another Saxophone"] + ) + + m( + """ + ### Using the model for cloning + You can also use this model a different way, to simply clone the audio file and reconstruct it + using machine learning. Here, we'll show a demo of that below: + """ + ) + + a2 = gr.Audio() + a2.change(reconstruct, a2, a2) + + m( + """ + Thanks for reading this! As you may have realized, all of the "models" in this demo are fake. They are just designed to show you what is possible using Blocks 🤗. + + For details of the model, read the [original report here](https://erlj.notion.site/Neural-Instrument-Cloning-from-very-few-samples-2cf41d8b630842ee8c7eb55036a1bfd6). + + *Details for nerds*: this report was "launched" on: + """ + ) + + t = gr.Textbox(label="timestamp") + + demo.load(get_time, [], t) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_neural_instrument_coding/sax.wav b/demo/blocks_neural_instrument_coding/sax.wav new file mode 100644 index 0000000..de6d16a Binary files /dev/null and b/demo/blocks_neural_instrument_coding/sax.wav differ diff --git a/demo/blocks_neural_instrument_coding/sax2.wav b/demo/blocks_neural_instrument_coding/sax2.wav new file mode 100644 index 0000000..73963a5 Binary files /dev/null and b/demo/blocks_neural_instrument_coding/sax2.wav differ diff --git a/demo/blocks_neural_instrument_coding/trombone.wav b/demo/blocks_neural_instrument_coding/trombone.wav new file mode 100644 index 0000000..55d17d8 Binary files /dev/null and b/demo/blocks_neural_instrument_coding/trombone.wav differ diff --git a/demo/blocks_outputs/run.py b/demo/blocks_outputs/run.py new file mode 100644 index 0000000..4402087 --- /dev/null +++ b/demo/blocks_outputs/run.py @@ -0,0 +1,93 @@ +import gradio as gr + +def make_markdown(): + return [ + [ + "# hello again", + "Hello my name is frank, I am liking the small turtle you have there. It would be a shame if it went missing.", + '', + ], + [ + "## hello again again", + "Hello my name is frank, I am liking the small turtle you have there. It would be a shame if it went missing.", + '', + ], + [ + "### hello thrice", + "Hello my name is frank, I am liking the small turtle you have there. It would be a shame if it went missing.", + '', + ], + ] + +with gr.Blocks() as demo: + with gr.Column(): + txt = gr.Textbox(label="Small Textbox", lines=1, show_label=False) + txt = gr.Textbox(label="Large Textbox", lines=5, show_label=False) + num = gr.Number(label="Number", show_label=False) + check = gr.Checkbox(label="Checkbox", show_label=False) + check_g = gr.CheckboxGroup( + label="Checkbox Group", choices=["One", "Two", "Three"], show_label=False + ) + radio = gr.Radio( + label="Radio", choices=["One", "Two", "Three"], show_label=False + ) + drop = gr.Dropdown( + label="Dropdown", choices=["One", "Two", "Three"], show_label=False + ) + slider = gr.Slider(label="Slider", show_label=False) + audio = gr.Audio(show_label=False) + file = gr.File(show_label=False) + video = gr.Video(show_label=False) + image = gr.Image(show_label=False) + df = gr.Dataframe(show_label=False) + html = gr.HTML(show_label=False) + json = gr.JSON(show_label=False) + md = gr.Markdown(show_label=False) + label = gr.Label(show_label=False) + highlight = gr.HighlightedText(show_label=False) + gr.Dataframe(interactive=True, column_count=3, column_limits=(3, 3), label="Dataframe") + gr.Dataframe(interactive=True, column_count=4, label="Dataframe") + gr.Dataframe( + interactive=True, headers=["One", "Two", "Three", "Four"], label="Dataframe" + ) + gr.Dataframe( + interactive=True, + headers=["One", "Two", "Three", "Four"], + column_count=4, + column_limits=(4, 4), + row_count=7, + row_limits=(7, 7), + value=[[0, 0, 0, 0]], + label="Dataframe", + ) + gr.Dataframe( + interactive=True, headers=["One", "Two", "Three", "Four"], column_count=4 + ) + df = gr.DataFrame( + [ + [ + "# hello", + "Hello my name is frank, I am liking the small turtle you have there. It would be a shame if it went missing.", + '', + ], + [ + "## hello", + "Hello my name is frank, I am liking the small turtle you have there. It would be a shame if it went missing.", + '', + ], + [ + "### hello", + "Hello my name is frank, I am liking the small turtle you have there. It would be a shame if it went missing.", + '', + ], + ], + headers=["One", "Two", "Three"], + wrap=True, + datatype=["markdown", "markdown", "html"], # type: ignore + interactive=True, + ) + btn = gr.Button("Run") + btn.click(fn=make_markdown, inputs=None, outputs=df) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_page_load/run.py b/demo/blocks_page_load/run.py new file mode 100644 index 0000000..4070d00 --- /dev/null +++ b/demo/blocks_page_load/run.py @@ -0,0 +1,12 @@ +import gradio as gr + +def print_message(n): + return "Welcome! This page has loaded for " + n + +with gr.Blocks() as demo: + t = gr.Textbox("Frank", label="Name") + t2 = gr.Textbox(label="Output") + demo.load(print_message, t, t2) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_plug/run.py b/demo/blocks_plug/run.py new file mode 100644 index 0000000..716ec94 --- /dev/null +++ b/demo/blocks_plug/run.py @@ -0,0 +1,36 @@ +import gradio as gr + +def change_tab(): + return gr.Tabs(selected=2) + +identity_demo, input_demo, output_demo = gr.Blocks(), gr.Blocks(), gr.Blocks() + +with identity_demo: + gr.Interface(lambda x: x, "text", "text") + +with input_demo: + t = gr.Textbox(label="Enter your text here") + with gr.Row(): + btn = gr.Button("Submit") + clr = gr.ClearButton(t) + +with output_demo: + gr.Textbox("This is a static output") + +with gr.Blocks() as demo: + gr.Markdown("Three demos in one!") + with gr.Tabs(selected=1) as tabs: + with gr.TabItem("Text Identity", id=0) as tab0: + tab0.select(lambda: gr.Tabs(selected=0), None, tabs) + identity_demo.render() + with gr.TabItem("Text Input", id=1) as tab1: + tab1.select(lambda: gr.Tabs(selected=1), None, tabs) + input_demo.render() + with gr.TabItem("Text Static", id=2) as tab2: + tab2.select(lambda: gr.Tabs(selected=2), None, tabs) + output_demo.render() + btn = gr.Button("Change tab") + btn.click(inputs=None, outputs=tabs, fn=change_tab) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_random_slider/run.py b/demo/blocks_random_slider/run.py new file mode 100644 index 0000000..9634fc6 --- /dev/null +++ b/demo/blocks_random_slider/run.py @@ -0,0 +1,16 @@ + +import gradio as gr + +def func(slider_1, slider_2): + return slider_1 * 5 + slider_2 + +with gr.Blocks() as demo: + slider = gr.Slider(minimum=-10.2, maximum=15, label="Random Slider (Static)", randomize=True) + slider_1 = gr.Slider(minimum=100, maximum=200, label="Random Slider (Input 1)", randomize=True) + slider_2 = gr.Slider(minimum=10, maximum=23.2, label="Random Slider (Input 2)", randomize=True) + slider_3 = gr.Slider(value=3, label="Non random slider") + btn = gr.Button("Run") + btn.click(func, inputs=[slider_1, slider_2], outputs=gr.Number()) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_scroll/run.py b/demo/blocks_scroll/run.py new file mode 100644 index 0000000..ec3f204 --- /dev/null +++ b/demo/blocks_scroll/run.py @@ -0,0 +1,23 @@ +import gradio as gr + +demo = gr.Blocks() + +with demo: + inp = gr.Textbox(placeholder="Enter text.") + scroll_btn = gr.Button("Scroll") + no_scroll_btn = gr.Button("No Scroll") + big_block = gr.HTML(""" +
+ """) + out = gr.Textbox() + + scroll_btn.click(lambda x: x, + inputs=inp, + outputs=out, + scroll_to_output=True) + no_scroll_btn.click(lambda x: x, + inputs=inp, + outputs=out) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_sidebar/run.py b/demo/blocks_sidebar/run.py new file mode 100644 index 0000000..77364c4 --- /dev/null +++ b/demo/blocks_sidebar/run.py @@ -0,0 +1,48 @@ +import gradio as gr +import random + +def generate_pet_name(animal_type, personality): + cute_prefixes = ["Fluffy", "Ziggy", "Bubbles", "Pickle", "Waffle", "Mochi", "Cookie", "Pepper"] + animal_suffixes = { + "Cat": ["Whiskers", "Paws", "Mittens", "Purrington"], + "Dog": ["Woofles", "Barkington", "Waggins", "Pawsome"], + "Bird": ["Feathers", "Wings", "Chirpy", "Tweets"], + "Rabbit": ["Hops", "Cottontail", "Bouncy", "Fluff"] + } + + prefix = random.choice(cute_prefixes) + suffix = random.choice(animal_suffixes[animal_type]) + + if personality == "Silly": + prefix = random.choice(["Sir", "Lady", "Captain", "Professor"]) + " " + prefix + elif personality == "Royal": + suffix += " the " + random.choice(["Great", "Magnificent", "Wise", "Brave"]) + + return f"{prefix} {suffix}" + +with gr.Blocks() as demo: + with gr.Sidebar(position="left"): + gr.Markdown("# 🐾 Pet Name Generator") + gr.Markdown("Use the options below to generate a unique pet name!") + + animal_type = gr.Dropdown( + choices=["Cat", "Dog", "Bird", "Rabbit"], + label="Choose your pet type", + value="Cat" + ) + personality = gr.Radio( + choices=["Normal", "Silly", "Royal"], + label="Personality type", + value="Normal" + ) + + name_output = gr.Textbox(label="Your pet's fancy name:", lines=2) + generate_btn = gr.Button("Generate Name! 🎲", variant="primary") + generate_btn.click( + fn=generate_pet_name, + inputs=[animal_type, personality], + outputs=name_output + ) + +if __name__ == "__main__": + demo.launch(theme=gr.themes.Soft()) diff --git a/demo/blocks_simple_squares/run.py b/demo/blocks_simple_squares/run.py new file mode 100644 index 0000000..f4311f5 --- /dev/null +++ b/demo/blocks_simple_squares/run.py @@ -0,0 +1,23 @@ +import gradio as gr + +demo = gr.Blocks() + +with demo: + default_json = {"a": "a"} + + num = gr.State(value=0) + squared = gr.Number(value=0) + btn = gr.Button("Next Square", elem_id="btn", elem_classes=["abc", "def"]) + + stats = gr.State(value=default_json) + table = gr.JSON() + + def increase(var, stats_history): + var += 1 + stats_history[str(var)] = var**2 + return var, var**2, stats_history, stats_history + + btn.click(increase, [num, stats], [num, squared, stats, table]) + +if __name__ == "__main__": + demo.launch(css="""#btn {color: red} .abc {font-family: "Comic Sans MS", "Comic Sans", cursive !important}""") diff --git a/demo/blocks_speech_text_sentiment/requirements.txt b/demo/blocks_speech_text_sentiment/requirements.txt new file mode 100644 index 0000000..39dab0f --- /dev/null +++ b/demo/blocks_speech_text_sentiment/requirements.txt @@ -0,0 +1,2 @@ +torch +transformers \ No newline at end of file diff --git a/demo/blocks_speech_text_sentiment/run.py b/demo/blocks_speech_text_sentiment/run.py new file mode 100644 index 0000000..0b72863 --- /dev/null +++ b/demo/blocks_speech_text_sentiment/run.py @@ -0,0 +1,29 @@ +from transformers import pipeline + +import gradio as gr + +asr = pipeline("automatic-speech-recognition", "facebook/wav2vec2-base-960h") +classifier = pipeline("text-classification") + +def speech_to_text(speech): + text = asr(speech)["text"] # type: ignore + return text + +def text_to_sentiment(text): + return classifier(text)[0]["label"] # type: ignore + +demo = gr.Blocks() + +with demo: + audio_file = gr.Audio(type="filepath") + text = gr.Textbox() + label = gr.Label() + + b1 = gr.Button("Recognize Speech") + b2 = gr.Button("Classify Sentiment") + + b1.click(speech_to_text, inputs=audio_file, outputs=text) + b2.click(text_to_sentiment, inputs=text, outputs=label) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_static/run.py b/demo/blocks_static/run.py new file mode 100644 index 0000000..79aba06 --- /dev/null +++ b/demo/blocks_static/run.py @@ -0,0 +1,13 @@ +import gradio as gr + +demo = gr.Blocks() + +with demo: + gr.Image( + "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80" + ) + gr.Textbox("hi") + gr.Number(3) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_textbox_max_lines/run.py b/demo/blocks_textbox_max_lines/run.py new file mode 100644 index 0000000..f1f2aed --- /dev/null +++ b/demo/blocks_textbox_max_lines/run.py @@ -0,0 +1,11 @@ +import gradio as gr + +def greet(name: str, repeat: float): + return "Hello " + name * int(repeat) + "!!" + +demo = gr.Interface( + fn=greet, inputs=[gr.Textbox(lines=2, max_lines=4), gr.Number()], outputs="textarea", api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_update/run.py b/demo/blocks_update/run.py new file mode 100644 index 0000000..03d4e5a --- /dev/null +++ b/demo/blocks_update/run.py @@ -0,0 +1,45 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown( + """ + # Animal Generator + Once you select a species, the detail panel should be visible. + """ + ) + + species = gr.Radio(label="Animal Class", choices=["Mammal", "Fish", "Bird"]) + animal = gr.Dropdown(label="Animal", choices=[]) + + with gr.Column(visible=False) as details_col: + weight = gr.Slider(0, 20) + details = gr.Textbox(label="Extra Details") + generate_btn = gr.Button("Generate") + output = gr.Textbox(label="Output") + + species_map = { + "Mammal": ["Elephant", "Giraffe", "Hamster"], + "Fish": ["Shark", "Salmon", "Tuna"], + "Bird": ["Chicken", "Eagle", "Hawk"], + } + + def filter_species(species): + return gr.Dropdown( + choices=species_map[species], value=species_map[species][1] + ), gr.Column(visible=True) + + species.change(filter_species, species, [animal, details_col]) + + def filter_weight(animal): + if animal in ("Elephant", "Shark", "Giraffe"): + return gr.Slider(maximum=100) + else: + return gr.Slider(maximum=20) + + animal.change(filter_weight, animal, weight) + weight.change(lambda w: gr.Textbox(lines=int(w / 10) + 1), weight, details) + + generate_btn.click(lambda x: x, details, output) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_webcam/run.py b/demo/blocks_webcam/run.py new file mode 100644 index 0000000..85b598f --- /dev/null +++ b/demo/blocks_webcam/run.py @@ -0,0 +1,11 @@ +import numpy as np + +import gradio as gr + +def snap(image): + return np.flipud(image) + +demo = gr.Interface(snap, gr.Image(sources=["webcam"]), "image") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/blocks_xray/run.py b/demo/blocks_xray/run.py new file mode 100644 index 0000000..0d674c5 --- /dev/null +++ b/demo/blocks_xray/run.py @@ -0,0 +1,59 @@ +import gradio as gr + +disease_values = [0.25, 0.5, 0.75] + +def xray_model(diseases, img): + return [{disease: disease_values[idx] for idx,disease in enumerate(diseases)}] + +def ct_model(diseases, img): + return [{disease: 0.1 for disease in diseases}] + +with gr.Blocks(fill_width=True) as demo: + gr.Markdown( + """ +# Detect Disease From Scan +With this model you can lorem ipsum +- ipsum 1 +- ipsum 2 +""" + ) + gr.DuplicateButton() + disease = gr.CheckboxGroup( + info="Select the diseases you want to scan for.", + choices=["Covid", "Malaria", "Lung Cancer"], label="Disease to Scan For" + ) + slider = gr.Slider(0, 100) + + with gr.Tab("X-ray") as x_tab: + with gr.Row(): + xray_scan = gr.Image() + xray_results = gr.JSON(show_indices=True) + xray_run = gr.Button("Run") + xray_run.click( + xray_model, + inputs=[disease, xray_scan], + outputs=xray_results, + api_name="xray_model" + ) + + with gr.Tab("CT Scan"): + with gr.Row(): + ct_scan = gr.Image() + ct_results = gr.JSON(show_indices=True) + ct_run = gr.Button("Run") + ct_run.click( + ct_model, + inputs=[disease, ct_scan], + outputs=ct_results, + api_name="ct_model" + ) + + upload_btn = gr.Button("Upload Results", variant="primary") + upload_btn.click( + lambda ct, xr: None, + inputs=[ct_results, xray_results], + outputs=[], + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/bokeh_plot/requirements.txt b/demo/bokeh_plot/requirements.txt new file mode 100644 index 0000000..8d941b8 --- /dev/null +++ b/demo/bokeh_plot/requirements.txt @@ -0,0 +1,2 @@ +bokeh>=3.0 +xyzservices \ No newline at end of file diff --git a/demo/bokeh_plot/run.py b/demo/bokeh_plot/run.py new file mode 100644 index 0000000..6264362 --- /dev/null +++ b/demo/bokeh_plot/run.py @@ -0,0 +1,90 @@ +# type: ignore +import gradio as gr +import xyzservices.providers as xyz +from bokeh.models import ColumnDataSource, Whisker +from bokeh.plotting import figure +from bokeh.sampledata.autompg2 import autompg2 as df +from bokeh.sampledata.penguins import data +from bokeh.transform import factor_cmap, jitter, factor_mark + +def get_plot(plot_type): + if plot_type == "map": + plot = figure( + x_range=(-2000000, 6000000), + y_range=(-1000000, 7000000), + x_axis_type="mercator", + y_axis_type="mercator", + ) + plot.add_tile(xyz.OpenStreetMap.Mapnik) # type: ignore + return plot + elif plot_type == "whisker": + classes = sorted(df["class"].unique()) + + p = figure( + height=400, + x_range=classes, + background_fill_color="#efefef", + title="Car class vs HWY mpg with quintile ranges", + ) + p.xgrid.grid_line_color = None + + g = df.groupby("class") + upper = g.hwy.quantile(0.80) + lower = g.hwy.quantile(0.20) + source = ColumnDataSource(data=dict(base=classes, upper=upper, lower=lower)) + + error = Whisker( + base="base", + upper="upper", + lower="lower", + source=source, + level="annotation", + line_width=2, + ) + error.upper_head.size = 20 + error.lower_head.size = 20 + p.add_layout(error) + + p.circle( + jitter("class", 0.3, range=p.x_range), + "hwy", + source=df, + alpha=0.5, + size=13, + line_color="white", + color=factor_cmap("class", "Light6", classes), + ) + return p + elif plot_type == "scatter": + + SPECIES = sorted(data.species.unique()) + MARKERS = ["hex", "circle_x", "triangle"] + + p = figure(title="Penguin size", background_fill_color="#fafafa") + p.xaxis.axis_label = "Flipper Length (mm)" + p.yaxis.axis_label = "Body Mass (g)" + + p.scatter( + "flipper_length_mm", + "body_mass_g", + source=data, + legend_group="species", + fill_alpha=0.4, + size=12, + marker=factor_mark("species", MARKERS, SPECIES), + color=factor_cmap("species", "Category10_3", SPECIES), + ) + + p.legend.location = "top_left" + p.legend.title = "Species" + return p + +with gr.Blocks() as demo: + with gr.Row(): + plot_type = gr.Radio(value="scatter", choices=["scatter", "whisker", "map"]) + plot = gr.Plot() + plot_type.change(get_plot, inputs=[plot_type], outputs=[plot]) + demo.load(get_plot, inputs=[plot_type], outputs=[plot]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/browser_state_component/run.py b/demo/browser_state_component/run.py new file mode 100644 index 0000000..20999a4 --- /dev/null +++ b/demo/browser_state_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.BrowserState() + +demo.launch() diff --git a/demo/browserstate/run.py b/demo/browserstate/run.py new file mode 100644 index 0000000..a150825 --- /dev/null +++ b/demo/browserstate/run.py @@ -0,0 +1,37 @@ +import random +import string +import gradio as gr +import time +with gr.Blocks() as demo: + gr.Markdown("Your Username and Password will get saved in the browser's local storage. " + "If you refresh the page, the values will be retained.") + username = gr.Textbox(label="Username") + password = gr.Textbox(label="Password", type="password") + btn = gr.Button("Generate Randomly") + local_storage = gr.BrowserState(["", ""]) + saved_message = gr.Markdown("✅ Saved to local storage", visible=False) + + @btn.click(outputs=[username, password]) + def generate_randomly(): + u = "".join(random.choices(string.ascii_letters + string.digits, k=10)) + p = "".join(random.choices(string.ascii_letters + string.digits, k=10)) + return u, p + + @demo.load(inputs=[local_storage], outputs=[username, password]) + def load_from_local_storage(saved_values): + print("loading from local storage", saved_values) + return saved_values[0], saved_values[1] + + @gr.on([username.change, password.change], inputs=[username, password], outputs=[local_storage]) + def save_to_local_storage(username, password): + return [username, password] + + @gr.on(local_storage.change, outputs=[saved_message]) + def show_saved_message(): + timestamp = time.strftime("%I:%M:%S %p") + return gr.Markdown( + f"✅ Saved to local storage at {timestamp}", + visible=True + ) + +demo.launch() diff --git a/demo/button_component/run.py b/demo/button_component/run.py new file mode 100644 index 0000000..88b0d22 --- /dev/null +++ b/demo/button_component/run.py @@ -0,0 +1,52 @@ +import gradio as gr + +icon = "https://cdn.icon-icons.com/icons2/2620/PNG/512/among_us_player_red_icon_156942.png" +with gr.Blocks() as demo: + with gr.Row(): + gr.Button(variant="primary") + gr.Button(variant="secondary") + gr.Button(variant="stop") + with gr.Row(): + gr.Button(variant="primary", size="sm") + gr.Button(variant="secondary", size="sm") + gr.Button(variant="stop", size="sm") + with gr.Row(): + gr.Button(variant="primary", icon=icon) + gr.Button(variant="secondary", icon=icon) + gr.Button(variant="stop", icon=icon) + + with gr.Row(): + gr.Button(variant="primary", size="sm", icon=icon) + gr.Button(variant="secondary", size="sm", icon=icon) + gr.Button(variant="stop", size="sm", icon=icon) + + with gr.Row(): + gr.Button(variant="primary", icon=icon, interactive=False) + gr.Button(variant="secondary", icon=icon, interactive=False) + gr.Button(variant="stop", icon=icon, interactive=False) + + with gr.Row(): + gr.Button(variant="primary", size="sm", icon=icon, interactive=False) + gr.Button(variant="secondary", size="sm", icon=icon, interactive=False) + gr.Button(variant="stop", size="sm", icon=icon, interactive=False) + + with gr.Row(): + gr.Button(variant="primary", interactive=False) + gr.Button(variant="secondary", interactive=False) + gr.Button(variant="stop", interactive=False) + + with gr.Row(): + gr.Button(variant="primary", size="sm", interactive=False) + gr.Button(variant="secondary", size="sm", interactive=False) + gr.Button(variant="stop", size="sm", interactive=False) + + with gr.Group(): + gr.Button(variant="primary") + gr.Button(variant="primary") + gr.Button(variant="secondary") + gr.Button(variant="secondary") + gr.Button(variant="stop") + gr.Button(variant="stop") + + +demo.launch() diff --git a/demo/cache_demo/run.py b/demo/cache_demo/run.py new file mode 100644 index 0000000..6294097 --- /dev/null +++ b/demo/cache_demo/run.py @@ -0,0 +1,187 @@ +"""Demo showcasing @gr.cache() with every function type.""" + +import asyncio +import tempfile +import time +import wave + +import numpy as np + +import gradio as gr + +CLASSES = ["cat", "dog", "bird", "fish", "car", "plane", "ship", "truck"] + + +@gr.cache +def classify_image(image): + time.sleep(2) + if image is None: + return {} + np.random.seed(int(image.mean()) % 100) + scores = np.random.dirichlet(np.ones(len(CLASSES))) + return {cls: float(s) for cls, s in zip(CLASSES, scores)} + + +TRANSLATIONS = { + "hello": "hola", + "goodbye": "adiós", + "thank you": "gracias", + "good morning": "buenos días", + "how are you": "¿cómo estás?", +} + + +@gr.cache +async def translate(text, target_language): + await asyncio.sleep(2) + if not text: + return "" + key = text.lower().strip() + if target_language == "Spanish": + return TRANSLATIONS.get(key, f"[translated to Spanish] {text}") + elif target_language == "French": + return f"[translated to French] {text}" + return text + + +RESPONSES = { + "hello": "Hi there! How can I help you today?", + "what is gradio": "Gradio is an open-source Python library for building ML demos.", + "what is caching": "Caching stores expensive results so they can be reused instantly.", + "tell me a joke": "Why do programmers prefer dark mode? Because light attracts bugs!", +} + + +def _message_plain_text(message): + content = message["content"] + if isinstance(content, str): + return content + if isinstance(content, list): + parts = [] + for part in content: + if isinstance(part, str): + parts.append(part) + elif isinstance(part, dict) and part.get("type") == "text": + parts.append(part.get("text", "")) + return "".join(parts) + return str(content) + + +@gr.cache +def chat_respond(history): + if not history: + yield history + return + user_text = _message_plain_text(history[-1]) + last_msg = user_text.lower().strip() + response = RESPONSES.get(last_msg, f"You said: '{user_text}'") + history.append({"role": "assistant", "content": ""}) + for i in range(len(response)): + history[-1]["content"] = response[: i + 1] + time.sleep(0.02) + yield history + + +def _make_wav_bytes(samples: np.ndarray, sample_rate: int = 24000) -> str: + pcm = (samples * 32767).astype(np.int16) + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: + with wave.open(f.name, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(sample_rate) + wf.writeframes(pcm.tobytes()) + return f.name + + +@gr.cache +def stream_audio(text, speed=1.0): + if not text: + return + sample_rate = 24000 + for word in text.split(): + time.sleep(0.5) + duration = max(0.2, len(word) * 0.08 / speed) + t = np.linspace(0, duration, int(sample_rate * duration), dtype=np.float32) + freq = 200 + (sum(ord(c) for c in word) % 300) + chunk = 0.3 * np.sin(2 * np.pi * freq * t) + fade = min(500, len(chunk) // 4) + chunk[:fade] *= np.linspace(0, 1, fade) + chunk[-fade:] *= np.linspace(1, 0, fade) + yield _make_wav_bytes(chunk, sample_rate) + + +@gr.cache +async def async_summarize(text): + if not text: + yield "" + return + words = text.split() + summary = "Summary: " + " ".join(words[: max(3, len(words) // 3)]) + "..." + result = "" + for char in summary: + await asyncio.sleep(0.03) + result += char + yield result + + +with gr.Blocks(title="gr.cache() Demo") as demo: + gr.Markdown( + "# `@gr.cache` Demo\n" + "Each tab shows a different function type. " + "**Submit the same input twice** — the second call replays from cache." + ) + + with gr.Tabs(): + with gr.Tab("Sync Function"): + gr.Markdown("Simulates image classification with a 2s delay.") + with gr.Row(): + img_in = gr.Image(type="numpy") + label_out = gr.Label(num_top_classes=5) + gr.Button("Classify").click(classify_image, img_in, label_out) + + with gr.Tab("Async Function"): + gr.Markdown("Simulates an async translation API with a 2s delay.") + trans_text = gr.Textbox(label="Text", value="hello") + trans_lang = gr.Dropdown( + choices=["Spanish", "French"], value="Spanish", label="Target" + ) + trans_out = gr.Textbox(label="Translation") + gr.Button("Translate").click( + translate, [trans_text, trans_lang], trans_out + ) + + with gr.Tab("Generator — Text"): + gr.Markdown("Streams a chatbot response char by char. All yields are replayed on cache hit.") + chatbot = gr.Chatbot() + chat_in = gr.Textbox(placeholder="Type a message...", show_label=False) + + def user_msg(msg, history): + history = history or [] + history.append({"role": "user", "content": msg}) + return "", history + + chat_in.submit( + user_msg, [chat_in, chatbot], [chat_in, chatbot] + ).then(chat_respond, chatbot, chatbot) + + with gr.Tab("Generator — Streaming Audio"): + gr.Markdown("Simulates streaming TTS. Each word is a separate audio chunk, all replayed on hit.") + audio_text = gr.Textbox(label="Text", value="Hello world this is cached") + audio_speed = gr.Slider(0.5, 2.0, value=1.0, step=0.1, label="Speed") + audio_out = gr.Audio(label="Output", streaming=True, autoplay=True) + gr.Button("Synthesize").click( + stream_audio, [audio_text, audio_speed], audio_out + ) + + with gr.Tab("Async Generator"): + gr.Markdown("Simulates async streaming summarization. All yields replayed on hit.") + summ_in = gr.Textbox( + label="Text to summarize", + value="The quick brown fox jumps over the lazy dog and runs through the forest", + lines=3, + ) + summ_out = gr.Textbox(label="Summary", lines=2) + gr.Button("Summarize").click(async_summarize, summ_in, summ_out) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/cache_intermediate_demo/run.py b/demo/cache_intermediate_demo/run.py new file mode 100644 index 0000000..baa4144 --- /dev/null +++ b/demo/cache_intermediate_demo/run.py @@ -0,0 +1,155 @@ +"""Demo showcasing runtime gr.cache(fn)(...) for an intermediate helper call.""" + +import re +import time + +import gradio as gr + +POLICIES = { + "Billing": [ + { + "title": "Refund windows", + "text": "Customers can request a refund within 30 days of purchase. " + "Annual subscriptions can be prorated if the request arrives after the " + "first 30 days but before day 90.", + }, + { + "title": "Invoice corrections", + "text": "Billing agents can correct invoice email addresses, company " + "names, and tax identifiers, but cannot alter the purchase date.", + }, + { + "title": "Duplicate charges", + "text": "If two charges appear within 24 hours for the same plan and " + "customer account, billing should confirm card fingerprint and issue a " + "same-day reversal.", + }, + ], + "IT": [ + { + "title": "Password resets", + "text": "IT can revoke all active sessions and force a password reset " + "after verifying the employee through the HR directory.", + }, + { + "title": "VPN access", + "text": "New VPN access is approved only for employees with manager " + "approval and a registered MFA device.", + }, + { + "title": "Laptop replacement", + "text": "Broken laptops should be tagged with the device asset number " + "and shipped to the repair center before a replacement is issued.", + }, + ], + "HR": [ + { + "title": "Parental leave", + "text": "Full-time employees are eligible for 16 weeks of paid parental " + "leave after six months of employment.", + }, + { + "title": "Address changes", + "text": "Employees should update their home address in the HR portal " + "before payroll closes on the 20th of each month.", + }, + { + "title": "Interview scheduling", + "text": "Recruiters should provide interview panels at least 48 hours " + "to review candidate materials before the session starts.", + }, + ], +} + +TONE_PREFIX = { + "Concise": "Give a short answer.", + "Friendly": "Give a warm, helpful answer.", + "Formal": "Give a professional and policy-focused answer.", +} + + +def _tokens(text: str) -> set[str]: + return set(re.findall(r"[a-z0-9]+", text.lower())) + + +def retrieve_passages(question: str, team: str) -> list[dict[str, str | int]]: + """Pretend retrieval is the expensive deterministic step.""" + time.sleep(2) + query_tokens = _tokens(question) + ranked = [] + for doc in POLICIES[team]: + combined_text = f"{doc['title']} {doc['text']}" + score = len(query_tokens & _tokens(combined_text)) + ranked.append( + { + "title": doc["title"], + "text": doc["text"], + "score": score, + } + ) + + ranked.sort(key=lambda item: item["score"], reverse=True) + return ranked[:2] + + +def draft_answer(question: str, team: str, tone: str) -> tuple[str, str, str]: + start = time.time() + + passages = gr.cache(retrieve_passages)(question, team) + top_match = passages[0] + bullets = "\n".join( + f"- {match['title']}: {match['text']}" for match in passages + ) + + answer = ( + f"{TONE_PREFIX[tone]}\n\n" + f"For a {team.lower()} request about '{question}', the strongest match is " + f"**{top_match['title']}**.\n\n" + f"Suggested reply: Based on the current {team.lower()} policy, {top_match['text']}" + ) + debug = ( + f"Retrieved {len(passages)} passages in {time.time() - start:.2f}s.\n" + "Try the same question twice, or change only the tone. " + "The retrieval helper should be reused from cache." + ) + return answer, bullets, debug + + +with gr.Blocks(title="Intermediate gr.cache() Demo") as demo: + gr.Markdown( + "# Intermediate `gr.cache()` Demo\n" + "This simulates a support assistant where the **retrieval** step is expensive " + "but deterministic, while the final answer still recomputes. " + "Submit the same question twice, or change only the tone, and watch Gradio " + "show the `used cache` badge when the cached helper is reused." + ) + + with gr.Row(): + with gr.Column(): + team = gr.Dropdown( + choices=list(POLICIES.keys()), + value="Billing", + label="Team", + ) + tone = gr.Dropdown( + choices=["Concise", "Friendly", "Formal"], + value="Friendly", + label="Answer style", + ) + question = gr.Textbox( + label="Customer question", + lines=3, + value="Can this customer get a refund after being charged twice?", + ) + draft = gr.Markdown() + retrieved = gr.Markdown() + debug = gr.Textbox(label="Debug", lines=3) + gr.Button("Draft reply").click( + draft_answer, + [question, team, tone], + outputs=[draft, retrieved, debug], + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/cache_kv_demo/run.py b/demo/cache_kv_demo/run.py new file mode 100644 index 0000000..95283e1 --- /dev/null +++ b/demo/cache_kv_demo/run.py @@ -0,0 +1,88 @@ +"""Demo: KV caching with gr.cache() and a transformers model. + +Uses gr.cache() as an injectable parameter to manually store and retrieve +transformer KV caches, reusing computation for shared prompt prefixes. +""" + +import time + +import gradio as gr +import torch +from transformers import AutoModelForCausalLM, AutoTokenizer + +MODEL_NAME = "HuggingFaceTB/SmolLM2-135M-Instruct" + +tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) +model = AutoModelForCausalLM.from_pretrained( + MODEL_NAME, torch_dtype=torch.float32, device_map="cpu" +) + + +def _find_longest_prefix(prompt: str, c: gr.Cache) -> tuple[str | None, int]: + best_key = None + best_len = 0 + for cached_key in c.keys(): + if prompt.startswith(cached_key) and best_len < len(cached_key) < len(prompt): + best_key = cached_key + best_len = len(cached_key) + return best_key, best_len + + +def generate(prompt, max_new_tokens, c=gr.Cache()): + start = time.time() + + best_prefix, prefix_len = _find_longest_prefix(prompt, c) + if best_prefix: + past = c.get(best_prefix)["kv"] # type: ignore + input_ids = tokenizer.encode(prompt, return_tensors="pt") + prefix_token_len = tokenizer.encode(best_prefix, return_tensors="pt").shape[1] + new_input_ids = input_ids[:, prefix_token_len:] + else: + past = None + new_input_ids = tokenizer.encode(prompt, return_tensors="pt") + prefix_token_len = 0 + + with torch.no_grad(): + outputs = model(new_input_ids, past_key_values=past, use_cache=True) + full_past = outputs.past_key_values + + gen_input = outputs.logits[:, -1:, :].argmax(dim=-1) + generated_tokens = [gen_input.item()] + current_past = full_past + + for _ in range(int(max_new_tokens) - 1): + with torch.no_grad(): + outputs = model(gen_input, past_key_values=current_past, use_cache=True) + current_past = outputs.past_key_values + gen_input = outputs.logits[:, -1:, :].argmax(dim=-1) + generated_tokens.append(gen_input.item()) + if gen_input.item() == tokenizer.eos_token_id: + break + + c.set(prompt, kv=full_past) + + elapsed = time.time() - start + generated_text = tokenizer.decode(generated_tokens, skip_special_tokens=True) + total_tokens = tokenizer.encode(prompt, return_tensors="pt").shape[1] + status = ( + f"Reused {prefix_token_len}/{total_tokens} prompt tokens from KV cache | " + f"Generated {len(generated_tokens)} tokens in {elapsed:.2f}s" + ) + return prompt + generated_text, status + + +with gr.Blocks(title="KV Cache Demo") as demo: + gr.Markdown( + "# KV Cache Demo with `gr.cache()`\n" + "Uses `gr.cache()` as an injectable parameter to store transformer KV caches. " + "When you extend a prompt, the cached KV states from the prefix are reused.\n\n" + "Try: **'The quick brown'** → then **'The quick brown fox'**" + ) + prompt = gr.Textbox(label="Prompt", value="The quick brown") + max_tokens = gr.Slider(10, 100, value=30, step=5, label="Max new tokens") + output = gr.Textbox(label="Generated text", lines=4) + status = gr.Textbox(label="Cache status") + gr.Button("Generate").click(generate, [prompt, max_tokens], [output, status]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/cache_manual_demo/run.py b/demo/cache_manual_demo/run.py new file mode 100644 index 0000000..2b93fc5 --- /dev/null +++ b/demo/cache_manual_demo/run.py @@ -0,0 +1,56 @@ +"""Demo showcasing simple manual caching with gr.Cache().""" + +import time + +import gradio as gr + +WEATHER_BY_CITY = { + "san francisco": ("Foggy", 61), + "new york": ("Cloudy", 72), + "tokyo": ("Sunny", 78), + "london": ("Rainy", 58), + "nairobi": ("Clear", 75), +} + + +def normalize_city(city: str) -> str: + return " ".join(city.lower().strip().split()) + + +def lookup_weather(city: str, c=gr.Cache()): + if not city.strip(): + return "", "Enter a city name.", "" + + cache_key = normalize_city(city) + cached = c.get(cache_key) + if cached is not None: + return cached["forecast"], "Cache hit", cache_key + + time.sleep(2) + condition, temperature = WEATHER_BY_CITY.get(cache_key, ("Windy", 68)) + forecast = ( + f"{city.strip()}: {condition}, {temperature} degF.\n" + f"Normalized cache key: {cache_key}" + ) + c.set(cache_key, forecast=forecast) + return forecast, "Computed and stored", cache_key + + +with gr.Blocks(title="gr.Cache() Demo") as demo: + gr.Markdown( + "# `gr.Cache()` Demo\n" + "This demo manually caches a normalized city lookup. " + "Try the same city twice, or vary capitalization and spacing " + "to reuse the same cached result." + ) + + city = gr.Textbox(label="City", value=" San Francisco ") + forecast = gr.Textbox(label="Forecast", lines=3) + status = gr.Textbox(label="Status") + cache_key = gr.Textbox(label="Cache key used") + + gr.Button("Lookup").click(lookup_weather, city, [forecast, status, cache_key]) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/calculator/examples/log.csv b/demo/calculator/examples/log.csv new file mode 100644 index 0000000..e69de29 diff --git a/demo/calculator/run.py b/demo/calculator/run.py new file mode 100644 index 0000000..1e672b5 --- /dev/null +++ b/demo/calculator/run.py @@ -0,0 +1,35 @@ +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" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/calculator/screenshot.gif b/demo/calculator/screenshot.gif new file mode 100644 index 0000000..84ff86c Binary files /dev/null and b/demo/calculator/screenshot.gif differ diff --git a/demo/calculator_blocks/run.py b/demo/calculator_blocks/run.py new file mode 100644 index 0000000..ac34a07 --- /dev/null +++ b/demo/calculator_blocks/run.py @@ -0,0 +1,37 @@ +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": + return num1 / num2 + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + num_1 = gr.Number(value=4) + operation = gr.Radio(["add", "subtract", "multiply", "divide"]) + num_2 = gr.Number(value=0) + submit_btn = gr.Button(value="Calculate") + with gr.Column(): + result = gr.Number() + + submit_btn.click( + calculator, inputs=[num_1, operation, num_2], outputs=[result], api_visibility="private" + ) + examples = gr.Examples( + examples=[ + [5, "add", 3], + [4, "divide", 2], + [-4, "multiply", 2.5], + [0, "subtract", 1.2], + ], + inputs=[num_1, operation, num_2], + ) + +if __name__ == "__main__": + demo.launch(footer_links=["gradio"]) diff --git a/demo/calculator_blocks_cached/run.py b/demo/calculator_blocks_cached/run.py new file mode 100644 index 0000000..f0fa341 --- /dev/null +++ b/demo/calculator_blocks_cached/run.py @@ -0,0 +1,34 @@ +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": + return num1 / num2 + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + num_1 = gr.Number() + operation = gr.Radio(["add", "subtract", "multiply", "divide"]) + num_2 = gr.Number() + submit_btn = gr.Button(value="Calculate") + with gr.Column(): + result = gr.Number() + + submit_btn.click(calculator, inputs=[num_1, operation, num_2], outputs=[result]) + examples = gr.Examples(examples=[[5, "add", 3], + [4, "divide", 2], + [-4, "multiply", 2.5], + [0, "subtract", 1.2]], + inputs=[num_1, operation, num_2], + outputs=[result], + fn=calculator, + cache_examples=True) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/calculator_list_and_dict/run.py b/demo/calculator_list_and_dict/run.py new file mode 100644 index 0000000..7188e2e --- /dev/null +++ b/demo/calculator_list_and_dict/run.py @@ -0,0 +1,20 @@ +import gradio as gr + +with gr.Blocks() as demo: + a = gr.Number(label="a") + b = gr.Number(label="b") + with gr.Row(): + add_btn = gr.Button("Add") + sub_btn = gr.Button("Subtract") + c = gr.Number(label="sum") + + def add(num1, num2): + return num1 + num2 + add_btn.click(add, inputs=[a, b], outputs=c) + + def sub(data): + return data[a] - data[b] + sub_btn.click(sub, inputs={a, b}, outputs=c) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/calculator_live/run.py b/demo/calculator_live/run.py new file mode 100644 index 0000000..09856c0 --- /dev/null +++ b/demo/calculator_live/run.py @@ -0,0 +1,24 @@ +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": + return num1 / num2 + +demo = gr.Interface( + calculator, + [ + "number", + gr.Radio(["add", "subtract", "multiply", "divide"]), + "number" + ], + "number", + live=True, +) +if __name__ == "__main__": + demo.launch() diff --git a/demo/calculator_live/screenshot.gif b/demo/calculator_live/screenshot.gif new file mode 100644 index 0000000..b6df1ce Binary files /dev/null and b/demo/calculator_live/screenshot.gif differ diff --git a/demo/cancel_events/run.py b/demo/cancel_events/run.py new file mode 100644 index 0000000..5ea06e8 --- /dev/null +++ b/demo/cancel_events/run.py @@ -0,0 +1,80 @@ +import time +import gradio as gr +import atexit +import pathlib + +log_file = pathlib.Path(__file__).parent / "cancel_events_output_log.txt" + + +def fake_diffusion(steps): + log_file.write_text("") + for i in range(steps): + print(f"Current step: {i}") + with log_file.open("a") as f: + f.write(f"Current step: {i}\n") + time.sleep(0.2) + yield str(i) + + +def long_prediction(*args, **kwargs): + time.sleep(4) + return 42, 42 + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + n = gr.Slider(1, 10, value=9, step=1, label="Number Steps") + run = gr.Button(value="Start Iterating") + output = gr.Textbox(label="Iterative Output") + stop = gr.Button(value="Stop Iterating") + with gr.Column(): + textbox = gr.Textbox(label="Prompt") + loading_box = gr.Textbox( + label="Loading indicator for expensive calculation" + ) + loading_box2 = gr.Textbox( + label="Loading indicator for expensive calculation" + ) + prediction = gr.Number(label="Expensive Calculation") + prediction2 = gr.Number(label="Expensive Calculation") + run_pred = gr.Button(value="Run Expensive Calculation") + with gr.Column(): + cancel_on_change = gr.Textbox( + label="Cancel Iteration and Expensive Calculation on Change" + ) + cancel_on_submit = gr.Textbox( + label="Cancel Iteration and Expensive Calculation on Submit" + ) + echo = gr.Textbox(label="Echo") + with gr.Row(): + with gr.Column(): + image = gr.Image( + sources=["webcam"], label="Cancel on clear", interactive=True + ) + with gr.Column(): + video = gr.Video( + sources=["webcam"], label="Cancel on start recording", interactive=True + ) + + click_event = run.click(fake_diffusion, n, output) + stop.click(fn=None, inputs=None, outputs=None, cancels=[click_event]) + pred_event = run_pred.click( + fn=long_prediction, + inputs=[textbox], + outputs=[prediction, prediction2], + show_progress_on=[loading_box, loading_box2], + ) + + cancel_on_change.change(None, None, None, cancels=[click_event, pred_event]) + cancel_on_submit.submit( + lambda s: s, cancel_on_submit, echo, cancels=[click_event, pred_event] + ) + image.clear(None, None, None, cancels=[click_event, pred_event]) + video.start_recording(None, None, None, cancels=[click_event, pred_event]) + + demo.queue(max_size=20) + atexit.register(lambda: log_file.unlink(True)) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/change_vs_input/files/world.mp4 b/demo/change_vs_input/files/world.mp4 new file mode 100644 index 0000000..efdda67 Binary files /dev/null and b/demo/change_vs_input/files/world.mp4 differ diff --git a/demo/change_vs_input/run.py b/demo/change_vs_input/run.py new file mode 100644 index 0000000..351c54c --- /dev/null +++ b/demo/change_vs_input/run.py @@ -0,0 +1,170 @@ +import gradio as gr +from gradio.media import get_image, get_audio, get_video, MEDIA_PATHS + + +with gr.Blocks() as demo: + set_button = gr.Button("Set Values") + counter = gr.Number(label="Change counter") + with gr.Row(): + gr.Markdown("# Enter Here") + gr.Markdown("# ON:INPUT/UPLOAD") + gr.Markdown("# ON:CHANGE") + gr.Markdown("# ON:CHANGE x2") + with gr.Row(): + text = gr.Textbox(label="TB Input") + text_in = gr.Textbox(label="Textbox Input Event") + text_ch = gr.Textbox(label="Textbox Change Event") + text_ch2 = gr.Textbox(label="Textbox Change Event x2") + with gr.Row(): + num = gr.Number() + num_in = gr.Number() + num_ch = gr.Number() + num_ch2 = gr.Number() + with gr.Row(): + slider = gr.Slider() + slider_in = gr.Slider() + slider_ch = gr.Slider() + slider_ch2 = gr.Slider() + with gr.Row(): + checkbox = gr.Checkbox(label="CB Input") + checkbox_in = gr.Checkbox(label="Checkbox Input Event") + checkbox_ch = gr.Checkbox(label="Checkbox Change Event") + checkbox_ch2 = gr.Checkbox(label="Checkbox Change Event x2") + with gr.Row(): + checkbox_group = gr.CheckboxGroup(["a", "b", "c"], label="CBG Input") + checkbox_group_in = gr.CheckboxGroup(["a", "b", "c"], label="CheckboxGroup Input Event") + checkbox_group_ch = gr.CheckboxGroup(["a", "b", "c"], label="CheckboxGroup Change Event") + checkbox_group_ch2 = gr.CheckboxGroup(["a", "b", "c"], label="CheckboxGroup Change Event x2") + with gr.Row(): + radio = gr.Radio(["a", "b", "c"], label="Radio Input") + radio_in = gr.Radio(["a", "b", "c"], label="Radio Input Event") + radio_ch = gr.Radio(["a", "b", "c"], label="Radio Change Event") + radio_ch2 = gr.Radio(["a", "b", "c"], label="Radio Change Event x2") + with gr.Row(): + dropdown = gr.Dropdown(["a", "b", "c"], label="DD Input") + dropdown_in = gr.Dropdown(["a", "b", "c"], label="Dropdown Input Event") + dropdown_ch = gr.Dropdown(["a", "b", "c"], label="Dropdown Change Event") + dropdown_ch2 = gr.Dropdown(["a", "b", "c"], label="Dropdown Change Event x2") + with gr.Row(): + colorpicker = gr.ColorPicker() + colorpicker_in = gr.ColorPicker() + colorpicker_ch = gr.ColorPicker() + colorpicker_ch2 = gr.ColorPicker() + with gr.Row(): + code = gr.Code() + code_in = gr.Code() + code_ch = gr.Code() + code_ch2 = gr.Code() + with gr.Row(): + dataframe = gr.Dataframe() + dataframe_in = gr.Dataframe() + dataframe_ch = gr.Dataframe() + dataframe_ch2 = gr.Dataframe() + with gr.Row(): + image = gr.Image(elem_id="image-original") + image_up = gr.Image(elem_id="image-upload") + image_ch = gr.Image(elem_id="image-change") + image_ch2 = gr.Image(elem_id="image-change-2") + with gr.Row(): + audio = gr.Audio(elem_id="audio-original") + audio_up = gr.Audio(elem_id="audio-upload") + audio_ch = gr.Audio(elem_id="audio-change") + audio_ch2 = gr.Audio(elem_id="audio-change-2") + with gr.Row(): + video = gr.Video(elem_id="video-original") + video_up = gr.Video(elem_id="video-upload") + video_ch = gr.Video(elem_id="video-change") + video_ch2 = gr.Video(elem_id="video-change-2") + + lion = get_image("lion.jpg") + cantina = get_audio("cantina.wav") + world = get_video("world.mp4") + + set_button.click( + lambda: [ + "asdf", + 555, + 12, + True, + ["a", "c"], + "b", + "b", + "#FF0000", + "import gradio as gr", + [["a", "b", "c", "d"], ["1", "2", "3", "4"]], + lion, + cantina, + world, + ], + None, + [ + text, + num, + slider, + checkbox, + checkbox_group, + radio, + dropdown, + colorpicker, + code, + dataframe, + image, + audio, + video, + ], + ) + + text.input(lambda x: x, text, text_in) + num.input(lambda x: x, num, num_in) + slider.input(lambda x: x, slider, slider_in) + checkbox.input(lambda x: x, checkbox, checkbox_in) + checkbox_group.input(lambda x: x, checkbox_group, checkbox_group_in) + radio.input(lambda x: x, radio, radio_in) + dropdown.input(lambda x: x, dropdown, dropdown_in) + colorpicker.input(lambda x: x, colorpicker, colorpicker_in) + code.input(lambda x: x, code, code_in) + dataframe.input(lambda x: x, dataframe, dataframe_in) + image.upload(lambda x: x, image, image_up) + audio.upload(lambda x: x, audio, audio_up) + video.upload(lambda x: x, video, video_up) + + text.change(lambda x, y: (x, y + 1), [text, counter], [text_ch, counter]) + num.change(lambda x, y: (x, y + 1), [num, counter], [num_ch, counter]) + slider.change(lambda x, y: (x, y + 1), [slider, counter], [slider_ch, counter]) + checkbox.change( + lambda x, y: (x, y + 1), [checkbox, counter], [checkbox_ch, counter] + ) + checkbox_group.change( + lambda x, y: (x, y + 1), [checkbox_group, counter], [checkbox_group_ch, counter] + ) + radio.change(lambda x, y: (x, y + 1), [radio, counter], [radio_ch, counter]) + dropdown.change( + lambda x, y: (x, y + 1), [dropdown, counter], [dropdown_ch, counter] + ) + colorpicker.change( + lambda x, y: (x, y + 1), [colorpicker, counter], [colorpicker_ch, counter] + ) + code.change(lambda x, y: (x, y + 1), [code, counter], [code_ch, counter]) + dataframe.change( + lambda x, y: (x, y + 1), [dataframe, counter], [dataframe_ch, counter] + ) + image.change(lambda x, y: (x, y + 1), [image, counter], [image_ch, counter]) + audio.change(lambda x, y: (x, y + 1), [audio, counter], [audio_ch, counter]) + video.change(lambda x, y: (x, y + 1), [video, counter], [video_ch, counter]) + + text_ch.change(lambda x: x, text_ch, text_ch2) + num_ch.change(lambda x: x, num_ch, num_ch2) + slider_ch.change(lambda x: x, slider_ch, slider_ch2) + checkbox_ch.change(lambda x: x, checkbox_ch, checkbox_ch2) + checkbox_group_ch.change(lambda x: x, checkbox_group_ch, checkbox_group_ch2) + radio_ch.change(lambda x: x, radio_ch, radio_ch2) + dropdown_ch.change(lambda x: x, dropdown_ch, dropdown_ch2) + colorpicker_ch.change(lambda x: x, colorpicker_ch, colorpicker_ch2) + code_ch.change(lambda x: x, code_ch, code_ch2) + dataframe_ch.change(lambda x: x, dataframe_ch, dataframe_ch2) + image_ch.change(lambda x: x, image_ch, image_ch2) + audio_ch.change(lambda x: x, audio_ch, audio_ch2) + video_ch.change(lambda x: x, video_ch, video_ch2) + +if __name__ == "__main__": + demo.launch(allowed_paths=MEDIA_PATHS) diff --git a/demo/chatbot_component/run.py b/demo/chatbot_component/run.py new file mode 100644 index 0000000..d57294f --- /dev/null +++ b/demo/chatbot_component/run.py @@ -0,0 +1,13 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Chatbot(value=[ # type: ignore + {"role": "user", "content": "Hello World"}, + {"role": "assistant", "content": "Hey Gradio!"}, + {"role": "user", "content": "❤️"}, + {"role": "assistant", "content": "😍"}, + {"role": "user", "content": "🔥"}, + {"role": "assistant", "content": "🤗"} + ]) + +demo.launch() diff --git a/demo/chatbot_consecutive/run.py b/demo/chatbot_consecutive/run.py new file mode 100644 index 0000000..495886b --- /dev/null +++ b/demo/chatbot_consecutive/run.py @@ -0,0 +1,25 @@ +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): + return "", history + [{"role": "user", "content": user_message}] + + def bot(history): + bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"]) + time.sleep(2) + history.append({"role": "assistant", "content": bot_message}) + return history + + msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then( + bot, chatbot, chatbot + ) + clear.click(lambda: None, None, chatbot, queue=False) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_core_components/files/sample.txt b/demo/chatbot_core_components/files/sample.txt new file mode 100644 index 0000000..11f15e4 --- /dev/null +++ b/demo/chatbot_core_components/files/sample.txt @@ -0,0 +1 @@ +hello friends \ No newline at end of file diff --git a/demo/chatbot_core_components/requirements.txt b/demo/chatbot_core_components/requirements.txt new file mode 100644 index 0000000..2f15c45 --- /dev/null +++ b/demo/chatbot_core_components/requirements.txt @@ -0,0 +1,4 @@ +plotly +numpy +pandas +matplotlib diff --git a/demo/chatbot_core_components/run.py b/demo/chatbot_core_components/run.py new file mode 100644 index 0000000..d3528e9 --- /dev/null +++ b/demo/chatbot_core_components/run.py @@ -0,0 +1,261 @@ +# type: ignore +import gradio as gr +import os +import plotly.express as px # type: ignore +import random + +# Chatbot demo with multimodal input (text, markdown, LaTeX, code blocks, image, audio, video, & model3d). Plus shows support for streaming text. + +txt = """ +Absolutely! The mycorrhizal network, often referred to as the "Wood Wide Web," is a symbiotic association between fungi and the roots of most plant species. Here’s a deeper dive into how it works and its implications: + +### How It Works + +1. **Symbiosis**: Mycorrhizal fungi attach to plant roots, extending far into the soil. The plant provides the fungi with carbohydrates produced via photosynthesis. In return, the fungi help the plant absorb water and essential nutrients like phosphorus and nitrogen from the soil. + +2. **Network Formation**: The fungal hyphae (thread-like structures) connect individual plants, creating an extensive underground network. This network can link many plants together, sometimes spanning entire forests. + +3. **Communication**: Trees and plants use this network to communicate and share resources. For example, a tree under attack by pests can send chemical signals through the mycorrhizal network to warn neighboring trees. These trees can then produce defensive chemicals to prepare for the impending threat. + +### Benefits and Functions + +1. **Resource Sharing**: The network allows for the redistribution of resources among plants. For instance, a large, established tree might share excess nutrients and water with smaller, younger trees, promoting overall forest health. + +2. **Defense Mechanism**: The ability to share information about pests and diseases enhances the resilience of plant communities. This early warning system helps plants activate their defenses before they are directly affected. + +3. **Support for Seedlings**: Young seedlings, which have limited root systems, benefit immensely from the mycorrhizal network. They receive nutrients and water from larger plants, increasing their chances of survival and growth. + +### Ecological Impact + +1. **Biodiversity**: The mycorrhizal network supports biodiversity by fostering a cooperative environment. Plants of different species can coexist and thrive because of the shared resources and information. + +2. **Forest Health**: The network enhances the overall health of forests. By enabling efficient nutrient cycling and supporting plant defenses, it contributes to the stability and longevity of forest ecosystems. + +3. **Climate Change Mitigation**: Healthy forests act as significant carbon sinks, absorbing carbon dioxide from the atmosphere. The mycorrhizal network plays a critical role in maintaining forest health and, consequently, in mitigating climate change. + +### Research and Discoveries + +1. **Suzanne Simard's Work**: Ecologist Suzanne Simard’s research has been pivotal in uncovering the complexities of the mycorrhizal network. She demonstrated that trees of different species can share resources and that "mother trees" (large, older trees) play a crucial role in nurturing younger plants. + +2. **Implications for Conservation**: Understanding the mycorrhizal network has significant implications for conservation efforts. It highlights the importance of preserving not just individual trees but entire ecosystems, including the fungal networks that sustain them. + +### Practical Applications + +1. **Agriculture**: Farmers and horticulturists are exploring the use of mycorrhizal fungi to improve crop yields and soil health. By incorporating these fungi into agricultural practices, they can reduce the need for chemical fertilizers and enhance plant resilience. + +2. **Reforestation**: In reforestation projects, introducing mycorrhizal fungi can accelerate the recovery of degraded lands. The fungi help establish healthy plant communities, ensuring the success of newly planted trees. + +The "Wood Wide Web" exemplifies the intricate and often hidden connections that sustain life on Earth. It’s a reminder of the profound interdependence within natural systems and the importance of preserving these delicate relationships. +""" + +def random_plot(): + df = px.data.iris() + fig = px.scatter( + df, + x="sepal_width", + y="sepal_length", + color="species", + size="petal_length", + hover_data=["petal_width"], + ) + return fig + +color_map = { + "harmful": "crimson", + "neutral": "gray", + "beneficial": "green", +} + +def html_src(harm_level): + return f""" +
+
+ {harm_level} +
+
+""" + +def print_like_dislike(x: gr.LikeData): + print(x.index, x.value, x.liked) + +def random_bokeh_plot(): + from bokeh.models import ColumnDataSource, Whisker + from bokeh.plotting import figure + from bokeh.sampledata.autompg2 import autompg2 as df + from bokeh.transform import factor_cmap, jitter + + classes = sorted(df["class"].unique()) + + p = figure( + height=400, + x_range=classes, + background_fill_color="#efefef", + title="Car class vs HWY mpg with quintile ranges", + ) + p.xgrid.grid_line_color = None + + g = df.groupby("class") + upper = g.hwy.quantile(0.80) + lower = g.hwy.quantile(0.20) + source = ColumnDataSource(data=dict(base=classes, upper=upper, lower=lower)) + + error = Whisker( + base="base", + upper="upper", + lower="lower", + source=source, + level="annotation", + line_width=2, + ) + error.upper_head.size = 20 + error.lower_head.size = 20 + p.add_layout(error) + + p.circle( + jitter("class", 0.3, range=p.x_range), + "hwy", + source=df, + alpha=0.5, + size=13, + line_color="white", + color=factor_cmap("class", "Light6", classes), + ) + return p + +# get_file(), get_image(), get_model3d(), get_video() return file paths to sample media included with Gradio +from gradio.media import get_file, get_image, get_model3d, get_video + +def random_matplotlib_plot(): + import numpy as np + import pandas as pd + import matplotlib.pyplot as plt + + countries = ["USA", "Canada", "Mexico", "UK"] + months = ["January", "February", "March", "April", "May"] + m = months.index("January") + r = 3.2 + start_day = 30 * m + final_day = 30 * (m + 1) + x = np.arange(start_day, final_day + 1) + pop_count = {"USA": 350, "Canada": 40, "Mexico": 300, "UK": 120} + df = pd.DataFrame({"day": x}) + for country in countries: + df[country] = x ** (r) * (pop_count[country] + 1) + + fig = plt.figure() + plt.plot(df["day"], df[countries].to_numpy()) + plt.title("Outbreak in " + "January") + plt.ylabel("Cases") + plt.xlabel("Days since Day 0") + plt.legend(countries) + return fig + +def add_message(history, message): + for x in message["files"]: + history.append({"role": "user", "content": {"path": x}}) + if message["text"] is not None: + history.append({"role": "user", "content": message["text"]}) + return history, gr.MultimodalTextbox(value=None, interactive=False) + +def bot(history, response_type): + msg = {"role": "assistant", "content": ""} + if response_type == "plot": + content = gr.Plot(random_plot()) + elif response_type == "bokeh_plot": + content = gr.Plot(random_bokeh_plot()) + elif response_type == "matplotlib_plot": + content = gr.Plot(random_matplotlib_plot()) + elif response_type == "gallery": + content = gr.Gallery( + [get_image("avatar.png"), get_image("avatar.png")] + ) + elif response_type == "dataframe": + content = gr.Dataframe( + interactive=True, + headers=["One", "Two", "Three"], + col_count=(3, "fixed"), + row_count=(3, "fixed"), + value=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], + label="Dataframe", + ) + elif response_type == "image": + content = gr.Image(get_image("avatar.png")) + elif response_type == "video": + content = gr.Video(get_video("world.mp4")) + elif response_type == "audio": + content = gr.Audio(os.path.join("files", "audio.wav")) + elif response_type == "audio_file": + content = {"path": os.path.join("files", "audio.wav"), "alt_text": "description"} + elif response_type == "image_file": + content = {"path": get_image("avatar.png"), "alt_text": "description"} + elif response_type == "video_file": + content = {"path": get_video("world.mp4"), "alt_text": "description"} + elif response_type == "txt_file": + content = {"path": get_file("sample.txt"), "alt_text": "description"} + elif response_type == "model3d_file": + content = {"path": get_model3d("Duck.glb"), "alt_text": "description"} + elif response_type == "html": + content = gr.HTML( + html_src(random.choice(["harmful", "neutral", "beneficial"])) + ) + elif response_type == "model3d": + content = gr.Model3D(get_model3d("Duck.glb")) + else: + content = txt + msg["content"] = content # type: ignore + history.append(msg) + return history + +fig = random_plot() + +with gr.Blocks(fill_height=True) as demo: + chatbot = gr.Chatbot( + elem_id="chatbot", + scale=1, + buttons=["copy"], + avatar_images=( + None, + get_image("avatar.png"), + ), + ) + response_type = gr.Radio( + [ + "audio_file", + "image_file", + "video_file", + "txt_file", + "model3d_file", + "plot", + "matplotlib_plot", + "bokeh_plot", + "image", + "text", + "gallery", + "dataframe", + "video", + "audio", + "html", + "model3d", + ], + value="text", + label="Response Type", + ) + + chat_input = gr.MultimodalTextbox( + interactive=True, + placeholder="Enter message or upload file...", + show_label=False, + ) + + chat_msg = chat_input.submit( + add_message, [chatbot, chat_input], [chatbot, chat_input] + ) + bot_msg = chat_msg.then( + bot, [chatbot, response_type], chatbot, api_name="bot_response" + ) + bot_msg.then(lambda: gr.MultimodalTextbox(interactive=True), None, [chat_input]) + + chatbot.like(print_like_dislike, None, None) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_core_components_simple/run.py b/demo/chatbot_core_components_simple/run.py new file mode 100644 index 0000000..c93e3a2 --- /dev/null +++ b/demo/chatbot_core_components_simple/run.py @@ -0,0 +1,106 @@ +import gradio as gr +import random + +# Chatbot demo with multimodal input (text, markdown, LaTeX, code blocks, image, audio, & video). Plus shows support for streaming text. + +color_map = { + "harmful": "crimson", + "neutral": "gray", + "beneficial": "green", +} + +def html_src(harm_level): + return f""" +
+
+ {harm_level} +
+
+""" + +def print_like_dislike(x: gr.LikeData): + print(x.index, x.value, x.liked) + +def add_message(history, message): + for x in message["files"]: + history.append({"role": "user", "content": {"path": x}}) + if message["text"] is not None: + history.append({"role": "user", "content": message['text']}) + return history, gr.MultimodalTextbox(value=None, interactive=False) + +def bot(history, response_type): + if response_type == "gallery": + msg = {"role": "assistant", + "content": gr.Gallery( + [ + "https://github.com/gradio-app/gradio/raw/main/gradio/media_assets/images/bus.png", + "https://github.com/gradio-app/gradio/raw/main/gradio/media_assets/images/bus.png", + ]) + } + elif response_type == "image": + msg = {"role": "assistant", + "content": gr.Image( + "https://github.com/gradio-app/gradio/raw/main/gradio/media_assets/images/bus.png" + ) + } + elif response_type == "video": + msg = {"role": "assistant", + "content": gr.Video("https://github.com/gradio-app/gradio/raw/main/gradio/media_assets/videos/world.mp4", + label="test") + } + elif response_type == "audio": + msg = {"role": "assistant", + "content": gr.Audio("https://github.com/gradio-app/gradio/raw/main/gradio/media_assets/audio/audio_sample.wav") + } + elif response_type == "html": + msg = {"role": "assistant", + "content": gr.HTML( + html_src(random.choice(["harmful", "neutral", "beneficial"])) + ) + } + elif response_type == "model3d": + msg = {"role": "assistant", "content": gr.Model3D( + "https://github.com/gradio-app/gradio/raw/main/gradio/media_assets/models3d/Fox.gltf" + )} + else: + msg = {"role": "assistant", "content": "Cool!"} + history.append(msg) + return history + +with gr.Blocks(fill_height=True) as demo: + chatbot = gr.Chatbot( + elem_id="chatbot", + scale=1, + ) + response_type = gr.Radio( + [ + "image", + "text", + "gallery", + "video", + "audio", + "html", + "model3d", + ], + value="text", + label="Response Type", + ) + + chat_input = gr.MultimodalTextbox( + interactive=True, + placeholder="Enter message or upload file...", + show_label=False, + ) + + chat_msg = chat_input.submit( + add_message, [chatbot, chat_input], [chatbot, chat_input] + ) + bot_msg = chat_msg.then( + bot, [chatbot, response_type], chatbot, api_name="bot_response" + ) + bot_msg.then(lambda: gr.MultimodalTextbox(interactive=True), None, [chat_input]) + + chatbot.like(print_like_dislike, None, None) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_dialogpt/requirements.txt b/demo/chatbot_dialogpt/requirements.txt new file mode 100644 index 0000000..39dab0f --- /dev/null +++ b/demo/chatbot_dialogpt/requirements.txt @@ -0,0 +1,2 @@ +torch +transformers \ No newline at end of file diff --git a/demo/chatbot_dialogpt/run.py b/demo/chatbot_dialogpt/run.py new file mode 100644 index 0000000..23e6ea9 --- /dev/null +++ b/demo/chatbot_dialogpt/run.py @@ -0,0 +1,45 @@ +# type: ignore +import gradio as gr +from transformers import AutoModelForCausalLM, AutoTokenizer +import torch + +tokenizer = AutoTokenizer.from_pretrained("microsoft/DialoGPT-medium") +model = AutoModelForCausalLM.from_pretrained("microsoft/DialoGPT-medium") + +def user(message, history): + return "", history + [[message, None]] + +def bot(history): + user_message = history[-1][0] + new_user_input_ids = tokenizer.encode( + user_message + tokenizer.eos_token, return_tensors="pt" + ) + + # append the new user input tokens to the chat history + bot_input_ids = torch.cat([torch.LongTensor([]), new_user_input_ids], dim=-1) + + # generate a response + response = model.generate( + bot_input_ids, max_length=1000, pad_token_id=tokenizer.eos_token_id + ).tolist() + + # convert the tokens to text, and then split the responses into lines + response = tokenizer.decode(response[0]).split("<|endoftext|>") + response = [ + (response[i], response[i + 1]) for i in range(0, len(response) - 1, 2) + ] # convert to tuples of list + history[-1] = response[0] + return history + +with gr.Blocks() as demo: + chatbot = gr.Chatbot() + msg = gr.Textbox() + clear = gr.Button("Clear") + + msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then( + bot, chatbot, chatbot + ) + clear.click(lambda: None, None, chatbot, queue=False) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_dialogpt/screenshot.gif b/demo/chatbot_dialogpt/screenshot.gif new file mode 100644 index 0000000..48961ca Binary files /dev/null and b/demo/chatbot_dialogpt/screenshot.gif differ diff --git a/demo/chatbot_dialogpt/screenshot.png b/demo/chatbot_dialogpt/screenshot.png new file mode 100644 index 0000000..d7d6618 Binary files /dev/null and b/demo/chatbot_dialogpt/screenshot.png differ diff --git a/demo/chatbot_editable/run.py b/demo/chatbot_editable/run.py new file mode 100644 index 0000000..d232712 --- /dev/null +++ b/demo/chatbot_editable/run.py @@ -0,0 +1,36 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + chatbot = gr.Chatbot(value=[], editable="user") + add_message_btn = gr.Button("Add Message") + add_user_message_btn = gr.Button("Add User Message") + + with gr.Row(): + concatenated_text1 = gr.Textbox(label="Concatenated Chat 1") + concatenated_text2 = gr.Textbox(label="Concatenated Chat 2") + edited_messages = gr.Textbox(label="Edited Message") + + def add_message(history: list): + usr_msg = "I'm a user" + bot_msg = "I'm a bot" + history.append({"role": "user", "content": usr_msg}) + history.append({"role": "assistant", "content": bot_msg}) + return history + + def add_user_message(history: list): + usr_msg = "I'm a user" + history.append({"role": "user", "content": usr_msg}) + return history + + add_message_btn.click(add_message, [chatbot], [chatbot]) + add_user_message_btn.click(add_user_message, [chatbot], [chatbot]) + chatbot.change(lambda m: "|".join(m["content"][0]["text"] for m in m), chatbot, concatenated_text1) + + def edit_message(edited_message: gr.EditData): # type: ignore + return f"from {edited_message.previous_value} to {edited_message.value} at {edited_message.index}" # type: ignore + + chatbot.edit(edit_message, None, edited_messages) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_examples/run.py b/demo/chatbot_examples/run.py new file mode 100644 index 0000000..0cdb80a --- /dev/null +++ b/demo/chatbot_examples/run.py @@ -0,0 +1,54 @@ +import gradio as gr +import os +# Multimodal Chatbot demo that shows support for examples (example messages shown within the chatbot). + +def print_like_dislike(x: gr.LikeData): + print(x.index, x.value, x.liked) + +def add_message(history, message): + for x in message["files"]: + history.append({"role": "user", "content": (x,)}) + if message["text"] is not None: + history.append({"role": "user", "content": message["text"]}) + return history, gr.MultimodalTextbox(value=None, interactive=False) + +def append_example_message(x: gr.SelectData, history): + if x.value["text"] is not None: + history.append({"role": "user", "content": x.value["text"]}) + if "files" in x.value: + if isinstance(x.value["files"], list): + for file in x.value["files"]: + history.append({"role": "user", "content": file}) + else: + history.append({"role": "user", "content": x.value["files"]}) + return history + +def respond(history): + history.append({"role": "assistant", "content": "Cool!", "options": [{"value": "Option 1"}, {"value": "Option 2"}]}) + return history + +with gr.Blocks() as demo: + chatbot = gr.Chatbot( + elem_id="chatbot", + scale=1, + placeholder='

Welcome to Gradio!

', + examples=[{"icon": os.path.join(os.path.dirname(__file__), "files/avatar.png"), "display_text": "Display Text Here!", "text": "Try this example with this audio.", "files": [os.path.join(os.path.dirname(__file__), "files/cantina.wav")]}, + {"text": "Try this example with this image.", "files": [os.path.join(os.path.dirname(__file__), "files/avatar.png")]}, + {"text": "This is just text, no files!"}, + {"text": "Try this example with this image.", "files": [os.path.join(os.path.dirname(__file__), "files/avatar.png"), os.path.join(os.path.dirname(__file__), "files/avatar.png")]}, + {"text": "Try this example with this Audio.", "files": [os.path.join(os.path.dirname(__file__), "files/cantina.wav")]}] + ) + + chat_input = gr.MultimodalTextbox(interactive=True, + file_count="multiple", + placeholder="Enter message or upload file...", show_label=False) + + chat_msg = chat_input.submit(add_message, [chatbot, chat_input], [chatbot, chat_input]) + bot_msg = chat_msg.then(respond, chatbot, chatbot, api_name="bot_response") + bot_msg.then(lambda: gr.MultimodalTextbox(interactive=True), None, [chat_input]) + + chatbot.like(print_like_dislike, None, None) + chatbot.example_select(append_example_message, [chatbot], [chatbot]).then(respond, chatbot, chatbot, api_name="respond") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_feedback/run.py b/demo/chatbot_feedback/run.py new file mode 100644 index 0000000..cde9ba6 --- /dev/null +++ b/demo/chatbot_feedback/run.py @@ -0,0 +1,26 @@ +import gradio as gr + +def test_liked_loading(): + test_history = [ + {"role": "user", "content": "test user message"}, + {"role": "assistant", "content": "test assistant message"} + ] + # Set feedback_value to ["Like"] for the assistant message + return gr.update(value=test_history, feedback_value=["Like"]) + +with gr.Blocks() as demo: + chatbot = gr.Chatbot( + resizable=True, + min_height=500, + layout="bubble", + ) + chatbot.like( + lambda: None, + inputs=[], + outputs=None, + ) + test_btn = gr.Button("Test Liked Loading") + test_btn.click(test_liked_loading, outputs=[chatbot]) + +if __name__ == "__main__": + demo.launch(debug=True) diff --git a/demo/chatbot_load_event/run.py b/demo/chatbot_load_event/run.py new file mode 100644 index 0000000..06c8607 --- /dev/null +++ b/demo/chatbot_load_event/run.py @@ -0,0 +1,22 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Tab('Tab 1'): + with gr.Row(): + gr.Chatbot() + with gr.Column(): + gr.Slider() + gr.Slider() + gr.Slider() + + with gr.Tab('Tab 2'): + textbox = gr.Textbox(label="Output") + + demo.load( + fn=lambda: 'some text', + inputs=None, + outputs=textbox, + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_multimodal/run.py b/demo/chatbot_multimodal/run.py new file mode 100644 index 0000000..bd9f7ab --- /dev/null +++ b/demo/chatbot_multimodal/run.py @@ -0,0 +1,50 @@ +import gradio as gr +import time + +# Chatbot demo with multimodal input (text, markdown, LaTeX, code blocks, image, audio, & video). Plus shows support for streaming text. + + +def print_like_dislike(x: gr.LikeData): + print(x.index, x.value, x.liked) + + +def add_message(history, message): + user_msg = {"role": "user", "content": []} + for x in message["files"]: # type: ignore + user_msg["content"].append({"path": x}) # type: ignore + if message["text"] is not None: # type: ignore + user_msg["content"].append(message["text"]) # type: ignore + history.append(user_msg) + return history, gr.MultimodalTextbox(value=None, interactive=False) + + +def bot(history: list): + response = "**That's cool!**" + history.append({"role": "assistant", "content": ""}) + for character in response: + history[-1]["content"] += character + time.sleep(0.05) + yield history + + +with gr.Blocks() as demo: + chatbot = gr.Chatbot(elem_id="chatbot", like_user_message=True) + + chat_input = gr.MultimodalTextbox( + interactive=True, + file_count="multiple", + placeholder="Enter message or upload file...", + show_label=False, + sources=["microphone", "upload"], + ) + + chat_msg = chat_input.submit( + add_message, [chatbot, chat_input], [chatbot, chat_input] + ) + bot_msg = chat_msg.then(bot, chatbot, chatbot, api_name="bot_response") + bot_msg.then(lambda: gr.MultimodalTextbox(interactive=True), None, [chat_input]) + + chatbot.like(print_like_dislike, None, None) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_nested_thoughts/run.py b/demo/chatbot_nested_thoughts/run.py new file mode 100644 index 0000000..250a92d --- /dev/null +++ b/demo/chatbot_nested_thoughts/run.py @@ -0,0 +1,79 @@ +import gradio as gr +from gradio import ChatMessage +import time + +sleep_time = 0.1 +long_sleep_time = 1 + +def generate_response(history): + history.append( + ChatMessage( + role="user", content="What is the weather in San Francisco right now?" + ) + ) + yield history + time.sleep(sleep_time) + history.append( + ChatMessage( + role="assistant", + content="In order to find the current weather in San Francisco, I will need to use my weather tool.", + ) + ) + yield history + time.sleep(sleep_time) + history.append( + ChatMessage( + role="assistant", + content="", + metadata={"title": "Gathering Weather Websites", "id": 1}, + ) + ) + yield history + time.sleep(long_sleep_time) + history[-1].content = "Will check: weather.com and sunny.org" + yield history + time.sleep(sleep_time) + history.append( + ChatMessage( + role="assistant", + content="Received weather from weather.com.", + metadata={"title": "API Success ✅", "parent_id": 1, "id": 2}, + ) + ) + yield history + time.sleep(sleep_time) + history.append( + ChatMessage( + role="assistant", + content="API Error when connecting to sunny.org.", + metadata={"title": "API Error 💥 ", "parent_id": 1, "id": 3}, + ) + ) + yield history + time.sleep(sleep_time) + + history.append( + ChatMessage( + role="assistant", + content="I will try yet again", + metadata={"title": "I will try again", "id": 4, "parent_id": 3}, + ) + ) + yield history + + time.sleep(sleep_time) + history.append( + ChatMessage( + role="assistant", + content="Failed again", + metadata={"title": "Failed again", "id": 6, "parent_id": 4}, + ) + ) + yield history + +with gr.Blocks() as demo: + chatbot = gr.Chatbot(height=500, buttons=["copy"]) + demo.load(generate_response, chatbot, chatbot) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_reasoning_tags/run.py b/demo/chatbot_reasoning_tags/run.py new file mode 100644 index 0000000..81e3741 --- /dev/null +++ b/demo/chatbot_reasoning_tags/run.py @@ -0,0 +1,32 @@ +import gradio as gr + +def respond(message, history): + response = """ +Let me analyze this problem step by step. +First, I need to understand what the user is asking. +Then I can formulate a proper response. + + +Based on your question, here's my answer: This is the main response content that should be visible by default. + + +Now let me consider if there are any edge cases. +I should make sure my response is complete. + + +And here's some additional information that might be helpful.""" + + return response + +demo = gr.ChatInterface( + fn=respond, + chatbot=gr.Chatbot( + reasoning_tags=[("", "")], + height=600 + ), + title="Test Collapse Thinking Feature", + description="This demo tests the reasoning_tags parameter. The thinking blocks should be collapsed by default." +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_retry_undo_like/requirements.txt b/demo/chatbot_retry_undo_like/requirements.txt new file mode 100644 index 0000000..6b964cc --- /dev/null +++ b/demo/chatbot_retry_undo_like/requirements.txt @@ -0,0 +1 @@ +huggingface_hub diff --git a/demo/chatbot_retry_undo_like/run.py b/demo/chatbot_retry_undo_like/run.py new file mode 100644 index 0000000..0561c2f --- /dev/null +++ b/demo/chatbot_retry_undo_like/run.py @@ -0,0 +1,63 @@ +from huggingface_hub import InferenceClient +import gradio as gr + +client = InferenceClient() + +def respond( + prompt: str, + history, +): + if not history: + history = [{"role": "system", "content": "You are a friendly chatbot"}] + history.append({"role": "user", "content": prompt}) + + yield history + + response = {"role": "assistant", "content": ""} + for message in client.chat_completion( # type: ignore + history, + temperature=0.95, + top_p=0.9, + max_tokens=512, + stream=True, + model="openai/gpt-oss-20b" + ): + response["content"] += message.choices[0].delta.content or "" if message.choices else "" + yield history + [response] + + +def handle_undo(history, undo_data: gr.UndoData): + return history[:undo_data.index], history[undo_data.index]['content'][0]["text"] + +def handle_retry(history, retry_data: gr.RetryData): + new_history = history[:retry_data.index] + previous_prompt = history[retry_data.index]['content'][0]["text"] + yield from respond(previous_prompt, new_history) + + +def handle_like(data: gr.LikeData): + if data.liked: + print("You upvoted this response: ", data.value) + else: + print("You downvoted this response: ", data.value) + + +with gr.Blocks() as demo: + gr.Markdown("# Chat with GPT-OSS 20b 🤗") + chatbot = gr.Chatbot( + label="Agent", + avatar_images=( + None, + "https://em-content.zobj.net/source/twitter/376/hugging-face_1f917.png", + ), + ) + prompt = gr.Textbox(max_lines=1, label="Chat Message") + prompt.submit(respond, [prompt, chatbot], [chatbot]) + prompt.submit(lambda: "", None, [prompt]) + chatbot.undo(handle_undo, chatbot, [chatbot, prompt]) + chatbot.retry(handle_retry, chatbot, [chatbot]) + chatbot.like(handle_like, None, None) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_simple/run.py b/demo/chatbot_simple/run.py new file mode 100644 index 0000000..0cd5ddb --- /dev/null +++ b/demo/chatbot_simple/run.py @@ -0,0 +1,20 @@ +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]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_streaming/run.py b/demo/chatbot_streaming/run.py new file mode 100644 index 0000000..2874694 --- /dev/null +++ b/demo/chatbot_streaming/run.py @@ -0,0 +1,27 @@ +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) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_thoughts/run.py b/demo/chatbot_thoughts/run.py new file mode 100644 index 0000000..bbe5e38 --- /dev/null +++ b/demo/chatbot_thoughts/run.py @@ -0,0 +1,65 @@ +import gradio as gr +from gradio import ChatMessage +import time + +def simulate_thinking_chat(message: str, history: list): + history.append( + ChatMessage( + role="assistant", + content="", + metadata={"title": "Thinking... ", "log": "Starting analysis"} + ) + ) + time.sleep(0.5) + yield history + + thoughts = [ + "First, I need to understand the core aspects of the query...", + "Now, considering the broader context and implications...", + "Analyzing potential approaches to formulate a comprehensive answer...", + "Finally, structuring the response for clarity and completeness..." + ] + + accumulated_thoughts = "" + + for i, thought in enumerate(thoughts): + time.sleep(0.5) + + accumulated_thoughts += f"- {thought}\n\n" + + history[-1] = ChatMessage( + role="assistant", + content=accumulated_thoughts.strip(), + metadata={ + "title": "Thinking...", + "log": f"Step {i+1} completed.", + "duration": 0.5 * (i + 1) + } + ) + yield history + + history.append( + ChatMessage( + role="assistant", + content="Based on my thoughts and analysis above, my response is: This dummy repro shows how thoughts of a thinking LLM can be progressively shown before providing its final answer." + ) + ) + yield history + +with gr.Blocks() as demo: + gr.Markdown("# Thinking LLM Demo 🤔") + chatbot = gr.Chatbot(render_markdown=True) + msg = gr.Textbox(placeholder="Type your message...") + + msg.submit( + lambda m, h: (m, h + [ChatMessage(role="user", content=m)]), + [msg, chatbot], + [msg, chatbot] + ).then( + simulate_thinking_chat, + [msg, chatbot], + chatbot + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatbot_with_tools/run.py b/demo/chatbot_with_tools/run.py new file mode 100644 index 0000000..f39320d --- /dev/null +++ b/demo/chatbot_with_tools/run.py @@ -0,0 +1,79 @@ +import gradio as gr +from gradio import ChatMessage +import time + +def generate_response(history): + history.append( + ChatMessage( + role="user", content="What is the weather in San Francisco right now?" + ) + ) + yield history + time.sleep(0.25) + history.append( + ChatMessage( + role="assistant", + content="In order to find the current weather in San Francisco, I will need to use my weather tool.", + ) + ) + yield history + time.sleep(0.25) + + history.append( + ChatMessage( + role="assistant", + content="API Error when connecting to weather service.", + metadata={"title": "💥 Error using tool 'Weather'"}, + ) + ) + yield history + time.sleep(0.25) + + history.append( + ChatMessage( + role="assistant", + content="I will try again", + ) + ) + yield history + time.sleep(0.25) + + history.append( + ChatMessage( + role="assistant", + content="Weather 72 degrees Fahrenheit with 20% chance of rain.", + metadata={"title": "🛠️ Used tool 'Weather'"}, + ) + ) + yield history + time.sleep(0.25) + + history.append( + ChatMessage( + role="assistant", + content="Now that the API succeeded I can complete my task.", + ) + ) + yield history + time.sleep(0.25) + + history.append( + ChatMessage( + role="assistant", + content="It's a sunny day in San Francisco with a current temperature of 72 degrees Fahrenheit and a 20% chance of rain. Enjoy the weather!", + ) + ) + yield history + +def like(evt: gr.LikeData): + print("User liked the response") + print(evt.index, evt.liked, evt.value) + +with gr.Blocks() as demo: + chatbot = gr.Chatbot(height=500, buttons=["copy"]) + button = gr.Button("Get San Francisco Weather") + button.click(generate_response, chatbot, chatbot) + chatbot.like(like) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatinterface_artifacts/run.py b/demo/chatinterface_artifacts/run.py new file mode 100644 index 0000000..f3d7bcf --- /dev/null +++ b/demo/chatinterface_artifacts/run.py @@ -0,0 +1,44 @@ +import gradio as gr + +python_code = """ +def fib(n): + if n <= 0: + return 0 + elif n == 1: + return 1 + else: + return fib(n-1) + fib(n-2) +""" + +js_code = """ +function fib(n) { + if (n <= 0) return 0; + if (n === 1) return 1; + return fib(n - 1) + fib(n - 2); +} +""" + +def chat(message, history): + if "python" in message.lower(): + return "Type Python or JavaScript to see the code.", gr.Code(language="python", value=python_code) + elif "javascript" in message.lower(): + return "Type Python or JavaScript to see the code.", gr.Code(language="javascript", value=js_code) + else: + return "Please ask about Python or JavaScript.", None + +with gr.Blocks() as demo: + code = gr.Code(render=False) + with gr.Row(): + with gr.Column(): + gr.Markdown("

Write Python or JavaScript

") + gr.ChatInterface( + chat, + examples=["Python", "JavaScript"], + additional_outputs=[code], + api_name="chat", + ) + with gr.Column(): + gr.Markdown("

Code Artifacts

") + code.render() + +demo.launch() diff --git a/demo/chatinterface_deep_link/run.py b/demo/chatinterface_deep_link/run.py new file mode 100644 index 0000000..0cda794 --- /dev/null +++ b/demo/chatinterface_deep_link/run.py @@ -0,0 +1,36 @@ +import time +import gradio as gr + +def slow_echo(message, history): + for i in range(len(message["text"])): + time.sleep(0.05) + yield "You typed: " + message["text"][: i + 1] + +chat = gr.ChatInterface( + slow_echo, + flagging_mode="manual", + flagging_options=["Like", "Spam", "Inappropriate", "Other"], + save_history=False, + multimodal=True, + api_name="chat" +) + +with gr.Blocks() as demo: + + chat.render() + gr.DeepLinkButton() + +with demo.route("cached_examples"): + gr.Interface(lambda x, y: f"{y}: {x}", + inputs=[gr.Textbox(label="name"), + gr.Radio(label="Salutation", choices=["Hello", "Greetings"]) + ], + outputs=gr.Textbox(label="Output"), + examples=[["Freddy", "Hello"]], + cache_examples=True, + api_name="predict", + deep_link=True) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatinterface_echo_multimodal/run.py b/demo/chatinterface_echo_multimodal/run.py new file mode 100644 index 0000000..50b901c --- /dev/null +++ b/demo/chatinterface_echo_multimodal/run.py @@ -0,0 +1,19 @@ +import gradio as gr + +def echo_multimodal(message, history): + response = [] + response.append("You wrote: '" + message["text"] + "' and uploaded:") + if message.get("files"): + for file in message["files"]: + response.append(gr.File(value=file)) + return response + +demo = gr.ChatInterface( + echo_multimodal, + multimodal=True, + textbox=gr.MultimodalTextbox(file_count="multiple"), + api_name="chat", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatinterface_nested_thoughts/run.py b/demo/chatinterface_nested_thoughts/run.py new file mode 100644 index 0000000..ce77b73 --- /dev/null +++ b/demo/chatinterface_nested_thoughts/run.py @@ -0,0 +1,81 @@ +import gradio as gr +from gradio import ChatMessage +import time + +sleep_time = 0.1 +long_sleep_time = 1 + +def generate_response(message, history): + start_time = time.time() + responses = [ + ChatMessage( + content="In order to find the current weather in San Francisco, I will need to use my weather tool.", + ) + ] + yield responses + time.sleep(sleep_time) + + main_thought = ChatMessage( + content="", + metadata={"title": "Using Weather Tool", "id": 1, "status": "pending"}, + ) + + responses.append(main_thought) + + yield responses + time.sleep(long_sleep_time) + responses[-1].content = "Will check: weather.com and sunny.org" + yield responses + time.sleep(sleep_time) + responses.append( + ChatMessage( + content="Received weather from weather.com.", + metadata={"title": "Checking weather.com", "parent_id": 1, "id": 2, "duration": 0.05}, + ) + ) + yield responses + + sunny_start_time = time.time() + time.sleep(sleep_time) + sunny_thought = ChatMessage( + content="API Error when connecting to sunny.org 💥", + metadata={"title": "Checking sunny.org", "parent_id": 1, "id": 3, "status": "pending"}, + ) + + responses.append(sunny_thought) + yield responses + + time.sleep(sleep_time) + responses.append( + ChatMessage( + content="Failed again", + metadata={"title": "I will try again", "id": 4, "parent_id": 3, "duration": 0.1}, + + ) + ) + sunny_thought.metadata["status"] = "done" + sunny_thought.metadata["duration"] = time.time() - sunny_start_time + + main_thought.metadata["status"] = "done" + main_thought.metadata["duration"] = time.time() - start_time + + yield responses + + time.sleep(long_sleep_time) + + responses.append( + ChatMessage( + content="Based on the data only from weather.com, the current weather in San Francisco is 60 degrees and sunny.", + ) + ) + yield responses + +demo = gr.ChatInterface( + generate_response, + title="Nested Thoughts Chat Interface", + examples=["What is the weather in San Francisco right now?"], + api_name="chat" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatinterface_options/run.py b/demo/chatinterface_options/run.py new file mode 100644 index 0000000..7c2b73c --- /dev/null +++ b/demo/chatinterface_options/run.py @@ -0,0 +1,31 @@ +import gradio as gr +import random + +example_code = """ +Here's an example Python lambda function: + +lambda x: x + {} + +Is this correct? +""" + +def chat(message, history): + if message == "Yes, that's correct.": + return "Great!" + else: + return gr.ChatMessage( + content=example_code.format(random.randint(1, 100)), + options=[ + {"value": "Yes, that's correct.", "label": "Yes"}, + {"value": "No"} + ] + ) + +demo = gr.ChatInterface( + chat, + examples=["Write an example Python lambda function."], + api_name="chat", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatinterface_prefill/run.py b/demo/chatinterface_prefill/run.py new file mode 100644 index 0000000..6a61126 --- /dev/null +++ b/demo/chatinterface_prefill/run.py @@ -0,0 +1,27 @@ +import gradio as gr +import random + +def prefill_chatbot(choice): + if choice == "Greeting": + return [ + {"role": "user", "content": "Hi there!"}, + {"role": "assistant", "content": "Hello! How can I assist you today?"} + ] + elif choice == "Complaint": + return [ + {"role": "user", "content": "I'm not happy with the service."}, + {"role": "assistant", "content": "I'm sorry to hear that. Can you please tell me more about the issue?"} + ] + else: + return [] + +def random_response(message, history): + return random.choice(["Yes", "No"]) + +with gr.Blocks() as demo: + radio = gr.Radio(["Greeting", "Complaint", "Blank"]) + chat = gr.ChatInterface(random_response, api_name="chat") + radio.change(prefill_chatbot, radio, chat.chatbot_value) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatinterface_random_response/run.py b/demo/chatinterface_random_response/run.py new file mode 100644 index 0000000..fe96bca --- /dev/null +++ b/demo/chatinterface_random_response/run.py @@ -0,0 +1,10 @@ +import random +import gradio as gr + +def random_response(message, history): + return random.choice(["Yes", "No"]) + +demo = gr.ChatInterface(random_response, autofocus=False, api_name="chat") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatinterface_random_response/screenshot.gif b/demo/chatinterface_random_response/screenshot.gif new file mode 100644 index 0000000..f93c1fb Binary files /dev/null and b/demo/chatinterface_random_response/screenshot.gif differ diff --git a/demo/chatinterface_save_history/run.py b/demo/chatinterface_save_history/run.py new file mode 100644 index 0000000..aa402b8 --- /dev/null +++ b/demo/chatinterface_save_history/run.py @@ -0,0 +1,15 @@ +import gradio as gr + +def echo_multimodal(message, history): + response = "You wrote: '" + message["text"] + "' and uploaded: " + str(len(message["files"])) + " files" + return response + +demo = gr.ChatInterface( + echo_multimodal, + multimodal=True, + textbox=gr.MultimodalTextbox(file_count="multiple"), + save_history=True, +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatinterface_streaming_echo/run.py b/demo/chatinterface_streaming_echo/run.py new file mode 100644 index 0000000..065972f --- /dev/null +++ b/demo/chatinterface_streaming_echo/run.py @@ -0,0 +1,17 @@ +import time +import gradio as gr + +def slow_echo(message, history): + for i in range(len(message)): + time.sleep(0.05) + yield "You typed: " + message[: i + 1] + +demo = gr.ChatInterface( + slow_echo, + flagging_mode="manual", + flagging_options=["Like", "Spam", "Inappropriate", "Other"], + save_history=True, +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatinterface_system_prompt/run.py b/demo/chatinterface_system_prompt/run.py new file mode 100644 index 0000000..ce824d7 --- /dev/null +++ b/demo/chatinterface_system_prompt/run.py @@ -0,0 +1,19 @@ +import gradio as gr +import time + +def echo(message, history, system_prompt, tokens): + response = f"System prompt: {system_prompt}\n Message: {message}." + for i in range(min(len(response), int(tokens))): + time.sleep(0.05) + yield response[: i + 1] + +demo = gr.ChatInterface( + echo, + additional_inputs=[ + gr.Textbox("You are helpful AI.", label="System Prompt"), + gr.Slider(10, 100), + ], +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/chatinterface_thoughts/run.py b/demo/chatinterface_thoughts/run.py new file mode 100644 index 0000000..8719d2a --- /dev/null +++ b/demo/chatinterface_thoughts/run.py @@ -0,0 +1,48 @@ +import gradio as gr +from gradio import ChatMessage +import time + +sleep_time = 0.5 + +def simulate_thinking_chat(message, history): + start_time = time.time() + response = ChatMessage( + content="", + metadata={"title": "_Thinking_ step-by-step", "id": 0, "status": "pending"} + ) + yield response + + thoughts = [ + "First, I need to understand the core aspects of the query...", + "Now, considering the broader context and implications...", + "Analyzing potential approaches to formulate a comprehensive answer...", + "Finally, structuring the response for clarity and completeness..." + ] + + accumulated_thoughts = "" + for thought in thoughts: + time.sleep(sleep_time) + accumulated_thoughts += f"- {thought}\n\n" + response.content = accumulated_thoughts.strip() + yield response + + response.metadata["status"] = "done" + response.metadata["duration"] = time.time() - start_time + yield response + + response = [ + response, + ChatMessage( + content="Based on my thoughts and analysis above, my response is: This dummy repro shows how thoughts of a thinking LLM can be progressively shown before providing its final answer." + ) + ] + yield response + + +demo = gr.ChatInterface( + simulate_thinking_chat, + title="Thinking LLM Chat Interface 🤔", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/checkbox_component/run.py b/demo/checkbox_component/run.py new file mode 100644 index 0000000..f45f043 --- /dev/null +++ b/demo/checkbox_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Checkbox() + +demo.launch() diff --git a/demo/checkboxgroup_component/run.py b/demo/checkboxgroup_component/run.py new file mode 100644 index 0000000..6ee98bc --- /dev/null +++ b/demo/checkboxgroup_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.CheckboxGroup(choices=["First Choice", "Second Choice", "Third Choice"]) + +demo.launch() diff --git a/demo/chicago-bikeshare-dashboard/requirements.txt b/demo/chicago-bikeshare-dashboard/requirements.txt new file mode 100644 index 0000000..5facfef --- /dev/null +++ b/demo/chicago-bikeshare-dashboard/requirements.txt @@ -0,0 +1,4 @@ +psycopg2 +matplotlib +SQLAlchemy +pandas diff --git a/demo/chicago-bikeshare-dashboard/run.py b/demo/chicago-bikeshare-dashboard/run.py new file mode 100644 index 0000000..4caf899 --- /dev/null +++ b/demo/chicago-bikeshare-dashboard/run.py @@ -0,0 +1,84 @@ +import os +import gradio as gr +import pandas as pd + +DB_USER = os.getenv("DB_USER") +DB_PASSWORD = os.getenv("DB_PASSWORD") +DB_HOST = os.getenv("DB_HOST") +PORT = 8080 +DB_NAME = "bikeshare" + +connection_string = ( + f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}?port={PORT}&dbname={DB_NAME}" +) + +def get_count_ride_type(): + df = pd.read_sql( + """ + SELECT COUNT(ride_id) as n, rideable_type + FROM rides + GROUP BY rideable_type + ORDER BY n DESC + """, + con=connection_string, + ) + return df + +def get_most_popular_stations(): + + df = pd.read_sql( + """ + SELECT COUNT(ride_id) as n, MAX(start_station_name) as station + FROM RIDES + WHERE start_station_name is NOT NULL + GROUP BY start_station_id + ORDER BY n DESC + LIMIT 5 + """, + con=connection_string, + ) + return df + +with gr.Blocks() as demo: + gr.Markdown( + """ + # Chicago Bike Share Dashboard + + This demo pulls Chicago bike share data for March 2022 from a postgresql database hosted on AWS. + This demo uses psycopg2 but any postgresql client library (SQLAlchemy) + is compatible with gradio. + + Connection credentials are handled by environment variables + defined as secrets in the Space. + + If data were added to the database, the plots in this demo would update + whenever the webpage is reloaded. + + This demo serves as a starting point for your database-connected apps! + """ + ) + with gr.Row(): + bike_type = gr.BarPlot( + x="rideable_type", + y='n', + title="Number of rides per bicycle type", + y_title="Number of Rides", + x_title="Bicycle Type", + tooltip=['rideable_type', "n"], + height=300, + ) + station = gr.BarPlot( + x='station', + y='n', + title="Most Popular Stations", + y_title="Number of Rides", + x_title="Station Name", + tooltip=['station', 'n'], + height=300 + ) + + demo.load(get_count_ride_type, inputs=None, outputs=bike_type) + demo.load(get_most_popular_stations, inputs=None, outputs=station) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/clear_components/__init__.py b/demo/clear_components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/clear_components/requirements.txt b/demo/clear_components/requirements.txt new file mode 100644 index 0000000..4021870 --- /dev/null +++ b/demo/clear_components/requirements.txt @@ -0,0 +1,3 @@ +numpy +pandas +matplotlib diff --git a/demo/clear_components/run.py b/demo/clear_components/run.py new file mode 100644 index 0000000..2d05a55 --- /dev/null +++ b/demo/clear_components/run.py @@ -0,0 +1,166 @@ +import gradio as gr +from datetime import datetime +import os +import random +import string +import pandas as pd + +import numpy as np +import matplotlib.pyplot as plt +# get_audio(), get_video() return file paths to sample media included with Gradio +from gradio.media import get_audio, get_video, MEDIA_ROOT + +def random_plot(): + start_year = 2020 + x = np.arange(start_year, start_year + 5) + year_count = x.shape[0] + plt_format = "-" + fig = plt.figure() + ax = fig.add_subplot(111) + series = np.arange(0, year_count, dtype=float) + series = series**2 + series += np.random.rand(year_count) + ax.plot(x, series, plt_format) + return fig + +images = [ + "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80", + "https://images.unsplash.com/photo-1554151228-14d9def656e4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=386&q=80", + "https://images.unsplash.com/photo-1542909168-82c3e7fdca5c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8aHVtYW4lMjBmYWNlfGVufDB8fDB8fA%3D%3D&w=1000&q=80", +] +highlighted_text_output_1 = [ + { + "entity": "I-LOC", + "score": 0.9988978, + "index": 2, + "word": "Chicago", + "start": 5, + "end": 12, + }, + { + "entity": "I-MISC", + "score": 0.9958592, + "index": 5, + "word": "Pakistani", + "start": 22, + "end": 31, + }, +] +highlighted_text_output_2 = [ + { + "entity": "I-LOC", + "score": 0.9988978, + "index": 2, + "word": "Chicago", + "start": 5, + "end": 12, + }, + { + "entity": "I-LOC", + "score": 0.9958592, + "index": 5, + "word": "Pakistan", + "start": 22, + "end": 30, + }, +] + +highlighted_text = "Does Chicago have any Pakistani restaurants" + +components = [ + gr.Textbox(value=lambda: datetime.now(), label="Current Time"), + gr.Number(value=lambda: random.random(), label="Random Percentage"), + gr.Slider(minimum=0, maximum=100, randomize=True, label="Slider with randomize"), + gr.Slider( + minimum=0, + maximum=1, + value=lambda: random.random(), + label="Slider with value func", + ), + gr.Checkbox(value=lambda: random.random() > 0.5, label="Random Checkbox"), + gr.CheckboxGroup( + choices=["a", "b", "c", "d"], + value=lambda: random.choice(["a", "b", "c", "d"]), + label="Random CheckboxGroup", + ), + gr.Radio( + choices=list(string.ascii_lowercase), + value=lambda: random.choice(string.ascii_lowercase), + ), + gr.Dropdown( + choices=["a", "b", "c", "d", "e"], + value=lambda: random.choice(["a", "b", "c"]), + ), + gr.Image( + value=lambda: random.choice(images) + ), + gr.Video(value=lambda: get_video("world.mp4")), + gr.Audio(value=lambda: get_audio("cantina.wav")), + gr.File( + value=gr.get_file + ), + # gr.Dataframe( + # value=lambda: pd.DataFrame({"random_number_rows": range(5)}, columns=["one", "two", "three"]) # type: ignore + # ), + gr.ColorPicker(value=lambda: random.choice(["#000000", "#ff0000", "#0000FF"])), + gr.Label(value=lambda: random.choice(["Pedestrian", "Car", "Cyclist"])), + gr.HighlightedText( + value=lambda: random.choice( + [ + {"text": highlighted_text, "entities": highlighted_text_output_1}, + {"text": highlighted_text, "entities": highlighted_text_output_2}, + ] + ), + ), + gr.JSON(value=lambda: random.choice([{"a": 1}, {"b": 2}])), + # gr.HTML( + # value=lambda: random.choice( + # [ + # '

I am red

', + # '

I am blue

', + # ] + # ) + # ), + gr.Gallery( + value=lambda: images + ), + gr.Model3D(value=gr.get_model3d), + # gr.Plot(value=random_plot), + gr.Markdown(value=lambda: f"### {random.choice(['Hello', 'Hi', 'Goodbye!'])}"), +] + +def evaluate_values(*args): + are_false = [] + for a in args: + if isinstance(a, (pd.DataFrame, np.ndarray)): + are_false.append(not a.any().any()) # type: ignore + elif isinstance(a, str) and a.startswith("#"): + are_false.append(a == "#000000") + else: + are_false.append(not a) + return all(are_false) + +with gr.Blocks() as demo: + for i, component in enumerate(components): + component.label = f"component_{str(i).zfill(2)}" + component.render() + clear = gr.ClearButton(value="Clear", components=components) + result = gr.Textbox(label="Are all cleared?") + hide = gr.Button(value="Hide") + reveal = gr.Button(value="Reveal") + clear_button_and_components = components + [clear] + hide.click( + lambda: [c.__class__(visible=False) for c in clear_button_and_components], + inputs=[], + outputs=clear_button_and_components + ) + reveal.click( + lambda: [c.__class__(visible=True) for c in clear_button_and_components], + inputs=[], + outputs=clear_button_and_components + ) + get_value = gr.Button(value="Get Values") + get_value.click(evaluate_values, components, result) + +if __name__ == "__main__": + demo.launch(allowed_paths=[str(MEDIA_ROOT)]) \ No newline at end of file diff --git a/demo/clearbutton_component/run.py b/demo/clearbutton_component/run.py new file mode 100644 index 0000000..fafc554 --- /dev/null +++ b/demo/clearbutton_component/run.py @@ -0,0 +1,8 @@ +import gradio as gr + +with gr.Blocks() as demo: + textbox = gr.Textbox(value="This is some text") + gr.ClearButton(textbox) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/code/file.css b/demo/code/file.css new file mode 100644 index 0000000..abc61b6 --- /dev/null +++ b/demo/code/file.css @@ -0,0 +1,11 @@ +.class { + color: blue; +} + +#id { + color: pink; +} + +div { + color: purple; +} \ No newline at end of file diff --git a/demo/code/run.py b/demo/code/run.py new file mode 100644 index 0000000..9d6ed77 --- /dev/null +++ b/demo/code/run.py @@ -0,0 +1,39 @@ +import gradio as gr +import os +from time import sleep + +css_file = os.path.join(os.path.dirname(__file__), "file.css") + +def set_lang(language): + print(language) + return gr.Code(language=language) + +def set_lang_from_path(): + sleep(1) + return gr.Code(open(css_file).read(), language="css") + +def code(language, code): + return gr.Code(code, language=language) + +io = gr.Interface(lambda x: x, "code", "code", api_name="predict") + +with gr.Blocks() as demo: + lang = gr.Dropdown(value="python", choices=gr.Code.languages) # type: ignore + with gr.Row(): + code_in = gr.Code( + language="python", + label="Input", + value='def all_odd_elements(sequence):\n """Returns every odd element of the sequence."""', + show_line_numbers = False + ) + code_out = gr.Code(label="Output", show_line_numbers = True) + btn = gr.Button("Run") + btn_two = gr.Button("Load File") + + lang.change(set_lang, inputs=lang, outputs=code_in) + btn.click(code, inputs=[lang, code_in], outputs=code_out) + btn_two.click(set_lang_from_path, inputs=None, outputs=code_out) + io.render() + +if __name__ == "__main__": + demo.launch() diff --git a/demo/code_component/run.py b/demo/code_component/run.py new file mode 100644 index 0000000..3b3738a --- /dev/null +++ b/demo/code_component/run.py @@ -0,0 +1,13 @@ +import gradio as gr + +demo = gr.Interface( + lambda x: x, + gr.Code(language="python"), + gr.Code(language="python"), + examples=[[("/Users/freddy/sources/gradio/demo/code_component/run.py",)], + ["print('Hello, World!')"], + [("/Users/freddy/sources/gradio/demo/code/run.py", )]] +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/color_generator/requirements.txt b/demo/color_generator/requirements.txt new file mode 100644 index 0000000..6f1e523 --- /dev/null +++ b/demo/color_generator/requirements.txt @@ -0,0 +1,2 @@ +opencv-python +numpy \ No newline at end of file diff --git a/demo/color_generator/run.py b/demo/color_generator/run.py new file mode 100644 index 0000000..418c4fd --- /dev/null +++ b/demo/color_generator/run.py @@ -0,0 +1,57 @@ +import gradio as gr +import cv2 # type: ignore +import numpy as np +import random + +# Convert decimal color to hexadecimal color +def RGB_to_Hex(rgb): + color = "#" + for i in rgb: + num = int(i) + color += str(hex(num))[-2:].replace("x", "0").upper() + return color + +# Randomly generate light or dark colors +def random_color(is_light=True): + return ( + random.randint(0, 127) + int(is_light) * 128, + random.randint(0, 127) + int(is_light) * 128, + random.randint(0, 127) + int(is_light) * 128, + ) + +def switch_color(color_style): + is_light = color_style == "light" + back_color_ = random_color(is_light) # Randomly generate colors + back_color = RGB_to_Hex(back_color_) # Convert to hexadecimal + + # Draw color pictures. + w, h = 50, 50 + img = np.zeros((h, w, 3), np.uint8) + cv2.rectangle(img, (0, 0), (w, h), back_color_, thickness=-1) + + return back_color, back_color, img + +inputs = [gr.Radio(["light", "dark"], value="light")] + +outputs = [ + gr.ColorPicker(label="color"), + gr.Textbox(label="hexadecimal color"), + gr.Image(type="numpy", label="color picture"), +] + +title = "Color Generator" +description = ( + "Click the Submit button, and a dark or light color will be randomly generated." +) + +demo = gr.Interface( + fn=switch_color, + inputs=inputs, + outputs=outputs, + title=title, + description=description, + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/color_picker/rabbit.png b/demo/color_picker/rabbit.png new file mode 100644 index 0000000..0ccd761 Binary files /dev/null and b/demo/color_picker/rabbit.png differ diff --git a/demo/color_picker/requirements.txt b/demo/color_picker/requirements.txt new file mode 100644 index 0000000..5873a22 --- /dev/null +++ b/demo/color_picker/requirements.txt @@ -0,0 +1 @@ +Pillow \ No newline at end of file diff --git a/demo/color_picker/run.py b/demo/color_picker/run.py new file mode 100644 index 0000000..ef78dd6 --- /dev/null +++ b/demo/color_picker/run.py @@ -0,0 +1,38 @@ +import gradio as gr +import numpy as np +from PIL import Image, ImageColor + +def change_color(icon, color): + + """ + Function that given an icon in .png format changes its color + Args: + icon: Icon whose color needs to be changed. + color: Chosen color with which to edit the input icon. + Returns: + edited_image: Edited icon. + """ + img = icon.convert("LA") + img = img.convert("RGBA") + image_np = np.array(icon) + _, _, _, alpha = image_np.T + mask = alpha > 0 + image_np[..., :-1][mask.T] = ImageColor.getcolor(color, "RGB") + edited_image = Image.fromarray(image_np) + return edited_image + +inputs = [ + gr.Image(label="icon", type="pil", image_mode="RGBA"), + gr.ColorPicker(label="color"), +] +outputs = gr.Image(label="colored icon") + +demo = gr.Interface( + fn=change_color, + inputs=inputs, + outputs=outputs, + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/color_picker/screenshot.gif b/demo/color_picker/screenshot.gif new file mode 100644 index 0000000..9ce8cdd Binary files /dev/null and b/demo/color_picker/screenshot.gif differ diff --git a/demo/color_picker/screenshot.png b/demo/color_picker/screenshot.png new file mode 100644 index 0000000..99b747f Binary files /dev/null and b/demo/color_picker/screenshot.png differ diff --git a/demo/colorpicker_component/run.py b/demo/colorpicker_component/run.py new file mode 100644 index 0000000..eafd1d8 --- /dev/null +++ b/demo/colorpicker_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.ColorPicker() + +demo.launch() diff --git a/demo/component_props/cheetah.jpg b/demo/component_props/cheetah.jpg new file mode 100644 index 0000000..ca8a7aa Binary files /dev/null and b/demo/component_props/cheetah.jpg differ diff --git a/demo/component_props/run.py b/demo/component_props/run.py new file mode 100644 index 0000000..1c56e37 --- /dev/null +++ b/demo/component_props/run.py @@ -0,0 +1,79 @@ +import gradio as gr +from gradio.media import get_image + +with gr.Blocks() as demo: + a = gr.Number(value=5, minimum=0, maximum=10, label="Input A", info="Enter a number between 0 and 10") + output_a = gr.JSON(label="Output", elem_id="output") + with gr.Row(): + show_value_btn = gr.Button("Show Value") + double_btn = gr.Button("Double Value and Maximum") + reset_btn = gr.Button("Reset") + + def process_with_props(x: gr.Number): + return { + "value": x.value, + "maximum": x.maximum, + "minimum": x.minimum, + } + show_value_btn.click(process_with_props, a, output_a) + + def double_value_and_max(x: gr.Number): + x.maximum *= 2 # type: ignore + x.value = (x.value or 0) * 2 + x.info = f"Enter a number between 0 and {x.maximum}" + return x + + double_btn.click(double_value_and_max, a, a).then( + process_with_props, a, output_a + ) + + def reset(x: gr.Number): + x.maximum = 10 + x.value = 5 + x.info = "Enter a number between 0 and 10" + return x + + reset_btn.click(reset, a, a).then( + process_with_props, a, output_a + ) + + # Image component demo + gr.Markdown("## Image Component Props") + b = gr.Image(value=get_image("cheetah.jpg"), label="Input Image", width=300, height=300, type="filepath") + output_b = gr.JSON(label="Image Props Output", elem_id="image-output") + with gr.Row(): + show_image_props_btn = gr.Button("Show Image Props") + change_image_size_btn = gr.Button("Change Image Size") + reset_image_btn = gr.Button("Reset Image") + + def show_image_props(x: gr.Image): + return { + "value": x.value if x.value is None else str(x.value), + "width": x.width, + "height": x.height, + "type": x.type, + } + show_image_props_btn.click(show_image_props, b, output_b) + + def change_image_size(x: gr.Image): + x.width = 400 + x.height = 400 + return x + + change_image_size_btn.click(change_image_size, b, b).then( + show_image_props, b, output_b + ) + + def reset_image(x: gr.Image): + x.width = 300 + x.height = 300 + x.value = get_image("cheetah.jpg") + return x + + reset_image_btn.click(reset_image, b, b).then( + show_image_props, b, output_b + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/concurrency_with_queue/run.py b/demo/concurrency_with_queue/run.py new file mode 100644 index 0000000..8e3d5e1 --- /dev/null +++ b/demo/concurrency_with_queue/run.py @@ -0,0 +1,14 @@ +import gradio as gr +import time + +def say_hello(name): + time.sleep(5) + return f"Hello {name}!" + +with gr.Blocks() as demo: + inp = gr.Textbox() + outp = gr.Textbox() + button = gr.Button() + button.click(say_hello, inp, outp) + + demo.launch() diff --git a/demo/concurrency_without_queue/run.py b/demo/concurrency_without_queue/run.py new file mode 100644 index 0000000..816a73e --- /dev/null +++ b/demo/concurrency_without_queue/run.py @@ -0,0 +1,14 @@ +import gradio as gr +import time + +def say_hello(name): + time.sleep(5) + return f"Hello {name}!" + +with gr.Blocks() as demo: + inp = gr.Textbox() + outp = gr.Textbox() + button = gr.Button() + button.click(say_hello, inp, outp) + + demo.launch(max_threads=41) diff --git a/demo/copy_events/run.py b/demo/copy_events/run.py new file mode 100644 index 0000000..778c524 --- /dev/null +++ b/demo/copy_events/run.py @@ -0,0 +1,22 @@ +import gradio as gr + +md = "This is **bold** text." + +def copy_callback(copy_data: gr.CopyData): + return copy_data.value + +with gr.Blocks() as demo: + textbox = gr.Textbox(label="Copied text") + with gr.Row(): + markdown = gr.Markdown(value=md, header_links=True, height=400, buttons=["copy"]) + chatbot = gr.Chatbot([("Hello", "World"), ("Goodbye", "World")], buttons=["copy"]) # type: ignore + textbox2 = gr.Textbox("Write something here", interactive=True, buttons=["copy"]) + + gr.on( + [markdown.copy, chatbot.copy, textbox2.copy], + copy_callback, + outputs=textbox + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/count_generator/run.py b/demo/count_generator/run.py new file mode 100644 index 0000000..d9fc3b0 --- /dev/null +++ b/demo/count_generator/run.py @@ -0,0 +1,25 @@ +import gradio as gr +import time + +def count(n): + for i in range(int(n)): + time.sleep(0.5) + yield i + +def show(n): + return str(list(range(int(n)))) + +with gr.Blocks() as demo: + with gr.Column(): + num = gr.Number(value=10) + with gr.Row(): + count_btn = gr.Button("Count") + list_btn = gr.Button("List") + with gr.Column(): + out = gr.Textbox() + + count_btn.click(count, num, out) + list_btn.click(show, num, out) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/custom_css/custom_css.css b/demo/custom_css/custom_css.css new file mode 100644 index 0000000..f65ffa1 --- /dev/null +++ b/demo/custom_css/custom_css.css @@ -0,0 +1,64 @@ +/* CSSKeyframesRule for animation */ +@keyframes animation { + from {background-color: red;} + to {background-color: blue;} +} + +.cool-col { + background-color: red; + animation-name: animation; + animation-duration: 4s; + animation-delay: 2s; + animation-iteration-count: infinite; + border-radius: 10px; + padding: 20px; +} + +/* CSSStyleRule */ +.markdown { + background-color: lightblue; + padding: 20px; +} + +.markdown p { + color: royalblue; +} + +/* CSSMediaRule */ +@media screen and (max-width: 600px) { + .markdown { + background: blue; + } + .markdown p { + color: lightblue; + } +} + +.dark .markdown { + background: pink; +} + +.darktest h3 { + color: black; +} + +.dark .darktest h3 { + color: yellow; +} + +/* CSSFontFaceRule */ +@font-face { + font-family: "test-font"; + src: url("https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf") format("truetype"); +} + +.cool-col { + font-family: "test-font"; +} + +/* CSSImportRule */ +@import url("https://fonts.googleapis.com/css2?family=Protest+Riot&display=swap"); + +.markdown { + font-family: "Protest Riot", sans-serif; +} \ No newline at end of file diff --git a/demo/custom_css/run.py b/demo/custom_css/run.py new file mode 100644 index 0000000..ecd96dd --- /dev/null +++ b/demo/custom_css/run.py @@ -0,0 +1,12 @@ +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"]) diff --git a/demo/custom_path/run.py b/demo/custom_path/run.py new file mode 100644 index 0000000..06722d1 --- /dev/null +++ b/demo/custom_path/run.py @@ -0,0 +1,59 @@ +""" +Test script for https://github.com/gradio-app/gradio/issues/11848 +Gradio does not show media when FastAPI is behind a reverse proxy (root_path). + +Simulates a reverse proxy by wrapping the FastAPI app in an outer +Starlette app mounted at /myapp. Gradio is mounted at /gradio inside. + +Open http://localhost:8000/myapp/gradio/ to test. +""" + +import os +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from starlette.applications import Starlette +from starlette.routing import Mount +import gradio as gr + +CUSTOM_PATH = "/gradio" +PROXY_PREFIX = "/myapp" + +app = FastAPI() + + +@app.get("/") +def read_main(): + return {"message": "This is your main app"} + + +def identity(image): + return image + + +demo = gr.Interface( + fn=identity, + inputs=gr.Image(type="filepath"), + outputs=gr.Image(label="Output Image"), +) + +app = gr.mount_gradio_app(app, demo, path=CUSTOM_PATH, root_path=f"{PROXY_PREFIX}{CUSTOM_PATH}") + + +@asynccontextmanager +async def lifespan(outer_app): + async with app.router.lifespan_context(app): + yield + + +outer_app = Starlette( + routes=[Mount(PROXY_PREFIX, app=app)], + lifespan=lifespan, +) + +if __name__ == "__main__": + import uvicorn + + port = int(os.environ.get("GRADIO_SERVER_PORT", 8000)) + print(f"\nOpen http://localhost:{port}{PROXY_PREFIX}{CUSTOM_PATH}/\n") + uvicorn.run(outer_app, host="0.0.0.0", port=port) diff --git a/demo/dashboard/DESCRIPTION.md b/demo/dashboard/DESCRIPTION.md new file mode 100644 index 0000000..6bd25ee --- /dev/null +++ b/demo/dashboard/DESCRIPTION.md @@ -0,0 +1 @@ +This demo shows how you can build an interactive dashboard with gradio. Click on a python library on the left hand side and then on the right hand side click on the metric you'd like to see plot over time. Data is pulled from HuggingFace Hub datasets. \ No newline at end of file diff --git a/demo/dashboard/helpers.py b/demo/dashboard/helpers.py new file mode 100644 index 0000000..01505f6 --- /dev/null +++ b/demo/dashboard/helpers.py @@ -0,0 +1,161 @@ +# type: ignore +import collections +from datetime import datetime + +from datasets import DatasetDict, load_dataset +import numpy as np + +datasets = { + "stars": load_dataset("open-source-metrics/stars").sort('dates'), + "issues": load_dataset("open-source-metrics/issues").sort('dates'), + "pip": load_dataset("open-source-metrics/pip").sort('day') +} + +val = 0 + +def _range(e): + global val + e['range'] = val + val += 1 + + current_date = datetime.strptime(e['dates'], "%Y-%m-%dT%H:%M:%SZ") + first_date = datetime.fromtimestamp(1) + week = abs(current_date - first_date).days // 7 + e['week'] = week + + return e + +def _ignore_org_members(e): + global val + e['range_non_org'] = val + + if e['type']['authorAssociation'] != 'MEMBER': + val += 1 + + return e + +stars = {} +for k, v in datasets['stars'].items(): + stars[k] = v.map(_range) + val = 0 + +issues = {} +for k, v in datasets['issues'].items(): + issues[k] = v.map(_range) + val = 0 + issues[k] = issues[k].map(_ignore_org_members) + val = 0 + +datasets['stars'] = DatasetDict(**stars) +datasets['issues'] = DatasetDict(**issues) + +def link_values(library_names, returned_values): + previous_values = {library_name: None for library_name in library_names} + for library_name in library_names: + for i in returned_values.keys(): + if library_name not in returned_values[i]: + returned_values[i][library_name] = previous_values[library_name] + else: + previous_values[library_name] = returned_values[i][library_name] + + return returned_values + +def running_mean(x, N, total_length=-1): + cumsum = np.cumsum(np.insert(x, 0, 0)) + to_pad = max(total_length - len(cumsum), 0) + return np.pad(cumsum[N:] - cumsum[:-N], (to_pad, 0)) / float(N) + +def retrieve_pip_installs(library_names, cumulated): + + if cumulated: + returned_values = {} + for library_name in library_names: + for i in datasets['pip'][library_name]: + if i['day'] in returned_values: + returned_values[i['day']]['Cumulated'] += i['num_downloads'] + else: + returned_values[i['day']] = {'Cumulated': i['num_downloads']} + + library_names = ['Cumulated'] + + else: + returned_values = {} + for library_name in library_names: + for i in datasets['pip'][library_name]: + if i['day'] in returned_values: + returned_values[i['day']][library_name] = i['num_downloads'] + else: + returned_values[i['day']] = {library_name: i['num_downloads']} + + for library_name in library_names: + for i in returned_values.keys(): + if library_name not in returned_values[i]: + returned_values[i][library_name] = None + + returned_values = collections.OrderedDict(sorted(returned_values.items())) + output = {l: [k[l] for k in returned_values.values()] for l in library_names} + output['day'] = list(returned_values.keys()) + return output + +def retrieve_stars(libraries, week_over_week): + returned_values = {} + dataset_dict = datasets['stars'] + + for library_name in libraries: + dataset = dataset_dict[library_name] + + last_value = 0 + last_week = dataset[0]['week'] + for i in dataset: + if week_over_week and last_week == i['week']: + continue + if i['dates'] in returned_values: + returned_values[i['dates']][library_name] = i['range'] - last_value + else: + returned_values[i['dates']] = {library_name: i['range'] - last_value} + + last_value = i['range'] if week_over_week else 0 + last_week = i['week'] + + returned_values = collections.OrderedDict(sorted(returned_values.items())) + returned_values = link_values(libraries, returned_values) + output = {l: [k[l] for k in returned_values.values()][::-1] for l in libraries} + output['day'] = list(returned_values.keys())[::-1] + + # Trim down to a smaller number of points. + output = {k: [v for i, v in enumerate(value) if i % int(len(value) / 100) == 0] for k, value in output.items()} + return output + +def retrieve_issues(libraries, exclude_org_members, week_over_week): + + returned_values = {} + dataset_dict = datasets['issues'] + range_id = 'range' if not exclude_org_members else 'range_non_org' + + for library_name in libraries: + dataset = dataset_dict[library_name] + + last_value = 0 + last_week = dataset[0]['week'] + for i in dataset: + if week_over_week and last_week == i['week']: + continue + + if i['dates'] in returned_values: + returned_values[i['dates']][library_name] = i[range_id] - last_value + else: + returned_values[i['dates']] = {library_name: i[range_id] - last_value} + + last_value = i[range_id] if week_over_week else 0 + last_week = i['week'] + + returned_values = collections.OrderedDict(sorted(returned_values.items())) + returned_values = link_values(libraries, returned_values) + output = {l: [k[l] for k in returned_values.values()][::-1] for l in libraries} + output['day'] = list(returned_values.keys())[::-1] + + # Trim down to a smaller number of points. + output = { + k: [v for i, v in enumerate(value) if i % int(len(value) / 100) == 0] for k, value in output.items() + } + return output diff --git a/demo/dashboard/requirements.txt b/demo/dashboard/requirements.txt new file mode 100644 index 0000000..b68957b --- /dev/null +++ b/demo/dashboard/requirements.txt @@ -0,0 +1,2 @@ +plotly +pandas diff --git a/demo/dashboard/run.py b/demo/dashboard/run.py new file mode 100644 index 0000000..81d79a0 --- /dev/null +++ b/demo/dashboard/run.py @@ -0,0 +1,65 @@ +import gradio as gr +import pandas as pd +import plotly.express as px # type: ignore +from helpers import retrieve_pip_installs, retrieve_stars, retrieve_issues # type: ignore + +LIBRARIES = ["accelerate", "datasets", "diffusers", "evaluate", "gradio", "hub_docs", + "huggingface_hub", "optimum", "pytorch_image_models", "tokenizers", "transformers"] + +def create_pip_plot(libraries, pip_choices): + if "Pip" not in pip_choices: + return gr.Plot(visible=False) + output = retrieve_pip_installs(libraries, "Cumulated" in pip_choices) + df = pd.DataFrame(output).melt(id_vars="day") + plot = px.line(df, x="day", y="value", color="variable", + title="Pip installs") + plot.update_layout(legend=dict(x=0.5, y=0.99), title_x=0.5, legend_title_text="") + return gr.Plot(value=plot, visible=True) + +def create_star_plot(libraries, star_choices): + if "Stars" not in star_choices: + return gr.Plot(visible=False) + output = retrieve_stars(libraries, "Week over Week" in star_choices) + df = pd.DataFrame(output).melt(id_vars="day") + plot = px.line(df, x="day", y="value", color="variable", + title="Number of stargazers") + plot.update_layout(legend=dict(x=0.5, y=0.99), title_x=0.5, legend_title_text="") + return gr.Plot(value=plot, visible=True) + +def create_issue_plot(libraries, issue_choices): + if "Issue" not in issue_choices: + return gr.Plot(visible=False) + output = retrieve_issues(libraries, + exclude_org_members="Exclude org members" in issue_choices, + week_over_week="Week over Week" in issue_choices) + df = pd.DataFrame(output).melt(id_vars="day") + plot = px.line(df, x="day", y="value", color="variable", + title="Cumulated number of issues, PRs, and comments", + ) + plot.update_layout(legend=dict(x=0.5, y=0.99), title_x=0.5, legend_title_text="") + return gr.Plot(value=plot, visible=True) + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + gr.Markdown("## Select libraries to display") + libraries = gr.CheckboxGroup(choices=LIBRARIES, show_label=False) + with gr.Column(): + gr.Markdown("## Select graphs to display") + pip = gr.CheckboxGroup(choices=["Pip", "Cumulated"], show_label=False) + stars = gr.CheckboxGroup(choices=["Stars", "Week over Week"], show_label=False) + issues = gr.CheckboxGroup(choices=["Issue", "Exclude org members", "week over week"], show_label=False) + with gr.Row(): + fetch = gr.Button(value="Fetch") + with gr.Row(): + with gr.Column(): + pip_plot = gr.Plot(visible=False) + star_plot = gr.Plot(visible=False) + issue_plot = gr.Plot(visible=False) + + fetch.click(create_pip_plot, inputs=[libraries, pip], outputs=pip_plot) + fetch.click(create_star_plot, inputs=[libraries, stars], outputs=star_plot) + fetch.click(create_issue_plot, inputs=[libraries, issues], outputs=issue_plot) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dataframe_block-ui-test/run.py b/demo/dataframe_block-ui-test/run.py new file mode 100644 index 0000000..e5cfe43 --- /dev/null +++ b/demo/dataframe_block-ui-test/run.py @@ -0,0 +1,15 @@ +import gradio as gr + +with gr.Blocks() as demo: + count = gr.Slider(minimum=1, maximum=10, step=1, label="count") + data = gr.DataFrame( + headers=["A", "B"], col_count=(2, "fixed"), type="array", interactive=True # type: ignore + ) + btn = gr.Button(value="click") + btn.click( + fn=lambda cnt: [[str(2 * i), str(2 * i + 1)] for i in range(int(cnt))], + inputs=[count], + outputs=[data], + ) + +demo.launch() diff --git a/demo/dataframe_colorful/requirements.txt b/demo/dataframe_colorful/requirements.txt new file mode 100644 index 0000000..fb6c7ed --- /dev/null +++ b/demo/dataframe_colorful/requirements.txt @@ -0,0 +1 @@ +pandas diff --git a/demo/dataframe_colorful/run.py b/demo/dataframe_colorful/run.py new file mode 100644 index 0000000..38f146c --- /dev/null +++ b/demo/dataframe_colorful/run.py @@ -0,0 +1,20 @@ +import pandas as pd +import gradio as gr + +df = pd.DataFrame( + { + "A": [14, 4, 5, 4, 1], + "B": [5, 2, 54, 3, 2], + "C": [20, 20, 7, 3, 8], + "D": [14, 3, 6, 2, 6], + "E": [23, 45, 64, 32, 23], + } +) + +t = df.style.highlight_max(color="lightgreen", axis=0) + +with gr.Blocks() as demo: + gr.Dataframe(t) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dataframe_component/run.py b/demo/dataframe_component/run.py new file mode 100644 index 0000000..64fd22e --- /dev/null +++ b/demo/dataframe_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Dataframe(interactive=True) + +demo.launch() diff --git a/demo/dataframe_custom_styling/run.py b/demo/dataframe_custom_styling/run.py new file mode 100644 index 0000000..a543b13 --- /dev/null +++ b/demo/dataframe_custom_styling/run.py @@ -0,0 +1,43 @@ +import gradio as gr + +data = [ + ["DeepSeek Coder", 79.3], + ["Llama 3.3", 68.9], + ["Qwen 2.5", 61.9], + ["Gemma 2", 59.5], + ["GPT 2", 18.3], +] + +headers = ["Model", "% Correct (LeetCode Hard)"] + +def get_styling(values): + return [["", f"background: linear-gradient(90deg, rgba(220, 242, 220) {row[1]}%, transparent {row[1]}%)"] for row in values] + +def get_display_value(values): + display_values = [] + medals = ["🥇", "🥈", "🥉"] + for i, row in enumerate(values): + if i < 3: + display_values.append([f"{medals[i]} {row[0]}", row[1]]) + else: + display_values.append([row[0], row[1]]) + return display_values + +styling = get_styling(data) +display_value = get_display_value(data) + + +value = { + "data": data, + "headers": headers, + "metadata": { + "styling": styling, + "display_value": display_value, + }, +} + +with gr.Blocks() as demo: + gr.Dataframe(value, show_search="search") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dataframe_datatype/requirements.txt b/demo/dataframe_datatype/requirements.txt new file mode 100644 index 0000000..5da331c --- /dev/null +++ b/demo/dataframe_datatype/requirements.txt @@ -0,0 +1,2 @@ +numpy +pandas diff --git a/demo/dataframe_datatype/run.py b/demo/dataframe_datatype/run.py new file mode 100644 index 0000000..bea0505 --- /dev/null +++ b/demo/dataframe_datatype/run.py @@ -0,0 +1,19 @@ +import gradio as gr +import pandas as pd +import numpy as np + +def make_dataframe(n_periods): + rng = np.random.default_rng() + return pd.DataFrame({"date_1": pd.date_range("2021-01-01", periods=n_periods), + "date_2": pd.date_range("2022-02-15", periods=n_periods).strftime('%B %d, %Y, %r'), + "number": rng.random(n_periods).astype(np.float64), + "number_2": rng.integers(0, 100, n_periods).astype(np.int32), + "bool": [True] * n_periods, + "markdown": ["# Hello"] * n_periods}) + +demo = gr.Interface(make_dataframe, + gr.Number(precision=0), + gr.Dataframe(datatype=["date", "date", "number", "number", "bool", "markdown"])) # type: ignore + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dataframe_datetime_long/run.py b/demo/dataframe_datetime_long/run.py new file mode 100644 index 0000000..849e4ff --- /dev/null +++ b/demo/dataframe_datetime_long/run.py @@ -0,0 +1,78 @@ +import random + +import gradio as gr + +ROWS = 5000 +rng = random.Random(42) + +headers = [ + "date", + "str_short", + "str_long", + "num", + "bool", + "markdown", + "html", +] +datatype = ["date", "str", "str", "number", "bool", "markdown", "html"] + +WORDS = [ + "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", + "iota", "kappa", "lambda", "mu", "nu", "xi", "omicron", "pi", +] + +MD_WRAPPERS = [ + lambda s: f"**{s}**", + lambda s: f"*{s}*", + lambda s: f"`{s}`", + lambda s: f"[{s}](https://example.com)", + lambda s: f"# {s}", + lambda s: s, +] + +HTML_WRAPPERS = [ + lambda s: f"{s}", + lambda s: f"{s}", + lambda s: f'{s}', + lambda s: f'{s}', + lambda s: f"{s}", + lambda s: s, +] + + +def random_text(min_words: int, max_words: int) -> str: + n = rng.randint(min_words, max_words) + return " ".join(rng.choice(WORDS) for _ in range(n)) + + +def random_md() -> str: + return rng.choice(MD_WRAPPERS)(random_text(1, 8)) + + +def random_html() -> str: + return rng.choice(HTML_WRAPPERS)(random_text(1, 8)) + + +data = [ + [ + f"2026-01-{(i % 28) + 1:02d}", + rng.choice(WORDS), + random_text(2, 6), + round(rng.random() * 1000, 2), + rng.random() > 0.5, + random_md(), + random_html(), + ] + for i in range(ROWS) +] + +with gr.Blocks() as demo: + gr.Markdown( + f"### Reproduction for #13279: {ROWS} rows × mixed dtypes (markdown/html/date/number/bool/str)" + ) + gr.Dataframe( + value=data, headers=headers, datatype=datatype, interactive=False # type: ignore + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dataframe_events/run.py b/demo/dataframe_events/run.py new file mode 100644 index 0000000..51320d4 --- /dev/null +++ b/demo/dataframe_events/run.py @@ -0,0 +1,224 @@ +import gradio as gr +import pandas as pd +import numpy as np + + +def update_dataframe(): + regular_df = pd.DataFrame( + np.random.randint(1, 10, size=(5, 5)), + columns=pd.Index([str(i) for i in range(5)]), + ) + wide_df = pd.DataFrame( + [ + [5, 22, 91, 17, 73, 38, 84, 46, 65, 10, 155, 122, 11, 144, 133], + [81, 42, 13, 97, 33, 77, 59, 100, 29, 61, 213, 195, 142, 118, 127], + [37, 71, 63, 102, 28, 94, 19, 55, 88, 44, 116, 139, 122, 150, 147], + [104, 52, 49, 26, 83, 67, 31, 92, 79, 18, 241, 115, 159, 123, 137], + [16, 95, 74, 68, 43, 101, 27, 85, 39, 57, 129, 148, 132, 111, 156], + ], + columns=pd.Index([f"col_{i}" for i in range(15)]), + ) + return regular_df, wide_df + + +def clear_dataframes(): + regular_empty_df = pd.DataFrame([], columns=pd.Index([str(i) for i in range(5)])) + wide_empty_df = pd.DataFrame([], columns=pd.Index([f"col_{i}" for i in range(15)])) + return regular_empty_df, wide_empty_df + + +def increment_select_counter(evt: gr.SelectData, count): + count_val = 1 if count is None else count + 1 + return count_val, evt.index, evt.value + + +def edit_dataframe(evt: gr.EditData, count): + event_data = ", ".join( + [ + f"index: {evt.index}", + f"value: {evt.value}", + f"previous_value: {evt.previous_value}", + ] + ) + return event_data, count + 1 + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(scale=1): + initial_regular_df = pd.DataFrame( + np.zeros((5, 5), dtype=int), + columns=pd.Index([str(i) for i in range(5)]), + ) + + df = gr.Dataframe( + value=initial_regular_df, + interactive=True, + label="Interactive Dataframe", + show_label=True, + elem_id="dataframe", + show_search="filter", + buttons=["copy"], + show_row_numbers=True, + static_columns=[4], + ) + + with gr.Column(scale=1): + initial_wide_df = pd.DataFrame( + np.zeros((5, 15), dtype=int), + columns=pd.Index([f"col_{i}" for i in range(15)]), + ) + + df_view = gr.Dataframe( + value=initial_wide_df, + interactive=False, + label="Non-Interactive View (Scroll Horizontally)", + show_label=True, + show_search="search", + elem_id="non-interactive-dataframe", + buttons=["copy", "fullscreen"], + show_row_numbers=True, + ) + + tall_df_value = [ + ["DeepSeek Coder", 79.3, True], + ["Llama 3.3", 68.9, True], + ["Qwen 2.5", 61.9, True], + ["Gemma 2", 59.5, False], + ["GPT 2", 18.3, False], + ["DeepSeek Coder", 79.3, True], + ["Llama 3.3", 68.9, True], + ["Qwen 2.5", 61.9, True], + ["Gemma 2", 59.5, False], + ["GPT 2", 18.3, False], + ["DeepSeek Coder", 79.3, True], + ["Llama 3.3", 68.9, True], + ["Qwen 2.5", 61.9, True], + ["Gemma 2", 59.5, False], + ["GPT 2", 18.3, False], + ["DeepSeek Coder", 79.3, True], + ["Llama 3.3", 68.9, True], + ["Qwen 2.5", 61.9, True], + ["Gemma 2", 59.5, False], + ["GPT 2", 18.3, False], + ["DeepSeek Coder", 79.3, True], + ["Llama 3.3", 68.9, True], + ["Qwen 2.5", 61.9, True], + ["Gemma 2", 59.5, False], + ["GPT 2", 18.3, False], + ["DeepSeek Coder", 79.3, True], + ["Llama 3.3", 68.9, True], + ["Qwen 2.5", 61.9, True], + ["Gemma 2", 59.5, False], + ["GPT 2", 18.3, False], + ["DeepSeek Coder", 79.3, True], + ["Llama 3.3", 68.9, True], + ["Qwen 2.5", 61.9, True], + ["Gemma 2", 59.5, False], + ["GPT 2", 18.3, False], + ["DeepSeek Coder", 79.3, True], + ["Llama 3.3", 68.9, True], + ["Qwen 2.5", 61.9, True], + ["Gemma 2", 59.5, False], + ["GPT 2", 18.3, False], + ["DeepSeek Coder", 79.3, True], + ["Llama 3.3", 68.9, True], + ["Qwen 2.5", 61.9, True], + ["Gemma 2", 59.5, False], + ["GPT 2", 18.3, False], + ["DeepSeek Coder", 79.3, True], + ["Llama 3.3", 68.9, True], + ["Qwen 2.5", 61.9, True], + ["Gemma 2", 59.5, False], + ["GPT 2", 18.3, False], + ] + + def get_display_value(values): + display_values = [] + medals = ["🥇", "🥈", "🥉"] + for i, row in enumerate(values): + if i < 3: + display_values.append([f"{medals[i]} {row[0]}", row[1]]) + else: + display_values.append([row[0], row[1]]) + return display_values + + display_value = get_display_value(tall_df_value) + + tall_df_value = { + "data": tall_df_value, + "headers": ["Model", "% Correct (LeetCode Hard)", "Is Open Source"], + "metadata": {"display_value": display_value}, + } + + with gr.Row(): + with gr.Column(): + df_tall = gr.Dataframe( + value=tall_df_value, + interactive=False, + label="Tall Dataframe (Scroll Vertically)", + datatype=["str", "number", "bool"], # type: ignore + max_height=200, + show_label=True, + elem_id="dataframe_tall", + buttons=["copy"], + show_row_numbers=True, + show_search="search", + ) + + df_tall_selected_cell_index = gr.Textbox( + label="Tall dataframe selected cell index", + elem_id="tall_selected_cell_index", + ) + df_tall_selected_cell_value = gr.Textbox( + label="Tall dataframe selected cell value", + elem_id="tall_selected_cell_value", + ) + + with gr.Row(): + with gr.Column(): + update_btn = gr.Button("Update dataframe", elem_id="update_btn") + clear_btn = gr.Button("Clear dataframe", elem_id="clear_btn") + + with gr.Row(): + change_events = gr.Number( + value=0, label="Change events", elem_id="change_events" + ) + input_events = gr.Number(value=0, label="Input events", elem_id="input_events") + select_events = gr.Number( + value=0, label="Select events", elem_id="select_events" + ) + edit_events = gr.Number(value=0, label="Edit events", elem_id="edit_events") + + with gr.Row(): + selected_cell_index = gr.Textbox( + label="Selected cell index", elem_id="selected_cell_index" + ) + selected_cell_value = gr.Textbox( + label="Selected cell value", elem_id="selected_cell_value" + ) + edit_data = gr.Textbox(label="Edit event data", elem_id="edit_data") + + update_btn.click(fn=update_dataframe, outputs=[df, df_view]) + clear_btn.click(fn=clear_dataframes, outputs=[df, df_view]) + df.change(fn=lambda x: x + 1, inputs=[change_events], outputs=[change_events]) + df.edit(edit_dataframe, inputs=[edit_events], outputs=[edit_data, edit_events]) + df.input(fn=lambda x: x + 1, inputs=[input_events], outputs=[input_events]) + df.select( + fn=increment_select_counter, + inputs=[select_events], + outputs=[select_events, selected_cell_index, selected_cell_value], + ) + + df_tall.select( + fn=increment_select_counter, + inputs=[select_events], + outputs=[ + select_events, + df_tall_selected_cell_index, + df_tall_selected_cell_value, + ], + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dataframe_streaming/run.py b/demo/dataframe_streaming/run.py new file mode 100644 index 0000000..d5108b4 --- /dev/null +++ b/demo/dataframe_streaming/run.py @@ -0,0 +1,34 @@ +import gradio as gr +import pandas as pd +import time + +def update_dataframe(df): + df.iloc[:, :] = 1 + yield df, 1 + time.sleep(0.1) + df.iloc[:, :] = 2 + yield df, 2 + +def sum_values(df): + return pd.to_numeric(df.values.flatten(), errors='coerce').sum() # type: ignore + +initial_df = pd.DataFrame(0, index=range(5), columns=range(5)) + +with gr.Blocks() as demo: + with gr.Row(): + button = gr.Button("Update DataFrame") + number = gr.Number(value=0, label="Number") + dataframe = gr.Dataframe(value=initial_df, label="Dataframe") + + button.click(fn=update_dataframe, inputs=dataframe, outputs=[dataframe, number]) + with gr.Row(): + change_events = gr.Number(label="Change events") + input_events = gr.Number(label="Input events") + sum_of_values = gr.Number(label="Sum of values") + + dataframe.change(lambda x:x+1, inputs=change_events, outputs=change_events) + dataframe.input(lambda x:x+1, inputs=input_events, outputs=input_events) + dataframe.change(sum_values, inputs=dataframe, outputs=sum_of_values) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dataframe_tab/run.py b/demo/dataframe_tab/run.py new file mode 100644 index 0000000..6a3729d --- /dev/null +++ b/demo/dataframe_tab/run.py @@ -0,0 +1,12 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Tab(): + gr.HTML("

hi

") + with gr.Tab(): + gr.Dataframe( + value=[[i + 1] for i in range(10)], + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dataframe_tab_select/run.py b/demo/dataframe_tab_select/run.py new file mode 100644 index 0000000..205e894 --- /dev/null +++ b/demo/dataframe_tab_select/run.py @@ -0,0 +1,20 @@ +import gradio as gr + + +def get_data(): + return [[i, f"Item {i}"] for i in range(10)] + + +with gr.Blocks() as demo: + gr.Markdown("Switching to 'Tab 2' populates the dataframe via its select event.") + + with gr.Tab("Tab 1"): + gr.Markdown("Click 'Tab 2'.") + + with gr.Tab("Tab 2") as tab2: + df = gr.Dataframe(headers=["ID", "Name"], elem_id="tab_df") + tab2.select(get_data, outputs=df) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dataframe_widths/run.py b/demo/dataframe_widths/run.py new file mode 100644 index 0000000..cab5eb9 --- /dev/null +++ b/demo/dataframe_widths/run.py @@ -0,0 +1,262 @@ +import gradio as gr + +headers = ["Short", "Medium Column", "Long Description", "Also Long"] + +short_row = ["id-1", "alpha", "small cell", "small cell"] +long_row = [ + "id-2", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + ( + "This is a deliberately very long cell value intended to exceed any " + "reasonable column width so we can see truncation and wrapping behavior " + "kick in. It keeps going and going and going so that the column must " + "either truncate with an ellipsis or wrap across multiple lines." + ), + "The quick brown fox jumps over the lazy dog several times in a row.", +] +unbreakable_row = [ + "id-3", + "ok", + "Supercalifragilisticexpialidocious" * 4, + "word " * 30, +] + +data = [ + short_row, + long_row, + unbreakable_row, + short_row, + long_row, + unbreakable_row, + short_row, + long_row, + unbreakable_row, +] + +with gr.Blocks(title="Dataframe widths & wrapping") as demo: + gr.Markdown( + """ + # Dataframe column widths and wrapping + + Demonstrates how `wrap` and `column_widths` interact. In every case the + column's max width is capped at 100% of the dataframe's viewport so a + single cell can never be wider than the scroll area. + """ + ) + + with gr.Row(): + with gr.Tab("Defaults (no wrap, auto widths)"): + gr.Markdown( + "No `wrap`, no `column_widths`. Columns size to the longest cell; " + "anything longer than the viewport truncates with an ellipsis." + ) + gr.Dataframe(value=data, headers=headers) + + with gr.Tab("wrap=True, auto widths"): + gr.Markdown( + "`wrap=True` and no `column_widths`. Columns still size to the " + "longest cell, but content that would exceed the viewport wraps " + "onto multiple lines instead of truncating." + ) + gr.Dataframe(value=data, headers=headers, wrap=True) + + with gr.Tab("Fixed pixel widths"): + gr.Markdown( + "`column_widths=['80px', '160px', '240px', '200px']`. Pixel widths " + "are honored exactly; long content truncates." + ) + gr.Dataframe( + value=data, + headers=headers, + column_widths=["80px", "160px", "240px", "200px"], + ) + + with gr.Tab("Fixed pixel widths + wrap"): + gr.Markdown( + "Same fixed widths but `wrap=True`. Long content wraps within the " + "fixed column instead of truncating." + ) + gr.Dataframe( + value=data, + headers=headers, + column_widths=["80px", "160px", "240px", "200px"], + wrap=True, + ) + + with gr.Tab("Percentage widths"): + gr.Markdown( + "`column_widths=['10%', '25%', '40%', '25%']`. Percentages resolve " + "against the dataframe's viewport width, so they re-flow when you " + "resize the window." + ) + gr.Dataframe( + value=data, + headers=headers, + column_widths=["10%", "25%", "40%", "25%"], + ) + + with gr.Tab("Percentage widths + wrap"): + gr.Markdown("Same percentages, but with `wrap=True`.") + gr.Dataframe( + value=data, + headers=headers, + column_widths=["10%", "25%", "40%", "25%"], + wrap=True, + ) + + with gr.Tab("Mixed widths (px / % / int)"): + gr.Markdown( + "`column_widths=[120, '20%', 'auto', '30%']`. Integers are treated " + "as pixels; `'auto'` falls back to content-based sizing (capped at " + "the viewport)." + ) + gr.Dataframe( + value=data, + headers=headers, + column_widths=[120, "20%", "auto", "30%"], + ) + + with gr.Tab("max_chars with wide data"): + gr.Markdown( + "`max_chars=20` truncates cell text to 20 characters with an " + "ellipsis. The column is sized to the rendered (truncated) text " + "rather than the full underlying value, so there's no wasted " + "horizontal space." + ) + gr.Dataframe( + value=data, + headers=headers, + max_chars=20, + ) + + with gr.Tab("max_chars + fixed widths + wrap"): + gr.Markdown( + "`max_chars=15`, explicit pixel widths, and `wrap=True`. Text is " + "first truncated by character count, then wrapped to fit the " + "column." + ) + gr.Dataframe( + value=data, + headers=headers, + max_chars=15, + column_widths=["80px", "160px", "200px", "200px"], + wrap=True, + ) + + with gr.Row(): + with gr.Tab("Defaults (no wrap, auto widths)"): + gr.Markdown( + "No `wrap`, no `column_widths`. Columns size to the longest cell; " + "anything longer than the viewport truncates with an ellipsis." + ) + gr.Dataframe( + value=data, + headers=headers, + interactive=True, + ) + + with gr.Tab("wrap=True, auto widths"): + gr.Markdown( + "`wrap=True` and no `column_widths`. Columns still size to the " + "longest cell, but content that would exceed the viewport wraps " + "onto multiple lines instead of truncating." + ) + gr.Dataframe( + value=data, + headers=headers, + wrap=True, + interactive=True, + ) + + with gr.Tab("Fixed pixel widths"): + gr.Markdown( + "`column_widths=['80px', '160px', '240px', '200px']`. Pixel widths " + "are honored exactly; long content truncates." + ) + gr.Dataframe( + interactive=True, + value=data, + headers=headers, + column_widths=["80px", "160px", "240px", "200px"], + ) + + with gr.Tab("Fixed pixel widths + wrap"): + gr.Markdown( + "Same fixed widths but `wrap=True`. Long content wraps within the " + "fixed column instead of truncating." + ) + gr.Dataframe( + interactive=True, + value=data, + headers=headers, + column_widths=["80px", "160px", "240px", "200px"], + wrap=True, + ) + + with gr.Tab("Percentage widths"): + gr.Markdown( + "`column_widths=['10%', '25%', '40%', '25%']`. Percentages resolve " + "against the dataframe's viewport width, so they re-flow when you " + "resize the window." + ) + gr.Dataframe( + interactive=True, + value=data, + headers=headers, + column_widths=["10%", "25%", "40%", "25%"], + ) + + with gr.Tab("Percentage widths + wrap"): + gr.Markdown("Same percentages, but with `wrap=True`.") + gr.Dataframe( + value=data, + interactive=True, + headers=headers, + column_widths=["10%", "25%", "40%", "25%"], + wrap=True, + ) + + with gr.Tab("Mixed widths (px / % / int)"): + gr.Markdown( + "`column_widths=[120, '20%', 'auto', '30%']`. Integers are treated " + "as pixels; `'auto'` falls back to content-based sizing (capped at " + "the viewport)." + ) + gr.Dataframe( + value=data, + interactive=True, + headers=headers, + column_widths=[120, "20%", "auto", "30%"], + ) + + with gr.Tab("max_chars with wide data"): + gr.Markdown( + "`max_chars=20` truncates cell text to 20 characters with an " + "ellipsis. The column is sized to the rendered (truncated) text " + "rather than the full underlying value, so there's no wasted " + "horizontal space." + ) + gr.Dataframe( + interactive=True, + value=data, + headers=headers, + max_chars=20, + ) + + with gr.Tab("max_chars + fixed widths + wrap"): + gr.Markdown( + "`max_chars=15`, explicit pixel widths, and `wrap=True`. Text is " + "first truncated by character count, then wrapped to fit the " + "column." + ) + gr.Dataframe( + interactive=True, + value=data, + headers=headers, + max_chars=15, + column_widths=["80px", "160px", "200px", "200px"], + wrap=True, + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dataset/requirements.txt b/demo/dataset/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/dataset/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/dataset/run.py b/demo/dataset/run.py new file mode 100755 index 0000000..d8ceef0 --- /dev/null +++ b/demo/dataset/run.py @@ -0,0 +1,173 @@ +import gradio as gr +import numpy as np +# get_image(), get_video(), get_audio(), get_file(), get_model3d() return file paths to sample media included with Gradio +from gradio.media import get_image, get_video, get_audio, get_file, get_model3d + +txt = "the quick brown fox" +num = 10 + +img = get_image("cheetah1.jpg") +vid = get_video("world.mp4") +audio = get_audio("cantina.wav") +csv = get_file("time.csv") +model = get_model3d("Bunny.obj") + +dataframe = [[1, 2, 3, 4], [4, 5, 6, 7], [8, 9, 1, 2], [3, 4, 5, 6]] + +with gr.Blocks() as demo: + gr.Markdown("# Dataset previews") + a = gr.Audio(visible=False) + gr.Dataset( + components=[a], + label="Audio", + samples=[ + [audio], + [audio], + [audio], + [audio], + [audio], + [audio], + ], + ) + c = gr.Checkbox(visible=False) + gr.Dataset( + label="Checkbox", + components=[c], + samples=[[True], [True], [False], [True], [False], [False]], + ) + + c_2 = gr.CheckboxGroup(visible=False, choices=['a', 'b', 'c']) + gr.Dataset( + label="CheckboxGroup", + components=[c_2], + samples=[ + [["a"]], + [["a", "b"]], + [["a", "b", "c"]], + [["b"]], + [["c"]], + [["a", "c"]], + ], + ) + c_3 = gr.ColorPicker(visible=False) + gr.Dataset( + label="ColorPicker", + components=[c_3], + samples=[ + ["#FFFFFF"], + ["#000000"], + ["#FFFFFF"], + ["#000000"], + ["#FFFFFF"], + ["#000000"], + ], + ) + d = gr.DataFrame(visible=False) + gr.Dataset( + components=[d], + label="Dataframe", + samples=[ + [np.zeros((3, 3)).tolist()], + [np.ones((2, 2)).tolist()], + [np.random.randint(0, 10, (3, 10)).tolist()], + [np.random.randint(0, 10, (10, 3)).tolist()], + [np.random.randint(0, 10, (10, 10)).tolist()], + ], + ) + d_2 = gr.Dropdown(visible=False, choices=["one", "two", "three"]) + gr.Dataset( + components=[d_2], + label="Dropdown", + samples=[["one"], ["two"], ["three"], ["one"], ["two"], ["three"]], + ) + f = gr.File(visible=False) + gr.Dataset( + components=[f], + label="File", + samples=[ + [csv], + [csv], + [csv], + [csv], + [csv], + [csv], + ], + ) + h = gr.HTML(visible=False) + gr.Dataset( + components=[h], + label="HTML", + samples=[ + ["

hi

"], + ["

hi

"], + ["

hi

"], + ["

hi

"], + ["

hi

"], + ["

hi

"], + ], + ) + i = gr.Image(visible=False) + gr.Dataset( + components=[i], + label="Image", + samples=[[img], [img], [img], [img], [img], [img]], + ) + m = gr.Markdown(visible=False) + gr.Dataset( + components=[m], + label="Markdown", + samples=[ + ["# hi"], + ["# hi"], + ["# hi"], + ["# hi"], + ["# hi"], + ["# hi"], + ], + ) + m_2 = gr.Model3D(visible=False) + gr.Dataset( + components=[m_2], + label="Model3D", + samples=[[model], [model], [model], [model], [model], [model]], + ) + n = gr.Number(visible=False) + gr.Dataset( + label="Number", + components=[n], + samples=[[1], [1], [1], [1], [1], [1]], + ) + r = gr.Radio(visible=False, choices=["one", "two", "three"]) + gr.Dataset( + components=[r], + label="Radio", + samples=[["one"], ["two"], ["three"], ["one"], ["two"], ["three"]], + ) + s = gr.Slider(visible=False) + gr.Dataset( + label="Slider", + components=[s], + samples=[[1], [1], [1], [1], [1], [1]], + ) + t = gr.Textbox(visible=False) + gr.Dataset( + label="Textbox", + components=[t], + samples=[ + ["Some value"], + ["Some value"], + ["Some value"], + ["Some value"], + ["Some value"], + ["Some value"], + ], + ) + v = gr.Video(visible=False) + gr.Dataset( + components=[v], + label="Video", + samples=[[vid], [vid], [vid], [vid], [vid], [vid]], + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dataset_component/run.py b/demo/dataset_component/run.py new file mode 100644 index 0000000..10be920 --- /dev/null +++ b/demo/dataset_component/run.py @@ -0,0 +1,15 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Dataset(components=[gr.Textbox(visible=False)], + label="Text Dataset", + samples=[ + ["The quick brown fox jumps over the lazy dog"], + ["Build & share delightful machine learning apps"], + ["She sells seashells by the seashore"], + ["Supercalifragilisticexpialidocious"], + ["Lorem ipsum"], + ["That's all folks!"] + ], + ) +demo.launch() diff --git a/demo/datetime_component/run.py b/demo/datetime_component/run.py new file mode 100644 index 0000000..3f58eeb --- /dev/null +++ b/demo/datetime_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.DateTime() + +demo.launch() diff --git a/demo/datetimes/run.py b/demo/datetimes/run.py new file mode 100644 index 0000000..5d5b2e0 --- /dev/null +++ b/demo/datetimes/run.py @@ -0,0 +1,26 @@ +import gradio as gr + +with gr.Blocks() as demo: + date1 = gr.DateTime(include_time=True, label="Date and Time", type="datetime", elem_id="date1") + date2 = gr.DateTime(include_time=False, label="Date Only", type="string", elem_id="date2") + date3 = gr.DateTime(elem_id="date3", timezone="Europe/Paris") + + with gr.Row(): + btn1 = gr.Button("Load Date 1") + btn2 = gr.Button("Load Date 2") + btn3 = gr.Button("Load Date 3") + + click_output = gr.Textbox(label="Last Load") + change_output = gr.Textbox(label="Last Change") + submit_output = gr.Textbox(label="Last Submit") + + btn1.click(lambda x:x, date1, click_output) + btn2.click(lambda x:x, date2, click_output) + btn3.click(lambda x:x, date3, click_output) + + for item in [date1, date2, date3]: + item.change(lambda x:x, item, change_output) + item.submit(lambda x:x, item, submit_output) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/deep_link/run.py b/demo/deep_link/run.py new file mode 100644 index 0000000..5b352c0 --- /dev/null +++ b/demo/deep_link/run.py @@ -0,0 +1,17 @@ +import gradio as gr +import random + +def random_response(message, history): + return random.choice(["Hi!", "Hello!", "Greetings!"]) + +with gr.Blocks() as demo: + gr.ChatInterface( + random_response, + title="Greeting Bot", + description="Ask anything and receive a nice greeting!", + api_name="chat", + ) + gr.DeepLinkButton() + +if __name__ == "__main__": + demo.launch(share=True) diff --git a/demo/depth_estimation/DESCRIPTION.md b/demo/depth_estimation/DESCRIPTION.md new file mode 100644 index 0000000..ae8ed89 --- /dev/null +++ b/demo/depth_estimation/DESCRIPTION.md @@ -0,0 +1 @@ +A demo for predicting the depth of an image and generating a 3D model of it. \ No newline at end of file diff --git a/demo/depth_estimation/examples/1-jonathan-borba-CgWTqYxHEkg-unsplash.jpg b/demo/depth_estimation/examples/1-jonathan-borba-CgWTqYxHEkg-unsplash.jpg new file mode 100644 index 0000000..811c174 Binary files /dev/null and b/demo/depth_estimation/examples/1-jonathan-borba-CgWTqYxHEkg-unsplash.jpg differ diff --git a/demo/depth_estimation/packages.txt b/demo/depth_estimation/packages.txt new file mode 100644 index 0000000..a72c3b8 --- /dev/null +++ b/demo/depth_estimation/packages.txt @@ -0,0 +1 @@ +libgl1-mesa-glx \ No newline at end of file diff --git a/demo/depth_estimation/requirements.txt b/demo/depth_estimation/requirements.txt new file mode 100644 index 0000000..2523321 --- /dev/null +++ b/demo/depth_estimation/requirements.txt @@ -0,0 +1,6 @@ +torch +git+https://github.com/nielsrogge/transformers.git@add_dpt_redesign#egg=transformers +numpy +Pillow +jinja2 +open3d \ No newline at end of file diff --git a/demo/depth_estimation/run.py b/demo/depth_estimation/run.py new file mode 100644 index 0000000..928bc01 --- /dev/null +++ b/demo/depth_estimation/run.py @@ -0,0 +1,119 @@ +# type: ignore +import gradio as gr +from transformers import DPTFeatureExtractor, DPTForDepthEstimation +import torch +import numpy as np +from PIL import Image +import open3d as o3d # type: ignore +from pathlib import Path + +feature_extractor = DPTFeatureExtractor.from_pretrained("Intel/dpt-large") +model = DPTForDepthEstimation.from_pretrained("Intel/dpt-large") + +def process_image(image_path): + image_path = Path(image_path) + image_raw = Image.open(image_path) + image = image_raw.resize( + (800, int(800 * image_raw.size[1] / image_raw.size[0])), + Image.Resampling.LANCZOS) + + # prepare image for the model + encoding = feature_extractor(image, return_tensors="pt") # type: ignore + + # forward pass + with torch.no_grad(): + outputs = model(**encoding) # type: ignore + predicted_depth = outputs.predicted_depth + + # interpolate to original size + prediction = torch.nn.functional.interpolate( + predicted_depth.unsqueeze(1), + size=image.size[::-1], + mode="bicubic", + align_corners=False, + ).squeeze() + output = prediction.cpu().numpy() + depth_image = (output * 255 / np.max(output)).astype('uint8') + try: + gltf_path = create_3d_obj(np.array(image), depth_image, image_path) + img = Image.fromarray(depth_image) + return [img, gltf_path, gltf_path] + except Exception: + gltf_path = create_3d_obj( + np.array(image), depth_image, image_path, depth=8) + img = Image.fromarray(depth_image) + return [img, gltf_path, gltf_path] + except: + print("Error reconstructing 3D model") + raise Exception("Error reconstructing 3D model") + +def create_3d_obj(rgb_image, depth_image, image_path, depth=10): + depth_o3d = o3d.geometry.Image(depth_image) + image_o3d = o3d.geometry.Image(rgb_image) + rgbd_image = o3d.geometry.RGBDImage.create_from_color_and_depth( + image_o3d, depth_o3d, convert_rgb_to_intensity=False) + w = int(depth_image.shape[1]) + h = int(depth_image.shape[0]) + + camera_intrinsic = o3d.camera.PinholeCameraIntrinsic() + camera_intrinsic.set_intrinsics(w, h, 500, 500, w/2, h/2) + + pcd = o3d.geometry.PointCloud.create_from_rgbd_image( + rgbd_image, camera_intrinsic) + + print('normals') + pcd.normals = o3d.utility.Vector3dVector( + np.zeros((1, 3))) # invalidate existing normals + pcd.estimate_normals( + search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.01, max_nn=30)) + pcd.orient_normals_towards_camera_location( + camera_location=np.array([0., 0., 1000.])) + pcd.transform([[1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, -1, 0], + [0, 0, 0, 1]]) + pcd.transform([[-1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]]) + + print('run Poisson surface reconstruction') + with o3d.utility.VerbosityContextManager(o3d.utility.VerbosityLevel.Debug): + mesh_raw, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson( + pcd, depth=depth, width=0, scale=1.1, linear_fit=True) + + voxel_size = max(mesh_raw.get_max_bound() - mesh_raw.get_min_bound()) / 256 + print(f'voxel_size = {voxel_size:e}') + mesh = mesh_raw.simplify_vertex_clustering( + voxel_size=voxel_size, + contraction=o3d.geometry.SimplificationContraction.Average) + + # vertices_to_remove = densities < np.quantile(densities, 0.001) + # mesh.remove_vertices_by_mask(vertices_to_remove) + bbox = pcd.get_axis_aligned_bounding_box() + mesh_crop = mesh.crop(bbox) + gltf_path = f'./{image_path.stem}.gltf' + o3d.io.write_triangle_mesh( + gltf_path, mesh_crop, write_triangle_uvs=True) + return gltf_path + +title = "Demo: zero-shot depth estimation with DPT + 3D Point Cloud" +description = "This demo is a variation from the original DPT Demo. It uses the DPT model to predict the depth of an image and then uses 3D Point Cloud to create a 3D object." +examples = [["examples/1-jonathan-borba-CgWTqYxHEkg-unsplash.jpg"]] + +iface = gr.Interface(fn=process_image, + inputs=[gr.Image( + type="filepath", label="Input Image")], + outputs=[gr.Image(label="predicted depth", type="pil"), + gr.Model3D(label="3d mesh reconstruction", clear_color=( + 1.0, 1.0, 1.0, 1.0)), + gr.File(label="3d gLTF")], + title=title, + description=description, + examples=examples, + flagging_mode="never", + cache_examples=False, + api_name="predict", + ) + +iface.launch(debug=True) diff --git a/demo/dia_dialogue_demo/run.py b/demo/dia_dialogue_demo/run.py new file mode 100644 index 0000000..64c338d --- /dev/null +++ b/demo/dia_dialogue_demo/run.py @@ -0,0 +1,108 @@ +import gradio as gr +import httpx + + +tags = [ + "(laughs)", + "(clears throat)", + "(sighs)", + "(gasps)", + "(coughs)", + "(singing)", + "(sings)", + "(mumbles)", + "(beep)", + "(groans)", + "(sniffs)", + "(claps)", + "(screams)", + "(inhales)", + "(exhales)", + "(applause)", + "(burps)", + "(humming)", + "(sneezes)", + "(chuckle)", + "(whistles)", +] +speakers = ["Speaker 1", "Speaker 2"] + +client = httpx.AsyncClient(timeout=180) +API_URL = "https://router.huggingface.co/fal-ai/fal-ai/dia-tts" + + +async def query(dialogue: str, token: gr.OAuthToken | None): + if token is None: + raise gr.Error( + "No token provided. Use Sign in with Hugging Face to get a token." + ) + headers = { + "Authorization": f"Bearer {token.token}", + } + response = await client.post(API_URL, headers=headers, json={"text": dialogue}) + url = response.json()["audio"]["url"] + print("URL: ", url) + return url + + +def formatter(speaker, text): + speaker = speaker.split(" ")[1] + return f"[S{speaker}] {text}" + + +with gr.Blocks() as demo: + with gr.Sidebar(): + login_button = gr.LoginButton() + gr.HTML( + """ +

+ Dancing Huggy Dia Dialogue Generation Model +

+

Model by   Nari Labs. Powered by HF and   Fal AI  API.

+

Dia is a dialogue generation model that can generate realistic dialogue between two speakers. Use the dialogue component to create a conversation and then hit the submit button in the bottom right corner to see it come to life .

+ """ + ) + with gr.Row(): + with gr.Column(): + dialogue = gr.Dialogue( + speakers=speakers, tags=tags, formatter=formatter + ) + with gr.Column(): + with gr.Row(): + audio = gr.Audio(label="Audio") + with gr.Row(): + gr.DeepLinkButton(value="Share Audio via Link") + with gr.Row(): + gr.Examples( + examples=[ + [ + [ + { + "speaker": "Speaker 1", + "text": "Why did the chicken cross the road?", + }, + {"speaker": "Speaker 2", "text": "I don't know!"}, + { + "speaker": "Speaker 1", + "text": "to get to the other side! (laughs)", + }, + ] + ], + [ + [ + { + "speaker": "Speaker 1", + "text": "I am a little tired today (sighs).", + }, + {"speaker": "Speaker 2", "text": "Hang in there!"}, + ] + ], + ], + inputs=[dialogue], + cache_examples=False, + ) + + dialogue.submit(query, [dialogue], audio) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dialogue_component/run.py b/demo/dialogue_component/run.py new file mode 100644 index 0000000..3117d24 --- /dev/null +++ b/demo/dialogue_component/run.py @@ -0,0 +1,12 @@ +import gradio as gr + +with gr.Blocks() as demo: + dd = gr.Dialogue(speakers=["Speaker 1", "Speaker 2"], + tags=["(laughs)", "(sighs)", "(clears throat)"], + value=[ + {"speaker": "Speaker 1", "text": "Hello, how are you?"}, + {"speaker": "Speaker 2", "text": "I'm fine, thank you!"}, + ], separator="\n", interactive=True) + output = gr.Textbox(label="Output", value="") + dd.submit(lambda x: x, inputs=dd, outputs=output) +demo.launch() diff --git a/demo/dialogue_diarization_demo/requirements.txt b/demo/dialogue_diarization_demo/requirements.txt new file mode 100644 index 0000000..548affd --- /dev/null +++ b/demo/dialogue_diarization_demo/requirements.txt @@ -0,0 +1,9 @@ +gradio +torch +torchaudio +pyannote.audio +openai-whisper +librosa +numpy +transformers +speechbrain \ No newline at end of file diff --git a/demo/dialogue_diarization_demo/run.py b/demo/dialogue_diarization_demo/run.py new file mode 100644 index 0000000..5a0a6e9 --- /dev/null +++ b/demo/dialogue_diarization_demo/run.py @@ -0,0 +1,126 @@ +# type: ignore +import gradio as gr +from pyannote.audio import Pipeline +import whisper + +diarization_pipeline = None +whisper_model = None + + +def load_models(): + global diarization_pipeline, whisper_model # noqa: PLW0603 + + if diarization_pipeline is None: + diarization_pipeline = Pipeline.from_pretrained( + "pyannote/speaker-diarization-3.1", use_auth_token=True + ) + + if whisper_model is None: + whisper_model = whisper.load_model("base") + + +def real_diarization(audio_file_path: str) -> list[dict[str, str]]: + try: + load_models() + + if diarization_pipeline is None or whisper_model is None: + raise Exception("Failed to load models") + + diarization = diarization_pipeline(audio_file_path) + + transcription = whisper_model.transcribe(audio_file_path) + segments = transcription["segments"] + + dialogue_segments = [] + speaker_mapping = {} + speaker_counter = 1 + + for segment in segments: + start_time = segment["start"] + end_time = segment["end"] + text = segment["text"].strip() + + speaker = "Speaker 1" + for turn, _, speaker_label in diarization.itertracks(yield_label=True): + if ( + turn.start <= start_time <= turn.end + or turn.start <= end_time <= turn.end + ): + if speaker_label not in speaker_mapping: + speaker_mapping[speaker_label] = f"Speaker {speaker_counter}" + speaker_counter += 1 + speaker = speaker_mapping[speaker_label] + break + + if text: + dialogue_segments.append({"speaker": speaker, "text": text}) + + return dialogue_segments + + except Exception as e: + print(f"Error in diarization: {str(e)}") + return [] + + +def process_audio(audio_file): + if audio_file is None: + gr.Warning("Please upload an audio file first.") + return [] + + try: + dialogue_segments = real_diarization(audio_file) + return dialogue_segments + except Exception as e: + gr.Error(f"Error processing audio: {str(e)}") + return [] + + +speakers = [ + "Speaker 1", + "Speaker 2", + "Speaker 3", + "Speaker 4", + "Speaker 5", + "Speaker 6", +] +tags = [ + "(pause)", + "(background noise)", + "(unclear)", + "(overlap)", + "(phone ringing)", + "(door closing)", + "(music)", + "(applause)", + "(laughter)", +] + + +def format_speaker(speaker, text): + return f"{speaker}: {text}" + + +with gr.Blocks(title="Audio Diarization Demo") as demo: + with gr.Row(): + with gr.Column(scale=1): + audio_input = gr.Audio( + label="Upload Audio File", + type="filepath", + sources=["upload", "microphone"], + ) + + process_btn = gr.Button("🔍 Analyze Speakers", variant="primary", size="lg") + + with gr.Column(scale=2): + dialogue_output = gr.Dialogue( + speakers=speakers, + tags=tags, + formatter=format_speaker, + label="AI-generated speaker-separated conversation", + value=[], + ) + + process_btn.click(fn=process_audio, inputs=[audio_input], outputs=[dialogue_output]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/dialogue_mock_diarization/run.py b/demo/dialogue_mock_diarization/run.py new file mode 100644 index 0000000..cb6b470 --- /dev/null +++ b/demo/dialogue_mock_diarization/run.py @@ -0,0 +1,49 @@ +import gradio as gr + +speakers = [ + "Speaker 1", + "Speaker 2", +] + +def format_speaker(speaker, text): + return f"{speaker}: {text}" + +def mock_diarization(audio): + return [ + { + "speaker": "Speaker 1", + "text": "Hello, how are you?", + }, + { + "speaker": "Speaker 2", + "text": "I'm fine, thank you!", + }, + { + "speaker": "Speaker 1", + "text": "What's your name?", + }, + { + "speaker": "Speaker 2", + "text": "My name is John Doe.", + }, + { + "speaker": "Speaker 1", + "text": "Nice to meet you!", + }, + { + "speaker": "Speaker 2", + "text": "Nice to meet you!", + }, + ] + +demo = gr.Interface( + fn=mock_diarization, + inputs=[gr.Audio(sources=["microphone"])], + outputs=[gr.Dialogue(speakers=speakers, tags=None, formatter=format_speaker)], + title="Mock Speech Diarization", + description="Mock speech diarization", + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/diff_texts/run.py b/demo/diff_texts/run.py new file mode 100644 index 0000000..bd173d4 --- /dev/null +++ b/demo/diff_texts/run.py @@ -0,0 +1,36 @@ +from difflib import Differ + +import gradio as gr + +def diff_texts(text1, text2): + d = Differ() + return [ + (token[2:], token[0] if token[0] != " " else None) + for token in d.compare(text1, text2) + ] + +demo = gr.Interface( + diff_texts, + [ + gr.Textbox( + label="Text 1", + info="Initial text", + lines=3, + value="The quick brown fox jumped over the lazy dogs.", + ), + gr.Textbox( + label="Text 2", + info="Text to compare", + lines=3, + value="The fast brown fox jumps over lazy dogs.", + ), + ], + gr.HighlightedText( + label="Diff", + combine_adjacent=True, + show_legend=True, + color_map={"+": "red", "-": "green"}), + api_name="predict", +) +if __name__ == "__main__": + demo.launch(theme=gr.themes.Base()) diff --git a/demo/diff_texts/screenshot.png b/demo/diff_texts/screenshot.png new file mode 100644 index 0000000..dfa051f Binary files /dev/null and b/demo/diff_texts/screenshot.png differ diff --git a/demo/diffusers_with_batching/requirements.txt b/demo/diffusers_with_batching/requirements.txt new file mode 100644 index 0000000..b6ca9fb --- /dev/null +++ b/demo/diffusers_with_batching/requirements.txt @@ -0,0 +1,3 @@ +torch +transformers +diffusers \ No newline at end of file diff --git a/demo/diffusers_with_batching/run.py b/demo/diffusers_with_batching/run.py new file mode 100644 index 0000000..46c123e --- /dev/null +++ b/demo/diffusers_with_batching/run.py @@ -0,0 +1,23 @@ +import torch +from diffusers import DiffusionPipeline # type: ignore +import gradio as gr + +generator = DiffusionPipeline.from_pretrained("CompVis/ldm-text2im-large-256") +# move to GPU if available +if torch.cuda.is_available(): + generator = generator.to("cuda") + +def generate(prompts): + images = generator(list(prompts)).images # type: ignore + return [images] + +demo = gr.Interface(generate, + "textbox", + "image", + batch=True, + max_batch_size=4, # Set the batch size based on your CPU/GPU memory + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/downloadbutton_component/run.py b/demo/downloadbutton_component/run.py new file mode 100644 index 0000000..20b2ba6 --- /dev/null +++ b/demo/downloadbutton_component/run.py @@ -0,0 +1,7 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.DownloadButton("📂 Click to download file", value="http://www.marketingtool.online/en/face-generator/img/faces/avatar-1151ce9f4b2043de0d2e3b7826127998.jpg") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/draggable_dashboard/run.py b/demo/draggable_dashboard/run.py new file mode 100644 index 0000000..cbc6064 --- /dev/null +++ b/demo/draggable_dashboard/run.py @@ -0,0 +1,84 @@ +import gradio as gr +import numpy as np +import pandas as pd + +with gr.Blocks() as demo: + gr.Markdown("# Draggable Dashboard Demo") + gr.Markdown("Drag the charts around to reorder them!") + + x = np.linspace(0, 10, 100) + data = pd.DataFrame({ + 'x': x, + 'y1': np.random.normal(100, 20, 100) + 10 * np.sin(x), + 'y2': np.random.normal(500, 100, 100) + 50 * np.cos(x), + 'y3': np.random.normal(1000, 200, 100) + 100 * np.sin(x/2), + 'y4': np.random.normal(0.15, 0.05, 100) + 0.05 * np.cos(x/3) + }) + + with gr.Row(): + with gr.Column(scale=1): + gr.Markdown("### Horizontal Layout (orientation='row')") + with gr.Draggable(orientation="row"): + gr.LinePlot( + data, + x="x", + y="y1", + title="Chart 1", + height=200, + ) + gr.LinePlot( + data, + x="x", + y="y2", + title="Chart 2", + height=200, + ) + gr.LinePlot( + data, + x="x", + y="y3", + title="Chart 3", + height=200, + ) + gr.LinePlot( + data, + x="x", + y="y4", + title="Chart 4", + height=200, + ) + + with gr.Column(scale=1): + gr.Markdown("### Vertical Layout (orientation='column')") + with gr.Draggable(orientation="column"): + gr.LinePlot( + data, + x="x", + y="y1", + title="Chart 1", + height=200, + ) + gr.LinePlot( + data, + x="x", + y="y2", + title="Chart 2", + height=200, + ) + gr.LinePlot( + data, + x="x", + y="y3", + title="Chart 3", + height=200, + ) + gr.LinePlot( + data, + x="x", + y="y4", + title="Chart 4", + height=200, + ) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/dropdown_component/run.py b/demo/dropdown_component/run.py new file mode 100644 index 0000000..3d4872d --- /dev/null +++ b/demo/dropdown_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Dropdown(choices=["First Choice", "Second Choice", "Third Choice"]) + +demo.launch() diff --git a/demo/dropdown_custom_value/run.py b/demo/dropdown_custom_value/run.py new file mode 100644 index 0000000..41de9f3 --- /dev/null +++ b/demo/dropdown_custom_value/run.py @@ -0,0 +1,13 @@ +import gradio + +with gradio.Blocks() as demo: + dropdown = gradio.Dropdown( + choices=[("hello", "goodbye"), ("abc", "123")], + allow_custom_value=True, + label="Dropdown", + ) + text = gradio.Textbox(label="Output") + dropdown.change(lambda x: x, inputs=dropdown, outputs=text) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/dropdown_key_up/run.py b/demo/dropdown_key_up/run.py new file mode 100644 index 0000000..dbbc653 --- /dev/null +++ b/demo/dropdown_key_up/run.py @@ -0,0 +1,16 @@ +import gradio as gr + +def test(value, key_up_data: gr.KeyUpData): + return { + "component value": value, + "input value": key_up_data.input_value, + "key": key_up_data.key + } + +with gr.Blocks() as demo: + d = gr.Dropdown(["abc", "def"], allow_custom_value=True) + t = gr.JSON() + d.key_up(test, d, t) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/duplicatebutton_component/run.py b/demo/duplicatebutton_component/run.py new file mode 100644 index 0000000..c53983d --- /dev/null +++ b/demo/duplicatebutton_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.DuplicateButton() + +demo.launch() diff --git a/demo/event_trigger/img/a.jpg b/demo/event_trigger/img/a.jpg new file mode 100644 index 0000000..765f840 Binary files /dev/null and b/demo/event_trigger/img/a.jpg differ diff --git a/demo/event_trigger/img/b.jpg b/demo/event_trigger/img/b.jpg new file mode 100644 index 0000000..7774fb8 Binary files /dev/null and b/demo/event_trigger/img/b.jpg differ diff --git a/demo/event_trigger/run.py b/demo/event_trigger/run.py new file mode 100644 index 0000000..d515ddb --- /dev/null +++ b/demo/event_trigger/run.py @@ -0,0 +1,140 @@ +# %% +import gradio as gr + +TEST_VIDEO_A = "mp4/a.mp4" +TEST_VIDEO_B = "mp4/b.mp4" + +TEST_IMAGE_A = "img/a.jpg" +TEST_IMAGE_B = "img/b.jpg" + +def alert_change(component, value): + print(f"Detected {component} change, {type(value)}") + + if type(value) == list or type(value) == str: + print(value) + +def change_interactive(state): + return gr.Video(interactive=not state), not state + +with gr.Blocks() as demo: + with gr.Tab(label="Text change"): + with gr.Row(): + with gr.Column(): + textbox1 = gr.Textbox() + textbox2 = gr.Textbox(interactive=True) + + with gr.Column(): + btn = gr.Button() + + def btn_click(state): + return state + + def text_change(value): + print("text_change", value) + + btn.click(fn=btn_click, inputs=textbox1, outputs=textbox2) + textbox2.change(fn=alert_change, inputs=[gr.State("Text"), textbox2]) + + with gr.Tab(label="Video change, play, pause"): + with gr.Row(): + with gr.Column(): + radio1 = gr.Radio( + choices=[TEST_VIDEO_A, TEST_VIDEO_B], + interactive=True, + type="index", + ) + + video_btn = gr.Button("Change interactive") + + with gr.Column(): + video1 = gr.Video(value=TEST_VIDEO_A, interactive=False) + video1_interactive = gr.State(value=False) + + def change_video(index): + if index == 0: + return TEST_VIDEO_A + elif index == 1: + return TEST_VIDEO_B + + def video_play(): + print("video_play") + + def video_pause(): + print("video_pause") + + def video_stop(): + print("video_stop") + + def video_end(): + print("video_end") + + video1.play(fn=video_play) + video1.pause(fn=video_pause) + video1.stop(fn=video_stop) + video1.end(fn=video_end) + + radio1.change(fn=change_video, inputs=radio1, outputs=video1) + video1.change(fn=alert_change, inputs=[gr.State("Video"), video1]) + + video_btn.click( + fn=change_interactive, + inputs=video1_interactive, + outputs=[video1, video1_interactive], + ) + + with gr.Tab(label="Image change"): + with gr.Row(): + with gr.Column(): + radio2 = gr.Radio( + choices=[TEST_IMAGE_A, TEST_IMAGE_B], + interactive=True, + type="index", + ) + + with gr.Column(): + image1 = gr.Image(value=TEST_IMAGE_A, interactive=True) + + def change_image(index): + if index == 0: + return TEST_IMAGE_A + elif index == 1: + return TEST_IMAGE_B + + radio2.change(fn=change_image, inputs=radio2, outputs=image1) + image1.change(fn=alert_change, inputs=[gr.State("Image"), image1]) + + with gr.Tab(label="File"): + with gr.Row(): + with gr.Column(): + radio3 = gr.Radio( + choices=["A", "B", "AB"], + interactive=True, + type="index", + ) + + file_btn = gr.Button("Change interactive") + + with gr.Column(): + file1 = gr.File( + value=[TEST_IMAGE_A, TEST_IMAGE_B], + interactive=False, + file_count="multiple", + ) + file1_interactive = gr.State(value=False) + + def change_file(index): + if index == 0 or index == 1: + return [TEST_IMAGE_A] + elif index == 2: + return [TEST_IMAGE_A, TEST_IMAGE_B] + + radio3.change(fn=change_file, inputs=radio3, outputs=file1) + file1.change(fn=alert_change, inputs=[gr.State("File"), file1]) + + file_btn.click( + fn=change_interactive, + inputs=file1_interactive, + outputs=[file1, file1_interactive], + ) + +demo.launch() diff --git a/demo/examples_component/images/lion.webp b/demo/examples_component/images/lion.webp new file mode 100644 index 0000000..69529a9 Binary files /dev/null and b/demo/examples_component/images/lion.webp differ diff --git a/demo/examples_component/run.py b/demo/examples_component/run.py new file mode 100644 index 0000000..98df89a --- /dev/null +++ b/demo/examples_component/run.py @@ -0,0 +1,28 @@ +import gradio as gr +# get_image() returns the file path to sample images included with Gradio +from gradio.media import get_image + +def flip(i): + return i.rotate(180) + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + img_i = gr.Image(label="Input Image", type="pil") + with gr.Column(): + img_o = gr.Image(label="Output Image") + with gr.Row(): + btn = gr.Button(value="Flip Image") + btn.click(flip, inputs=[img_i], outputs=[img_o]) + + gr.Examples( + [ + get_image("cheetah1.jpg"), + get_image("lion.jpg"), + ], + img_i, + img_o, + flip, + ) + +demo.launch() diff --git a/demo/fake_diffusion/DESCRIPTION.md b/demo/fake_diffusion/DESCRIPTION.md new file mode 100644 index 0000000..0eab50a --- /dev/null +++ b/demo/fake_diffusion/DESCRIPTION.md @@ -0,0 +1 @@ +This demo uses a fake model to showcase iterative output. The Image output will update every time a generator is returned until the final image. \ No newline at end of file diff --git a/demo/fake_diffusion/requirements.txt b/demo/fake_diffusion/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/fake_diffusion/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/fake_diffusion/run.py b/demo/fake_diffusion/run.py new file mode 100644 index 0000000..4f71352 --- /dev/null +++ b/demo/fake_diffusion/run.py @@ -0,0 +1,21 @@ +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") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/fake_diffusion_with_gif/requirements.txt b/demo/fake_diffusion_with_gif/requirements.txt new file mode 100644 index 0000000..6dd520b --- /dev/null +++ b/demo/fake_diffusion_with_gif/requirements.txt @@ -0,0 +1,3 @@ +numpy +requests +Pillow diff --git a/demo/fake_diffusion_with_gif/run.py b/demo/fake_diffusion_with_gif/run.py new file mode 100644 index 0000000..4397721 --- /dev/null +++ b/demo/fake_diffusion_with_gif/run.py @@ -0,0 +1,46 @@ +import gradio as gr +import numpy as np +import time +import os +from PIL import Image +import requests +from io import BytesIO + +def create_gif(images): + pil_images = [] + for image in images: + if isinstance(image, str): + response = requests.get(image) + image = Image.open(BytesIO(response.content)) + else: + image = Image.fromarray((image * 255).astype(np.uint8)) + pil_images.append(image) + fp_out = os.path.join(os.path.dirname(__file__), "image.gif") + img = pil_images.pop(0) + img.save(fp=fp_out, format='GIF', append_images=pil_images, + save_all=True, duration=400, loop=0) + return fp_out + +def fake_diffusion(steps): + rng = np.random.default_rng() + images = [] + for _ in range(steps): + time.sleep(1) + image = rng.random((600, 600, 3)) + images.append(image) + yield image, gr.Image(visible=False) + + time.sleep(1) + image = "https://gradio-builds.s3.amazonaws.com/diffusion_image/cute_dog.jpg" + images.append(image) + gif_path = create_gif(images) + + yield image, gr.Image(value=gif_path, visible=True) + +demo = gr.Interface(fake_diffusion, + inputs=gr.Slider(1, 10, 3, step=1), + outputs=["image", gr.Image(label="All Images", visible=False)], + api_name="predict",) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/fake_gan/DESCRIPTION.md b/demo/fake_gan/DESCRIPTION.md new file mode 100644 index 0000000..2eb2efc --- /dev/null +++ b/demo/fake_gan/DESCRIPTION.md @@ -0,0 +1 @@ +This is a fake GAN that shows how to create a text-to-image interface for image generation. Check out the Stable Diffusion demo for more: https://hf.co/spaces/stabilityai/stable-diffusion/ \ No newline at end of file diff --git a/demo/fake_gan/run.py b/demo/fake_gan/run.py new file mode 100644 index 0000000..601c0c4 --- /dev/null +++ b/demo/fake_gan/run.py @@ -0,0 +1,33 @@ +# This demo needs to be run from the repo folder. +# python demo/fake_gan/run.py +import random +import time +import gradio as gr + +def fake_gan(): + time.sleep(1) + images = [ + (random.choice( + [ + "http://www.marketingtool.online/en/face-generator/img/faces/avatar-1151ce9f4b2043de0d2e3b7826127998.jpg", + "http://www.marketingtool.online/en/face-generator/img/faces/avatar-116b5e92936b766b7fdfc242649337f7.jpg", + "http://www.marketingtool.online/en/face-generator/img/faces/avatar-1163530ca19b5cebe1b002b8ec67b6fc.jpg", + "http://www.marketingtool.online/en/face-generator/img/faces/avatar-1116395d6e6a6581eef8b8038f4c8e55.jpg", + "http://www.marketingtool.online/en/face-generator/img/faces/avatar-11319be65db395d0e8e6855d18ddcef0.jpg", + ] + ), f"label {i}") + for i in range(3) + ] + return images, "Done" + +with gr.Blocks() as demo: + gallery = gr.Gallery( + label="Generated images", show_label=False, elem_id="gallery" + , columns=1, object_fit="contain", height="auto") + t = gr.Textbox(label="Progress", show_label=False) + btn = gr.Button("Generate images", scale=0) + + btn.click(fake_gan, None, [gallery, t], show_progress="hidden", show_progress_on=t) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/fake_gan_2/run.py b/demo/fake_gan_2/run.py new file mode 100644 index 0000000..66ccb38 --- /dev/null +++ b/demo/fake_gan_2/run.py @@ -0,0 +1,30 @@ +# This demo needs to be run from the repo folder. +# python demo/fake_gan/run.py +import time + +import gradio as gr +# get_image() returns file paths (randomly if None) to sample media included with Gradio +from gradio.media import get_image + +def fake_gan(desc): + if desc == "NSFW": + raise gr.Error("NSFW - banned content.") + if desc == "error": + raise ValueError("error") + time.sleep(9) + image = get_image() + + return image + +demo = gr.Interface( + fn=fake_gan, + inputs=gr.Textbox(), + outputs=gr.Image(label="Generated Image"), + title="FD-GAN", + description="This is a fake demo of a GAN. In reality, the images are randomly chosen from Unsplash.", + api_name="predict" +) +demo.queue(max_size=3) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/fake_gan_no_input/run.py b/demo/fake_gan_no_input/run.py new file mode 100644 index 0000000..7937375 --- /dev/null +++ b/demo/fake_gan_no_input/run.py @@ -0,0 +1,24 @@ +import time + +import gradio as gr + +def fake_gan(): + time.sleep(1) + images = [ + "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80", + "https://images.unsplash.com/photo-1554151228-14d9def656e4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=386&q=80", + "https://images.unsplash.com/photo-1542909168-82c3e7fdca5c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8aHVtYW4lMjBmYWNlfGVufDB8fDB8fA%3D%3D&w=1000&q=80", + ] + return images + +demo = gr.Interface( + fn=fake_gan, + inputs=None, + outputs=gr.Gallery(label="Generated Images", columns=2), + title="FD-GAN", + description="This is a fake demo of a GAN. In reality, the images are randomly chosen from Unsplash.", + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/file_component/run.py b/demo/file_component/run.py new file mode 100644 index 0000000..f0f492b --- /dev/null +++ b/demo/file_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.File() + +demo.launch() diff --git a/demo/file_component_events/run.py b/demo/file_component_events/run.py new file mode 100644 index 0000000..9ee9dd8 --- /dev/null +++ b/demo/file_component_events/run.py @@ -0,0 +1,100 @@ +import gradio as gr + + +def delete_file(n: int, file: gr.DeletedFileData): + return [file.file.path, n + 1] + + +with gr.Blocks() as demo: + + with gr.Row(): + with gr.Column(): + file_component = gr.File(label="Upload Single File", file_count="single") + with gr.Column(): + output_file_1 = gr.File( + label="Upload Single File Output", file_count="single" + ) + num_load_btn_1 = gr.Number(label="# Load Upload Single File", value=0) + file_component.upload( + lambda s, n: (s, n + 1), + [file_component, num_load_btn_1], + [output_file_1, num_load_btn_1], + ) + with gr.Row(): + with gr.Column(): + file_component_multiple = gr.File( + label="Upload Multiple Files", file_count="multiple" + ) + with gr.Column(): + output_file_2 = gr.File( + label="Upload Multiple Files Output", file_count="multiple" + ) + num_load_btn_2 = gr.Number(label="# Load Upload Multiple Files", value=0) + file_component_multiple.upload( + lambda s, n: (s, n + 1), + [file_component_multiple, num_load_btn_2], + [output_file_2, num_load_btn_2], + ) + with gr.Row(): + with gr.Column(): + file_component_specific = gr.File( + label="Upload Multiple Files Image/Video", + file_count="multiple", + file_types=["image", "video"], + ) + with gr.Column(): + output_file_3 = gr.File( + label="Upload Multiple Files Output Image/Video", file_count="multiple" + ) + num_load_btn_3 = gr.Number( + label="# Load Upload Multiple Files Image/Video", value=0 + ) + file_component_specific.upload( + lambda s, n: (s, n + 1), + [file_component_specific, num_load_btn_3], + [output_file_3, num_load_btn_3], + ) + with gr.Row(): + with gr.Column(): + file_component_pdf = gr.File(label="Upload PDF File", file_types=[".pdf"]) + with gr.Column(): + output_file_4 = gr.File(label="Upload PDF File Output") + num_load_btn_4 = gr.Number(label="# Load Upload PDF File", value=0) + file_component_pdf.upload( + lambda s, n: (s, n + 1), + [file_component_pdf, num_load_btn_4], + [output_file_4, num_load_btn_4], + ) + with gr.Row(): + with gr.Column(): + file_component_invalid = gr.File( + label="Upload File with Invalid file_types", + file_types=["invalid file_type"], + ) + with gr.Column(): + output_file_5 = gr.File(label="Upload File with Invalid file_types Output") + num_load_btn_5 = gr.Number( + label="# Load Upload File with Invalid file_types", value=0 + ) + file_component_invalid.upload( + lambda s, n: (s, n + 1), + [file_component_invalid, num_load_btn_5], + [output_file_5, num_load_btn_5], + ) + with gr.Row(): + with gr.Column(): + del_file_input = gr.File(label="Delete File", file_count="multiple") + with gr.Column(): + del_file_data = gr.Textbox(label="Delete file data") + num_load_btn_6 = gr.Number(label="# Deleted File", value=0) + del_file_input.delete( + delete_file, + [num_load_btn_6], + [del_file_data, num_load_btn_6], + ) + # f = gr.File(label="Upload many File", file_count="multiple") + # # f.delete(delete_file) + # f.delete(delete_file, inputs=None, outputs=None) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/file_explorer/run.py b/demo/file_explorer/run.py new file mode 100644 index 0000000..27a5f8a --- /dev/null +++ b/demo/file_explorer/run.py @@ -0,0 +1,46 @@ +import gradio as gr +from pathlib import Path + +current_file_path = Path(__file__).resolve() +relative_path = "path/to/file" +absolute_path = (current_file_path.parent / ".." / ".." / "gradio").resolve() + +def get_file_content(file): + if file is None or Path(file).is_dir(): + return None + return Path(file).read_text() + +with gr.Blocks() as demo: + gr.Markdown('### `FileExplorer` to `FileExplorer` -- `file_count="multiple"`') + submit_btn = gr.Button("Select") + with gr.Row(): + file = gr.FileExplorer( + glob="**/components/*.py", + root_dir=absolute_path, + ignore_glob="**/__init__.py", + ) + + file2 = gr.FileExplorer( + glob="**/components/*.py", + root_dir=absolute_path, + ignore_glob="**/__init__.py", + ) + submit_btn.click(lambda x: x, file, file2) + + gr.Markdown("---") + gr.Markdown('### `FileExplorer` to `Code` -- `file_count="single"`') + with gr.Row(): + file_3 = gr.FileExplorer( + scale=1, + glob="**/components/*.py", + value=["components/file_explorer.py"], + file_count="single", + root_dir=absolute_path, + ) + + code = gr.Code(lines=30, scale=2, language="python") + + file_3.change(get_file_content, file_3, code) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/file_explorer_component/run.py b/demo/file_explorer_component/run.py new file mode 100644 index 0000000..a9e6806 --- /dev/null +++ b/demo/file_explorer_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.FileExplorer() + +demo.launch() diff --git a/demo/file_explorer_component_events/dir1/bar.txt b/demo/file_explorer_component_events/dir1/bar.txt new file mode 100644 index 0000000..e69de29 diff --git a/demo/file_explorer_component_events/dir1/foo.txt b/demo/file_explorer_component_events/dir1/foo.txt new file mode 100644 index 0000000..e69de29 diff --git a/demo/file_explorer_component_events/dir2/baz.png b/demo/file_explorer_component_events/dir2/baz.png new file mode 100644 index 0000000..e69de29 diff --git a/demo/file_explorer_component_events/dir2/foo.png b/demo/file_explorer_component_events/dir2/foo.png new file mode 100644 index 0000000..e69de29 diff --git a/demo/file_explorer_component_events/dir3/dir3_bar.log b/demo/file_explorer_component_events/dir3/dir3_bar.log new file mode 100644 index 0000000..e69de29 diff --git a/demo/file_explorer_component_events/dir3/dir4/dir5/dir5_foo.txt b/demo/file_explorer_component_events/dir3/dir4/dir5/dir5_foo.txt new file mode 100644 index 0000000..e69de29 diff --git a/demo/file_explorer_component_events/dir3/dir4/dir7/dir7_foo.txt b/demo/file_explorer_component_events/dir3/dir4/dir7/dir7_foo.txt new file mode 100644 index 0000000..e69de29 diff --git a/demo/file_explorer_component_events/dir3/dir4/dir_4_bar.log b/demo/file_explorer_component_events/dir3/dir4/dir_4_bar.log new file mode 100644 index 0000000..e69de29 diff --git a/demo/file_explorer_component_events/dir3/dir4/dir_4_foo.txt b/demo/file_explorer_component_events/dir3/dir4/dir_4_foo.txt new file mode 100644 index 0000000..e69de29 diff --git a/demo/file_explorer_component_events/run.py b/demo/file_explorer_component_events/run.py new file mode 100644 index 0000000..69d51fd --- /dev/null +++ b/demo/file_explorer_component_events/run.py @@ -0,0 +1,37 @@ +import gradio as gr +from pathlib import Path + +base_root = Path(__file__).parent.resolve() + +with gr.Blocks() as demo: + with gr.Row(): + dd = gr.Dropdown(label="Select File Explorer Root", + value=str(base_root / "dir1"), + choices=[str(base_root / "dir1"), + str(base_root / "dir2"), + str(base_root / "dir3")]) + with gr.Group(): + txt_only_glob = gr.Checkbox(label="Show only text files", value=False) + ignore_txt_in_glob = gr.Checkbox(label="Ignore text files in glob", value=False) + + fe = gr.FileExplorer(root_dir=str(base_root / "dir1"), + glob="**/*", interactive=True) + textbox = gr.Textbox(label="Selected Directory") + run = gr.Button("Run") + total_changes = gr.Number(0, elem_id="total-changes") + + txt_only_glob.select(lambda s: gr.FileExplorer(glob="*.txt" if s else "*") , + inputs=[txt_only_glob], outputs=[fe]) + ignore_txt_in_glob.select(lambda s: gr.FileExplorer(ignore_glob="*.txt" if s else None), + inputs=[ignore_txt_in_glob], outputs=[fe]) + + dd.select(lambda s: gr.FileExplorer(root_dir=s), inputs=[dd], outputs=[fe]) + run.click(lambda s: ",".join(s) if isinstance(s, list) else s, inputs=[fe], outputs=[textbox]) + fe.change(lambda num: num + 1, inputs=total_changes, outputs=total_changes) + + with gr.Row(): + a = gr.Textbox(elem_id="input-box") + a.change(lambda x: x, inputs=[a]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/filter_records/run.py b/demo/filter_records/run.py new file mode 100644 index 0000000..2cc5224 --- /dev/null +++ b/demo/filter_records/run.py @@ -0,0 +1,24 @@ +import gradio as gr + +def filter_records(records, gender): + return records[records["gender"] == gender] + +demo = gr.Interface( + filter_records, + [ + gr.Dataframe( + headers=["name", "age", "gender"], + datatype=["str", "number", "str"], # type: ignore + row_count=5, + column_count=3, + column_limits=(3, 3), + ), + gr.Dropdown(["M", "F", "O"]), + ], + "dataframe", + api_name="predict", + description="Enter gender as 'M', 'F', or 'O' for other.", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/filter_records/screenshot.png b/demo/filter_records/screenshot.png new file mode 100644 index 0000000..a53732b Binary files /dev/null and b/demo/filter_records/screenshot.png differ diff --git a/demo/fraud_detector/fraud.csv b/demo/fraud_detector/fraud.csv new file mode 100644 index 0000000..8fe13de --- /dev/null +++ b/demo/fraud_detector/fraud.csv @@ -0,0 +1,11 @@ +time,retail,food,other +0,109,145,86 +1,35,87,43 +2,49,117,34 +3,127,66,17 +4,39,82,17 +5,101,56,79 +6,100,129,67 +7,17,88,97 +8,76,85,145 +9,111,106,35 diff --git a/demo/fraud_detector/requirements.txt b/demo/fraud_detector/requirements.txt new file mode 100644 index 0000000..1411a4a --- /dev/null +++ b/demo/fraud_detector/requirements.txt @@ -0,0 +1 @@ +pandas \ No newline at end of file diff --git a/demo/fraud_detector/run.py b/demo/fraud_detector/run.py new file mode 100644 index 0000000..20cdc53 --- /dev/null +++ b/demo/fraud_detector/run.py @@ -0,0 +1,36 @@ +import random +import os +import gradio as gr + +def fraud_detector(card_activity, categories, sensitivity): + activity_range = random.randint(0, 100) + drop_columns = [ + column for column in ["retail", "food", "other"] if column not in categories + ] + if len(drop_columns): + card_activity.drop(columns=drop_columns, inplace=True) + return ( + card_activity, + card_activity, + {"fraud": activity_range / 100.0, "not fraud": 1 - activity_range / 100.0}, + ) + +demo = gr.Interface( + fraud_detector, + [ + gr.CheckboxGroup( + ["retail", "food", "other"], value=["retail", "food", "other"] + ), + gr.Slider(1, 3), + ], + [ + "dataframe", + gr.Label(label="Fraud Level"), + ], + examples=[ + [os.path.join(os.path.dirname(__file__), "fraud.csv"), ["retail", "food", "other"], 1.0], + ], + api_name="predict" +) +if __name__ == "__main__": + demo.launch() diff --git a/demo/fraud_detector/screenshot.png b/demo/fraud_detector/screenshot.png new file mode 100644 index 0000000..2a03d3b Binary files /dev/null and b/demo/fraud_detector/screenshot.png differ diff --git a/demo/function_values/run.py b/demo/function_values/run.py new file mode 100644 index 0000000..8092999 --- /dev/null +++ b/demo/function_values/run.py @@ -0,0 +1,51 @@ +import gradio as gr +import random + +countries = [ + "Algeria", + "Argentina", + "Australia", + "Brazil", + "Canada", + "China", + "Democratic Republic of the Congo", + "Greenland (Denmark)", + "India", + "Kazakhstan", + "Mexico", + "Mongolia", + "Peru", + "Russia", + "Saudi Arabia", + "Sudan", + "United States", +] + +with gr.Blocks() as demo: + with gr.Row(): + count = gr.Slider(1, 10, step=1, label="Country Count") + alpha_order = gr.Checkbox(True, label="Alphabetical Order") + + gr.JSON( + lambda count, alpha_order: countries[:count] + if alpha_order + else countries[-count:], + inputs=[count, alpha_order], + ) + timer = gr.Timer(1) + with gr.Row(): + gr.Textbox( + lambda: random.choice(countries), label="Random Country", every=timer + ) + gr.Textbox( + lambda count: ", ".join(random.sample(countries, count)), + inputs=count, + label="Random Countries", + every=timer, + ) + 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) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/gallery_component/run.py b/demo/gallery_component/run.py new file mode 100644 index 0000000..312065b --- /dev/null +++ b/demo/gallery_component/run.py @@ -0,0 +1,14 @@ +import gradio as gr +from gradio.media import get_image, get_video + +with gr.Blocks() as demo: + gallery_items = [ + ("https://upload.wikimedia.org/wikipedia/commons/0/09/TheCheethcat.jpg", "cheetah1"), + ("https://nationalzoo.si.edu/sites/default/files/animals/cheetah-003.jpg", "cheetah2"), + ("https://videos.pexels.com/video-files/3209828/3209828-uhd_2560_1440_25fps.mp4", "world"), + (get_image("cheetah.jpg"), "cheetah3"), + (get_video("world.mp4"), "world2") + ] + gr.Gallery(value=gallery_items, columns=4) + +demo.launch() diff --git a/demo/gallery_component_events/run.py b/demo/gallery_component_events/run.py new file mode 100644 index 0000000..ec53a25 --- /dev/null +++ b/demo/gallery_component_events/run.py @@ -0,0 +1,35 @@ +import gradio as gr + +with gr.Blocks() as demo: + files = [ + "https://gradio-builds.s3.amazonaws.com/assets/cheetah-003.jpg", + "https://gradio-static-files.s3.amazonaws.com/world.mp4", + "https://gradio-builds.s3.amazonaws.com/assets/TheCheethcat.jpg", + ] + with gr.Row(): + with gr.Column(): + gal = gr.Gallery(columns=4, interactive=True, label="Input Gallery", + sources=["upload", "webcam", "clipboard"]) + btn = gr.Button() + with gr.Column(): + output_gal = gr.Gallery(columns=4, interactive=True, label="Output Gallery") + with gr.Row(): + textbox = gr.Json(label="uploaded files") + num_upload = gr.Number(value=0, label="Num Upload") + num_change = gr.Number(value=0, label="Num Change") + preview_open = gr.Number(value=0, label="Preview Open?") + select_output = gr.Textbox(label="Select Data") + gal.upload(lambda v,n: (v, v, n+1), [gal, num_upload], [textbox, output_gal, num_upload]) + gal.change(lambda v,n: (v, v, n+1), [gal, num_change], [textbox, output_gal, num_change]) + output_gal.preview_open(lambda: 1, inputs=None, outputs=preview_open) + output_gal.preview_close(lambda: 0, inputs=None, outputs=preview_open) + + btn.click(lambda: files, None, [output_gal]) + + def select(select_data: gr.SelectData): + return select_data.value['image']['url'] if 'image' in select_data.value else select_data.value['video']['url'] + + output_gal.select(select, None, select_output) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/gallery_selections/requirements.txt b/demo/gallery_selections/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/gallery_selections/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/gallery_selections/run.py b/demo/gallery_selections/run.py new file mode 100644 index 0000000..0518de0 --- /dev/null +++ b/demo/gallery_selections/run.py @@ -0,0 +1,42 @@ +import gradio as gr +import numpy as np + +with gr.Blocks() as demo: + imgs = gr.State() + gallery = gr.Gallery(allow_preview=False) + + def deselect_images(): + return gr.Gallery(selected_index=None) + + def generate_images(): + images = [] + for _ in range(9): + image = np.ones((100, 100, 3), dtype=np.uint8) * np.random.randint( + 0, 255, 3 + ) # image is a solid single color + images.append(image) + return images, images + + demo.load(generate_images, None, [gallery, imgs]) + + with gr.Row(): + selected = gr.Number(show_label=False) + darken_btn = gr.Button("Darken selected") + deselect_button = gr.Button("Deselect") + + deselect_button.click(deselect_images, None, gallery) + + def get_select_index(evt: gr.SelectData): + return evt.index + + gallery.select(get_select_index, None, selected) + + def darken_img(imgs, index): + index = int(index) + imgs[index] = np.round(imgs[index] * 0.8).astype(np.uint8) + return imgs, imgs + + darken_btn.click(darken_img, [imgs, selected], [imgs, gallery]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/generate_english_german/requirements.txt b/demo/generate_english_german/requirements.txt new file mode 100644 index 0000000..4803a9a --- /dev/null +++ b/demo/generate_english_german/requirements.txt @@ -0,0 +1,2 @@ +transformers +torch \ No newline at end of file diff --git a/demo/generate_english_german/run.py b/demo/generate_english_german/run.py new file mode 100644 index 0000000..7d70d6b --- /dev/null +++ b/demo/generate_english_german/run.py @@ -0,0 +1,25 @@ +import gradio as gr + +from transformers import pipeline + +english_translator = gr.load(name="spaces/gradio/english_translator") +english_generator = pipeline("text-generation", model="distilgpt2") + +def generate_text(text): + english_text = english_generator(text)[0]["generated_text"] # type: ignore + german_text = english_translator(english_text) + return english_text, german_text + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + seed = gr.Text(label="Input Phrase") + with gr.Column(): + english = gr.Text(label="Generated English Text") + german = gr.Text(label="Generated German Text") + btn = gr.Button("Generate") + btn.click(generate_text, inputs=[seed], outputs=[english, german]) + gr.Examples(["My name is Clara and I am"], inputs=[seed]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/generate_tone/requirements.txt b/demo/generate_tone/requirements.txt new file mode 100644 index 0000000..296d654 --- /dev/null +++ b/demo/generate_tone/requirements.txt @@ -0,0 +1 @@ +numpy \ No newline at end of file diff --git a/demo/generate_tone/run.py b/demo/generate_tone/run.py new file mode 100644 index 0000000..5861363 --- /dev/null +++ b/demo/generate_tone/run.py @@ -0,0 +1,26 @@ +import numpy as np +import gradio as gr + +notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] + +def generate_tone(note, octave, duration): + sr = 48000 + a4_freq, tones_from_a4 = 440, 12 * (octave - 4) + (note - 9) + frequency = a4_freq * 2 ** (tones_from_a4 / 12) + duration = int(duration) + audio = np.linspace(0, duration, duration * sr) + audio = (20000 * np.sin(audio * (2 * np.pi * frequency))).astype(np.int16) + return sr, audio + +demo = gr.Interface( + generate_tone, + [ + gr.Dropdown(notes, type="index"), + gr.Slider(4, 6, step=1), + gr.Textbox(value="1", label="Duration in seconds"), + ], + "audio", + api_name="predict" +) +if __name__ == "__main__": + demo.launch() diff --git a/demo/generate_tone/screenshot.png b/demo/generate_tone/screenshot.png new file mode 100644 index 0000000..b89ce99 Binary files /dev/null and b/demo/generate_tone/screenshot.png differ diff --git a/demo/gif_maker/images/1.png b/demo/gif_maker/images/1.png new file mode 100644 index 0000000..5574550 Binary files /dev/null and b/demo/gif_maker/images/1.png differ diff --git a/demo/gif_maker/images/2.png b/demo/gif_maker/images/2.png new file mode 100644 index 0000000..031962c Binary files /dev/null and b/demo/gif_maker/images/2.png differ diff --git a/demo/gif_maker/images/3.png b/demo/gif_maker/images/3.png new file mode 100644 index 0000000..601656b Binary files /dev/null and b/demo/gif_maker/images/3.png differ diff --git a/demo/gif_maker/run.py b/demo/gif_maker/run.py new file mode 100644 index 0000000..36e523d --- /dev/null +++ b/demo/gif_maker/run.py @@ -0,0 +1,42 @@ +import gradio as gr +import os +import tempfile +from PIL import Image + +def gif_maker(img_files): + img_array = [] + for filename, _ in img_files: + pil_img = Image.open(filename) + if pil_img.mode in ('RGBA', 'LA', 'P'): + pil_img = pil_img.convert('RGB') + img_array.append(pil_img) + + with tempfile.NamedTemporaryFile(suffix='.gif', delete=False) as tmp_file: + output_file = tmp_file.name + + if img_array: + img_array[0].save( + output_file, + save_all=True, + append_images=img_array[1:], + duration=200, + loop=0 + ) + return output_file + +demo = gr.Interface( + gif_maker, + inputs=gr.Gallery(), + outputs=gr.Image(), + examples=[ + [[ + os.path.join(os.path.dirname(__file__), "images/1.png"), + os.path.join(os.path.dirname(__file__), "images/2.png"), + os.path.join(os.path.dirname(__file__), "images/3.png"), + ]], + ], + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/gpt2_xl/run.py b/demo/gpt2_xl/run.py new file mode 100644 index 0000000..330c192 --- /dev/null +++ b/demo/gpt2_xl/run.py @@ -0,0 +1,19 @@ +import gradio as gr + +title = "gpt2-xl" + +examples = [ + ["The tower is 324 metres (1,063 ft) tall,"], + ["The Moon's orbit around Earth has"], + ["The smooth Borealis basin in the Northern Hemisphere covers 40%"], +] + +demo = gr.load( + "huggingface/gpt2-xl", + inputs=gr.Textbox(lines=5, max_lines=6, label="Input Text"), + title=title, + examples=examples, +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/gpt2_xl_unified/run.py b/demo/gpt2_xl_unified/run.py new file mode 100644 index 0000000..eb92d80 --- /dev/null +++ b/demo/gpt2_xl_unified/run.py @@ -0,0 +1,14 @@ +import gradio as gr + +component = gr.Textbox(lines=5, label="Text") +api = gr.load("huggingface/gpt2-xl") + +demo = gr.Interface( + fn=lambda x: x[:-50] + api(x[-50:]), + inputs=component, + outputs=component, + title="gpt2-xl", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/gradio_pdf_demo/contract.pdf b/demo/gradio_pdf_demo/contract.pdf new file mode 100644 index 0000000..4d7abec Binary files /dev/null and b/demo/gradio_pdf_demo/contract.pdf differ diff --git a/demo/gradio_pdf_demo/requirements.txt b/demo/gradio_pdf_demo/requirements.txt new file mode 100644 index 0000000..01b311d --- /dev/null +++ b/demo/gradio_pdf_demo/requirements.txt @@ -0,0 +1 @@ +gradio_pdf==0.0.7 diff --git a/demo/gradio_pdf_demo/run.py b/demo/gradio_pdf_demo/run.py new file mode 100644 index 0000000..d366a0a --- /dev/null +++ b/demo/gradio_pdf_demo/run.py @@ -0,0 +1,14 @@ +import gradio as gr +from gradio_pdf import PDF +from pathlib import Path + +current_dir = Path(__file__).parent + +demo = gr.Interface(lambda x: x, + PDF(), + gr.File(), + examples=[[str(current_dir / "contract.pdf")]], + api_name="predict") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/hangman/run.py b/demo/hangman/run.py new file mode 100644 index 0000000..ba5f1b5 --- /dev/null +++ b/demo/hangman/run.py @@ -0,0 +1,35 @@ +import gradio as gr + +secret_word = "gradio" + +with gr.Blocks() as demo: + used_letters_var = gr.State([]) + with gr.Row() as row: + with gr.Column(): + input_letter = gr.Textbox(label="Enter letter") + btn = gr.Button("Guess Letter") + with gr.Column(): + hangman = gr.Textbox( + label="Hangman", + value="_"*len(secret_word) + ) + used_letters_box = gr.Textbox(label="Used Letters") + + def guess_letter(letter, used_letters): + used_letters.append(letter) + answer = "".join([ + (letter if letter in used_letters else "_") + for letter in secret_word + ]) + return { + used_letters_var: used_letters, + used_letters_box: ", ".join(used_letters), + hangman: answer + } + btn.click( + guess_letter, + [input_letter, used_letters_var], + [used_letters_var, used_letters_box, hangman] + ) +if __name__ == "__main__": + demo.launch() diff --git a/demo/hello_blocks/run.py b/demo/hello_blocks/run.py new file mode 100644 index 0000000..f11bca1 --- /dev/null +++ b/demo/hello_blocks/run.py @@ -0,0 +1,15 @@ +import gradio as gr + + +def greet(name): + return "Hello " + name + "!" + + +with gr.Blocks() as demo: + name = gr.Textbox(label="Name") + output = gr.Textbox(label="Output Box") + greet_btn = gr.Button("Greet") + greet_btn.click(fn=greet, inputs=name, outputs=output, api_name="greet") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/hello_blocks_decorator/run.py b/demo/hello_blocks_decorator/run.py new file mode 100644 index 0000000..c9181e9 --- /dev/null +++ b/demo/hello_blocks_decorator/run.py @@ -0,0 +1,13 @@ +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") + + @greet_btn.click(inputs=name, outputs=output) + def greet(name): + return "Hello " + name + "!" + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/hello_login/run.py b/demo/hello_login/run.py new file mode 100644 index 0000000..6005ad7 --- /dev/null +++ b/demo/hello_login/run.py @@ -0,0 +1,18 @@ +import gradio as gr +import argparse +import sys + +parser = argparse.ArgumentParser() +parser.add_argument("--name", type=str, default="User") +args, unknown = parser.parse_known_args() +print(sys.argv) + +with gr.Blocks() as demo: + gr.Markdown(f"# Greetings {args.name}!") + inp = gr.Textbox() + out = gr.Textbox() + + inp.change(fn=lambda x: x, inputs=inp, outputs=out) + +if __name__ == "__main__": + demo.launch(auth=("admin", "admin")) diff --git a/demo/hello_login/screenshot.png b/demo/hello_login/screenshot.png new file mode 100644 index 0000000..a87e6d8 Binary files /dev/null and b/demo/hello_login/screenshot.png differ diff --git a/demo/hello_world/DESCRIPTION.md b/demo/hello_world/DESCRIPTION.md new file mode 100644 index 0000000..e4c1c39 --- /dev/null +++ b/demo/hello_world/DESCRIPTION.md @@ -0,0 +1 @@ +The simplest possible Gradio demo. It wraps a 'Hello {name}!' function in an Interface that accepts and returns text. \ No newline at end of file diff --git a/demo/hello_world/run.py b/demo/hello_world/run.py new file mode 100644 index 0000000..fc3a842 --- /dev/null +++ b/demo/hello_world/run.py @@ -0,0 +1,11 @@ +import gradio as gr + + +def greet(name): + return "Hello " + name + "!" + + +demo = gr.Interface(fn=greet, inputs="textbox", outputs="textbox", api_name="predict") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/hello_world/screenshot.gif b/demo/hello_world/screenshot.gif new file mode 100644 index 0000000..8b923a0 Binary files /dev/null and b/demo/hello_world/screenshot.gif differ diff --git a/demo/hello_world/screenshot.png b/demo/hello_world/screenshot.png new file mode 100644 index 0000000..962ce78 Binary files /dev/null and b/demo/hello_world/screenshot.png differ diff --git a/demo/hello_world_2/run.py b/demo/hello_world_2/run.py new file mode 100644 index 0000000..5a0452b --- /dev/null +++ b/demo/hello_world_2/run.py @@ -0,0 +1,14 @@ +import gradio as gr + +def greet(name, intensity): + return "Hello, " + name + "!" * intensity + +demo = gr.Interface( + fn=greet, + inputs=["text", gr.Slider(value=2, minimum=1, maximum=10, step=1)], + outputs=[gr.Textbox(label="greeting", lines=3)], + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/hello_world_2/screenshot.gif b/demo/hello_world_2/screenshot.gif new file mode 100644 index 0000000..1e6ea1d Binary files /dev/null and b/demo/hello_world_2/screenshot.gif differ diff --git a/demo/hello_world_3/run.py b/demo/hello_world_3/run.py new file mode 100644 index 0000000..b5158ef --- /dev/null +++ b/demo/hello_world_3/run.py @@ -0,0 +1,16 @@ +import gradio as gr + +def greet(name, is_morning, temperature): + salutation = "Good morning" if is_morning else "Good evening" + greeting = f"{salutation} {name}. It is {temperature} degrees today" + celsius = (temperature - 32) * 5 / 9 + return greeting, round(celsius, 2) + +demo = gr.Interface( + fn=greet, + inputs=["text", "checkbox", gr.Slider(0, 100)], + outputs=["text", "number"], + api_name="predict" +) +if __name__ == "__main__": + demo.launch() diff --git a/demo/hello_world_3/screenshot.gif b/demo/hello_world_3/screenshot.gif new file mode 100644 index 0000000..44d8ed0 Binary files /dev/null and b/demo/hello_world_3/screenshot.gif differ diff --git a/demo/hello_world_4/run.py b/demo/hello_world_4/run.py new file mode 100644 index 0000000..4347171 --- /dev/null +++ b/demo/hello_world_4/run.py @@ -0,0 +1,14 @@ +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" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/hello_world_4/screenshot.gif b/demo/hello_world_4/screenshot.gif new file mode 100644 index 0000000..eb7e335 Binary files /dev/null and b/demo/hello_world_4/screenshot.gif differ diff --git a/demo/hidden_change/run.py b/demo/hidden_change/run.py new file mode 100644 index 0000000..f8c1893 --- /dev/null +++ b/demo/hidden_change/run.py @@ -0,0 +1,91 @@ +import gradio as gr +import time +import random + + +def screen_data(data, n): + entry = data.get(f"{n}", {}) + ts = entry.get("timestamp", "unknown time") + msg = entry.get("message", "unknown message") + return f"At {ts}, JS says: “{msg}”" + + +def increment_counter(counter): + return counter + 1 + + +def update_hidden_json(hidden_json, n): + new_n = n + 1 + return { + **hidden_json, + f"{new_n}": {"timestamp": time.time(), "message": f"number {new_n + 1}"}, + }, new_n + + +with gr.Blocks() as demo: + with gr.Tab(label="hidden component"): + n = gr.State(0) + + hidden_json = gr.JSON(visible=False) + + display = gr.Textbox(label="Screened Output") + + demo.load( + fn=None, + js=""" + () => { + const data = { + "0": { + message: "Hello from client JS! Number 1", + timestamp: new Date().toLocaleTimeString() + }, + + }; + return data; // this goes into hidden_json + } + """, + outputs=[hidden_json], + ) + + counter = gr.Number(label="Counter", value=0) + hidden_json.change(fn=increment_counter, inputs=[counter], outputs=[counter]) + hidden_json.change(fn=screen_data, inputs=[hidden_json, n], outputs=[display]) + button = gr.Button("Update hidden_json") + button.click( + fn=update_hidden_json, inputs=[hidden_json, n], outputs=[hidden_json, n] + ) + + with gr.Tab(label="same data"): + btnA = gr.Button("A") + boxA = gr.Textbox() + btnA.click(lambda: "A", outputs=boxA) + + btnB = gr.Button("B") + boxB = gr.Textbox(visible=False) + btnB.click(lambda x: x, boxA, boxB) + + with gr.Row(): + num1 = gr.Textbox(label="Text A") + num2 = gr.Textbox(label="Text B") + + boxA.change(random.random, outputs=num1) + boxB.change(random.random, outputs=num2) + + with gr.Tab(label="hidden parent"): + btnA = gr.Button("A") + boxA = gr.Textbox() + btnA.click(lambda: "A", outputs=boxA) + btnB = gr.Button("B") + with gr.Row(visible=False): + boxB = gr.Textbox() + btnB.click(lambda x: x, boxA, boxB) + + with gr.Row(): + num1 = gr.Textbox(label="Text A") + num2 = gr.Textbox(label="Text B") + + boxA.change(random.random, outputs=num1) + boxB.change(random.random, outputs=num2) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/highlight_pdf/Lorem_ipsum.pdf b/demo/highlight_pdf/Lorem_ipsum.pdf new file mode 100644 index 0000000..186e901 Binary files /dev/null and b/demo/highlight_pdf/Lorem_ipsum.pdf differ diff --git a/demo/highlight_pdf/requirements.txt b/demo/highlight_pdf/requirements.txt new file mode 100644 index 0000000..7683106 --- /dev/null +++ b/demo/highlight_pdf/requirements.txt @@ -0,0 +1,2 @@ +gradio_pdf>=0.0.22 +pymupdf>=1.25.3 \ No newline at end of file diff --git a/demo/highlight_pdf/run.py b/demo/highlight_pdf/run.py new file mode 100644 index 0000000..2628feb --- /dev/null +++ b/demo/highlight_pdf/run.py @@ -0,0 +1,52 @@ +import gradio as gr +from gradio_pdf import PDF +import pymupdf # type: ignore +import os +from pathlib import Path + +current_dir = Path(os.path.abspath('')) + +def highlight_text_in_pdf(pdf_file: Path, highlight_text: str): + page_number = 0 + doc = pymupdf.open(pdf_file) + for page in doc: + text_instances = page.search_for(highlight_text) + if len(text_instances) > 0: + page_number = page.number + for inst in text_instances: + page.add_highlight_annot(inst) + + new_pdf_file = str(pdf_file.parents[0]) + "/new_" + pdf_file.name + doc.save(new_pdf_file) + + if page_number is None: + page_number = 0 + + return new_pdf_file, page_number + 1 + +def ask(query): + result = f"Something about : {query}" + sources = "Document 1" + pdf_path = current_dir / "Lorem_ipsum.pdf" + pdf_name = "Document 1" + context_to_highlight = "Ut velit mauris" + + pdf, page_number = highlight_text_in_pdf(pdf_path, context_to_highlight) + return result, sources + f" - Page {page_number}", PDF(pdf, label=pdf_name, starting_page=page_number, interactive=True) # type: ignore + + +if __name__ == "__main__": + with gr.Blocks() as demo: + title = gr.HTML(f"

Bot

") + with gr.Row(): + with gr.Column(scale=2): + input = gr.Textbox(label="Question", autofocus=True, interactive=True) + btn = gr.Button("Ask", variant="primary") + output = gr.Markdown(label="Anwser") + with gr.Column(scale=2): + srcs = gr.Textbox(label="Sources", interactive=False) + pdf = PDF(label="Document") + + btn.click(fn=ask, inputs=input, outputs=[output, srcs, pdf]) + + demo.launch() diff --git a/demo/highlightedtext_component/run.py b/demo/highlightedtext_component/run.py new file mode 100644 index 0000000..6a0dddd --- /dev/null +++ b/demo/highlightedtext_component/run.py @@ -0,0 +1,10 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.HighlightedText( + value=[("This is highlighted", "highlight"), ("This is not", None), ("This is also highlighted", "highlight")], + color_map={"highlight": "yellow"}, + combine_adjacent=True, + ) + +demo.launch() diff --git a/demo/html_autoscroll/run.py b/demo/html_autoscroll/run.py new file mode 100644 index 0000000..43f1220 --- /dev/null +++ b/demo/html_autoscroll/run.py @@ -0,0 +1,14 @@ +import gradio as gr +import time + +def longer(val): + for i in range(10): + val = val + f"

This is paragraph {i+1}.

" + time.sleep(0.2) + yield val + +with gr.Blocks() as demo: + h = gr.HTML(value="

This is a paragraph 0.

", max_height=200, autoscroll=True) + demo.load(longer, h, h) + +demo.launch() diff --git a/demo/html_children/run.py b/demo/html_children/run.py new file mode 100644 index 0000000..fe3ac0d --- /dev/null +++ b/demo/html_children/run.py @@ -0,0 +1,37 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.HTML(html_template=''' + +

${form_name}

+ @children + + ''', css_template=''' + border: 2px solid gray; + border-radius: 12px; + padding: 20px; + + .maximize { + position: absolute; + top: 10px; + right: 10px; + background: none; + border: none; + z-index: 1000; + } + ''', js_on_load=''' + element.querySelector('.submit').addEventListener('click', () => { + trigger('submit'); + }); + element.querySelector('.maximize').addEventListener('click', () => { + element.requestFullscreen(); + }); + ''', form_name="Custom Form") as form: + name = gr.Textbox(label="Name") + email = gr.Textbox(label="Email") + + output = gr.Textbox(label="Output") + + form.submit(lambda name, email: f"Name: {name}, Email: {email}", inputs=[name, email], outputs=output) + +demo.launch() \ No newline at end of file diff --git a/demo/html_component/run.py b/demo/html_component/run.py new file mode 100644 index 0000000..7d30d0f --- /dev/null +++ b/demo/html_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.HTML(value="

This example was written in HTML

") + +demo.launch() diff --git a/demo/html_custom_event/run.py b/demo/html_custom_event/run.py new file mode 100644 index 0000000..68d9e7c --- /dev/null +++ b/demo/html_custom_event/run.py @@ -0,0 +1,22 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown("# Custom Event Demo\nPress any key to see it displayed below.") + + keyboard = gr.HTML( + js_on_load=""" + document.addEventListener('keydown', (e) => { + trigger('keypress', {key: e.key}); + }); + """, + ) + + textbox = gr.Textbox(label="Key pressed") + + def get_key(evt_data: gr.EventData): + return evt_data.key + + keyboard.keypress(get_key, None, textbox) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/html_head_script_async/run.py b/demo/html_head_script_async/run.py new file mode 100644 index 0000000..72267bb --- /dev/null +++ b/demo/html_head_script_async/run.py @@ -0,0 +1,17 @@ +import gradio as gr + +# Fixture for the "explicit async is honored" test: both scripts are `async`, +# so download-completion order applies (delayed first runs after fast second). + +with gr.Blocks() as demo: + gr.HTML( + html_template="
pending
", + head=( + '' + '' + ), + elem_id="async_demo", + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/html_head_script_order/run.py b/demo/html_head_script_order/run.py new file mode 100644 index 0000000..3132f1f --- /dev/null +++ b/demo/html_head_script_order/run.py @@ -0,0 +1,17 @@ +import gradio as gr + +# Fixture for the head-script order test: "plugin" depends on a global set by +# "core", so it must run after core (document order). + +with gr.Blocks() as demo: + gr.HTML( + html_template="
pending
", + head=( + '' + '' + ), + elem_id="order_demo", + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/html_server_functions/run.py b/demo/html_server_functions/run.py new file mode 100644 index 0000000..be9e49c --- /dev/null +++ b/demo/html_server_functions/run.py @@ -0,0 +1,44 @@ +import os + +import gradio as gr + + +def list_files(path): + try: + return os.listdir(path) + except (FileNotFoundError, PermissionError) as e: + return [f"Error: {e}"] + + +with gr.Blocks() as demo: + gr.Markdown( + "# Server Functions Demo\nClick 'Load Files' to list files in the directory." + ) + filetree = gr.HTML( + value=os.path.dirname(__file__), + html_template=""" +
+

Directory: ${value}

+
+ +
+ """, + js_on_load=""" + const loadBtn = element.querySelector('.load-btn'); + const tree = element.querySelector('.tree'); + loadBtn.addEventListener('click', async () => { + const files = await server.list_files(props.value); + tree.innerHTML = ''; + files.forEach(file => { + const fileEl = document.createElement('div'); + fileEl.textContent = file; + tree.appendChild(fileEl); + }); + }); + """, + server_functions=[list_files], + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/html_upload/run.py b/demo/html_upload/run.py new file mode 100644 index 0000000..30b1094 --- /dev/null +++ b/demo/html_upload/run.py @@ -0,0 +1,32 @@ +import gradio as gr +from pathlib import Path + +with gr.Blocks() as demo: + + file_uploader = gr.HTML( + html_template=""" +
+ + +
+ """, + js_on_load=""" + const input = element.querySelector('#file-input'); + const btn = element.querySelector('#upload-btn'); + + btn.addEventListener('click', async () => { + const file = input.files[0]; + const { path } = await upload(file); + props.value = path; + }); + """, + elem_id="file_uploader" + ) + + view_content_btn = gr.Button("View Uploaded File Content") + upload_content = gr.Textbox(label="Uploaded File Content") + + view_content_btn.click(lambda path: Path(path).read_text(), file_uploader, upload_content) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/i18n/run.py b/demo/i18n/run.py new file mode 100644 index 0000000..62610fa --- /dev/null +++ b/demo/i18n/run.py @@ -0,0 +1,51 @@ +import gradio as gr + +# create an i18n instance with translations for different languages +i18n = gr.I18n( + en={"greeting": "Hello, welcome to my app!", "name_label": "Your Name", "submit_button": "Greet", "john_doe": "John English", "result_label": "Result", "format_label": "Format", "choice_bold": "Bold", "choice_italic": "Italic"}, + es={"greeting": "¡Hola, bienvenido a mi aplicación!", "name_label": "Tu Nombre", "submit_button": "Saludar", "john_doe": "John Spanish", "result_label": "Resultado", "format_label": "Formato", "choice_bold": "Negrita", "choice_italic": "Cursiva"}, + fr={"greeting": "Bonjour, bienvenue dans mon application!", "name_label": "Votre Nom", "submit_button": "Saluer", "john_doe": "John French", "result_label": "Résultat", "format_label": "Format", "choice_bold": "Gras", "choice_italic": "Italique"}, + de={"greeting": "Hallo, willkommen in meiner App!", "name_label": "Dein Name", "submit_button": "Grüßen", "john_doe": "John German", "result_label": "Ergebnis", "format_label": "Format", "choice_bold": "Fett", "choice_italic": "Kursiv"}, +) + +def add_hello_world(name): + return "hello " + name + +with gr.Blocks() as demo: + gr.Markdown(value=i18n("greeting")) + + with gr.Row(): + # use i18n() for any string that should be translated + name_input = gr.Textbox(label=i18n("name_label"), value=i18n("john_doe")) + + with gr.Row(): + output_text = gr.Textbox(label=i18n("result_label")) + + with gr.Row(): + greet_btn = gr.Button(value=i18n("submit_button")) + + with gr.Row(): + reset_btn = gr.Button("Reset Name") + + with gr.Row(): + # choices use (display, value) tuples: only the display side is translated + format_radio = gr.Radio( + choices=[(i18n("choice_bold"), "bold"), (i18n("choice_italic"), "italic")], + value="bold", + label=i18n("format_label"), + ) + selected_format = gr.Textbox(label="Selected Format") + + greet_btn.click(fn=add_hello_world, inputs=name_input, outputs=output_text) + format_radio.change(fn=lambda f: f, inputs=format_radio, outputs=selected_format) + reset_btn.click(fn=lambda: i18n("john_doe"), inputs=None, outputs=name_input) + + gr.Markdown(""" + This demo shows Gradio's internationalization (i18n) functionality. + The interface automatically displays text in the user's browser language + (if available in our translations), or falls back to English. + """) + +if __name__ == "__main__": + # pass i18n to the launch function + demo.launch(i18n=i18n) diff --git a/demo/iframe_resizer/run.py b/demo/iframe_resizer/run.py new file mode 100644 index 0000000..42bf150 --- /dev/null +++ b/demo/iframe_resizer/run.py @@ -0,0 +1,49 @@ +import gradio as gr +import time +import os +from gradio import get_image + + +def greet(): + gr.Info("Warning in 1 second") + time.sleep(1) + gr.Warning("Error in 1 second") + time.sleep(1) + raise Exception("test") + + +im = get_image("cheetah.jpg") + +with gr.Blocks() as demo: + with gr.Tab("Accordions"): + with gr.Row(height=1500): + gr.Markdown("Scroll down to see UI.") + greet_btn = gr.Button("Trigger toast") + greet_btn.click(fn=greet) + + with gr.Accordion("Accordion"): + gr.Markdown( + """ + ## Accordion content + ### Accordion content + #### Accordion content + ##### Accordion content + ###### Accordion content + """ + ) + with gr.Tab("Images"): + gr.Image(value=im) + gr.Image(value=im) + gr.Image(value=im) + gr.Image(value=im) + gr.Image(value=im) + gr.Image(value=im) + gr.Image(value=im) + gr.Image(value=im) + gr.Image(value=im) + gr.Image(value=im) + gr.Image(value=im) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image-simple/run.py b/demo/image-simple/run.py new file mode 100644 index 0000000..e89d1a7 --- /dev/null +++ b/demo/image-simple/run.py @@ -0,0 +1,13 @@ +import gradio as gr + +def image(im): + return im + +with gr.Blocks() as demo: + im = gr.Image() + im2 = gr.Image() + btn = gr.Button() + btn.click(lambda x: x, outputs=im2, inputs=im) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_classification/DESCRIPTION.md b/demo/image_classification/DESCRIPTION.md new file mode 100644 index 0000000..7b0ce50 --- /dev/null +++ b/demo/image_classification/DESCRIPTION.md @@ -0,0 +1 @@ +Simple image classification in Pytorch with Gradio's Image input and Label output. \ No newline at end of file diff --git a/demo/image_classification/requirements.txt b/demo/image_classification/requirements.txt new file mode 100644 index 0000000..5ec2045 --- /dev/null +++ b/demo/image_classification/requirements.txt @@ -0,0 +1,3 @@ +torch +torchvision +requests diff --git a/demo/image_classification/run.py b/demo/image_classification/run.py new file mode 100644 index 0000000..9da25d3 --- /dev/null +++ b/demo/image_classification/run.py @@ -0,0 +1,24 @@ +import gradio as gr +import torch +import requests +from torchvision import transforms # type: ignore + +model = torch.hub.load('pytorch/vision:v0.6.0', 'resnet18', pretrained=True).eval() +response = requests.get("https://git.io/JJkYN") +labels = response.text.split("\n") + +def predict(inp): + inp = transforms.ToTensor()(inp).unsqueeze(0) + with torch.no_grad(): + prediction = torch.nn.functional.softmax(model(inp)[0], dim=0) + confidences = {labels[i]: float(prediction[i]) for i in range(1000)} + return confidences + +demo = gr.Interface(fn=predict, + inputs=gr.Image(type="pil"), + outputs=gr.Label(num_top_classes=3), + examples=[["cheetah.jpg"]], + api_name="predict" + ) + +demo.launch() diff --git a/demo/image_classifier/files/imagenet_labels.json b/demo/image_classifier/files/imagenet_labels.json new file mode 100644 index 0000000..fa059ce --- /dev/null +++ b/demo/image_classifier/files/imagenet_labels.json @@ -0,0 +1,1000 @@ +["tench", + "goldfish", + "great white shark", + "tiger shark", + "hammerhead shark", + "electric ray", + "stingray", + "cock", + "hen", + "ostrich", + "brambling", + "goldfinch", + "house finch", + "junco", + "indigo bunting", + "American robin", + "bulbul", + "jay", + "magpie", + "chickadee", + "American dipper", + "kite", + "bald eagle", + "vulture", + "great grey owl", + "fire salamander", + "smooth newt", + "newt", + "spotted salamander", + "axolotl", + "American bullfrog", + "tree frog", + "tailed frog", + "loggerhead sea turtle", + "leatherback sea turtle", + "mud turtle", + "terrapin", + "box turtle", + "banded gecko", + "green iguana", + "Carolina anole", + "desert grassland whiptail lizard", + "agama", + "frilled-necked lizard", + "alligator lizard", + "Gila monster", + "European green lizard", + "chameleon", + "Komodo dragon", + "Nile crocodile", + "American alligator", + "triceratops", + "worm snake", + "ring-necked snake", + "eastern hog-nosed snake", + "smooth green snake", + "kingsnake", + "garter snake", + "water snake", + "vine snake", + "night snake", + "boa constrictor", + "African rock python", + "Indian cobra", + "green mamba", + "sea snake", + "Saharan horned viper", + "eastern diamondback rattlesnake", + "sidewinder", + "trilobite", + "harvestman", + "scorpion", + "yellow garden spider", + "barn spider", + "European garden spider", + "southern black widow", + "tarantula", + "wolf spider", + "tick", + "centipede", + "black grouse", + "ptarmigan", + "ruffed grouse", + "prairie grouse", + "peacock", + "quail", + "partridge", + "grey parrot", + "macaw", + "sulphur-crested cockatoo", + "lorikeet", + "coucal", + "bee eater", + "hornbill", + "hummingbird", + "jacamar", + "toucan", + "duck", + "red-breasted merganser", + "goose", + "black swan", + "tusker", + "echidna", + "platypus", + "wallaby", + "koala", + "wombat", + "jellyfish", + "sea anemone", + "brain coral", + "flatworm", + "nematode", + "conch", + "snail", + "slug", + "sea slug", + "chiton", + "chambered nautilus", + "Dungeness crab", + "rock crab", + "fiddler crab", + "red king crab", + "American lobster", + "spiny lobster", + "crayfish", + "hermit crab", + "isopod", + "white stork", + "black stork", + "spoonbill", + "flamingo", + "little blue heron", + "great egret", + "bittern", + "crane (bird)", + "limpkin", + "common gallinule", + "American coot", + "bustard", + "ruddy turnstone", + "dunlin", + "common redshank", + "dowitcher", + "oystercatcher", + "pelican", + "king penguin", + "albatross", + "grey whale", + "killer whale", + "dugong", + "sea lion", + "Chihuahua", + "Japanese Chin", + "Maltese", + "Pekingese", + "Shih Tzu", + "King Charles Spaniel", + "Papillon", + "toy terrier", + "Rhodesian Ridgeback", + "Afghan Hound", + "Basset Hound", + "Beagle", + "Bloodhound", + "Bluetick Coonhound", + "Black and Tan Coonhound", + "Treeing Walker Coonhound", + "English foxhound", + "Redbone Coonhound", + "borzoi", + "Irish Wolfhound", + "Italian Greyhound", + "Whippet", + "Ibizan Hound", + "Norwegian Elkhound", + "Otterhound", + "Saluki", + "Scottish Deerhound", + "Weimaraner", + "Staffordshire Bull Terrier", + "American Staffordshire Terrier", + "Bedlington Terrier", + "Border Terrier", + "Kerry Blue Terrier", + "Irish Terrier", + "Norfolk Terrier", + "Norwich Terrier", + "Yorkshire Terrier", + "Wire Fox Terrier", + "Lakeland Terrier", + "Sealyham Terrier", + "Airedale Terrier", + "Cairn Terrier", + "Australian Terrier", + "Dandie Dinmont Terrier", + "Boston Terrier", + "Miniature Schnauzer", + "Giant Schnauzer", + "Standard Schnauzer", + "Scottish Terrier", + "Tibetan Terrier", + "Australian Silky Terrier", + "Soft-coated Wheaten Terrier", + "West Highland White Terrier", + "Lhasa Apso", + "Flat-Coated Retriever", + "Curly-coated Retriever", + "Golden Retriever", + "Labrador Retriever", + "Chesapeake Bay Retriever", + "German Shorthaired Pointer", + "Vizsla", + "English Setter", + "Irish Setter", + "Gordon Setter", + "Brittany", + "Clumber Spaniel", + "English Springer Spaniel", + "Welsh Springer Spaniel", + "Cocker Spaniels", + "Sussex Spaniel", + "Irish Water Spaniel", + "Kuvasz", + "Schipperke", + "Groenendael", + "Malinois", + "Briard", + "Australian Kelpie", + "Komondor", + "Old English Sheepdog", + "Shetland Sheepdog", + "collie", + "Border Collie", + "Bouvier des Flandres", + "Rottweiler", + "German Shepherd Dog", + "Dobermann", + "Miniature Pinscher", + "Greater Swiss Mountain Dog", + "Bernese Mountain Dog", + "Appenzeller Sennenhund", + "Entlebucher Sennenhund", + "Boxer", + "Bullmastiff", + "Tibetan Mastiff", + "French Bulldog", + "Great Dane", + "St. Bernard", + "husky", + "Alaskan Malamute", + "Siberian Husky", + "Dalmatian", + "Affenpinscher", + "Basenji", + "pug", + "Leonberger", + "Newfoundland", + "Pyrenean Mountain Dog", + "Samoyed", + "Pomeranian", + "Chow Chow", + "Keeshond", + "Griffon Bruxellois", + "Pembroke Welsh Corgi", + "Cardigan Welsh Corgi", + "Toy Poodle", + "Miniature Poodle", + "Standard Poodle", + "Mexican hairless dog", + "grey wolf", + "Alaskan tundra wolf", + "red wolf", + "coyote", + "dingo", + "dhole", + "African wild dog", + "hyena", + "red fox", + "kit fox", + "Arctic fox", + "grey fox", + "tabby cat", + "tiger cat", + "Persian cat", + "Siamese cat", + "Egyptian Mau", + "cougar", + "lynx", + "leopard", + "snow leopard", + "jaguar", + "lion", + "tiger", + "cheetah", + "brown bear", + "American black bear", + "polar bear", + "sloth bear", + "mongoose", + "meerkat", + "tiger beetle", + "ladybug", + "ground beetle", + "longhorn beetle", + "leaf beetle", + "dung beetle", + "rhinoceros beetle", + "weevil", + "fly", + "bee", + "ant", + "grasshopper", + "cricket", + "stick insect", + "cockroach", + "mantis", + "cicada", + "leafhopper", + "lacewing", + "dragonfly", + "damselfly", + "red admiral", + "ringlet", + "monarch butterfly", + "small white", + "sulphur butterfly", + "gossamer-winged butterfly", + "starfish", + "sea urchin", + "sea cucumber", + "cottontail rabbit", + "hare", + "Angora rabbit", + "hamster", + "porcupine", + "fox squirrel", + "marmot", + "beaver", + "guinea pig", + "common sorrel", + "zebra", + "pig", + "wild boar", + "warthog", + "hippopotamus", + "ox", + "water buffalo", + "bison", + "ram", + "bighorn sheep", + "Alpine ibex", + "hartebeest", + "impala", + "gazelle", + "dromedary", + "llama", + "weasel", + "mink", + "European polecat", + "black-footed ferret", + "otter", + "skunk", + "badger", + "armadillo", + "three-toed sloth", + "orangutan", + "gorilla", + "chimpanzee", + "gibbon", + "siamang", + "guenon", + "patas monkey", + "baboon", + "macaque", + "langur", + "black-and-white colobus", + "proboscis monkey", + "marmoset", + "white-headed capuchin", + "howler monkey", + "titi", + "Geoffroy's spider monkey", + "common squirrel monkey", + "ring-tailed lemur", + "indri", + "Asian elephant", + "African bush elephant", + "red panda", + "giant panda", + "snoek", + "eel", + "coho salmon", + "rock beauty", + "clownfish", + "sturgeon", + "garfish", + "lionfish", + "pufferfish", + "abacus", + "abaya", + "academic gown", + "accordion", + "acoustic guitar", + "aircraft carrier", + "airliner", + "airship", + "altar", + "ambulance", + "amphibious vehicle", + "analog clock", + "apiary", + "apron", + "waste container", + "assault rifle", + "backpack", + "bakery", + "balance beam", + "balloon", + "ballpoint pen", + "Band-Aid", + "banjo", + "baluster", + "barbell", + "barber chair", + "barbershop", + "barn", + "barometer", + "barrel", + "wheelbarrow", + "baseball", + "basketball", + "bassinet", + "bassoon", + "swimming cap", + "bath towel", + "bathtub", + "station wagon", + "lighthouse", + "beaker", + "military cap", + "beer bottle", + "beer glass", + "bell-cot", + "bib", + "tandem bicycle", + "bikini", + "ring binder", + "binoculars", + "birdhouse", + "boathouse", + "bobsleigh", + "bolo tie", + "poke bonnet", + "bookcase", + "bookstore", + "bottle cap", + "bow", + "bow tie", + "brass", + "bra", + "breakwater", + "breastplate", + "broom", + "bucket", + "buckle", + "bulletproof vest", + "high-speed train", + "butcher shop", + "taxicab", + "cauldron", + "candle", + "cannon", + "canoe", + "can opener", + "cardigan", + "car mirror", + "carousel", + "tool kit", + "carton", + "car wheel", + "automated teller machine", + "cassette", + "cassette player", + "castle", + "catamaran", + "CD player", + "cello", + "mobile phone", + "chain", + "chain-link fence", + "chain mail", + "chainsaw", + "chest", + "chiffonier", + "chime", + "china cabinet", + "Christmas stocking", + "church", + "movie theater", + "cleaver", + "cliff dwelling", + "cloak", + "clogs", + "cocktail shaker", + "coffee mug", + "coffeemaker", + "coil", + "combination lock", + "computer keyboard", + "confectionery store", + "container ship", + "convertible", + "corkscrew", + "cornet", + "cowboy boot", + "cowboy hat", + "cradle", + "crane (machine)", + "crash helmet", + "crate", + "infant bed", + "Crock Pot", + "croquet ball", + "crutch", + "cuirass", + "dam", + "desk", + "desktop computer", + "rotary dial telephone", + "diaper", + "digital clock", + "digital watch", + "dining table", + "dishcloth", + "dishwasher", + "disc brake", + "dock", + "dog sled", + "dome", + "doormat", + "drilling rig", + "drum", + "drumstick", + "dumbbell", + "Dutch oven", + "electric fan", + "electric guitar", + "electric locomotive", + "entertainment center", + "envelope", + "espresso machine", + "face powder", + "feather boa", + "filing cabinet", + "fireboat", + "fire engine", + "fire screen sheet", + "flagpole", + "flute", + "folding chair", + "football helmet", + "forklift", + "fountain", + "fountain pen", + "four-poster bed", + "freight car", + "French horn", + "frying pan", + "fur coat", + "garbage truck", + "gas mask", + "gas pump", + "goblet", + "go-kart", + "golf ball", + "golf cart", + "gondola", + "gong", + "gown", + "grand piano", + "greenhouse", + "grille", + "grocery store", + "guillotine", + "barrette", + "hair spray", + "half-track", + "hammer", + "hamper", + "hair dryer", + "hand-held computer", + "handkerchief", + "hard disk drive", + "harmonica", + "harp", + "harvester", + "hatchet", + "holster", + "home theater", + "honeycomb", + "hook", + "hoop skirt", + "horizontal bar", + "horse-drawn vehicle", + "hourglass", + "iPod", + "clothes iron", + "jack-o'-lantern", + "jeans", + "jeep", + "T-shirt", + "jigsaw puzzle", + "pulled rickshaw", + "joystick", + "kimono", + "knee pad", + "knot", + "lab coat", + "ladle", + "lampshade", + "laptop computer", + "lawn mower", + "lens cap", + "paper knife", + "library", + "lifeboat", + "lighter", + "limousine", + "ocean liner", + "lipstick", + "slip-on shoe", + "lotion", + "speaker", + "loupe", + "sawmill", + "magnetic compass", + "mail bag", + "mailbox", + "tights", + "tank suit", + "manhole cover", + "maraca", + "marimba", + "mask", + "match", + "maypole", + "maze", + "measuring cup", + "medicine chest", + "megalith", + "microphone", + "microwave oven", + "military uniform", + "milk can", + "minibus", + "miniskirt", + "minivan", + "missile", + "mitten", + "mixing bowl", + "mobile home", + "Model T", + "modem", + "monastery", + "monitor", + "moped", + "mortar", + "square academic cap", + "mosque", + "mosquito net", + "scooter", + "mountain bike", + "tent", + "computer mouse", + "mousetrap", + "moving van", + "muzzle", + "nail", + "neck brace", + "necklace", + "nipple", + "notebook computer", + "obelisk", + "oboe", + "ocarina", + "odometer", + "oil filter", + "organ", + "oscilloscope", + "overskirt", + "bullock cart", + "oxygen mask", + "packet", + "paddle", + "paddle wheel", + "padlock", + "paintbrush", + "pajamas", + "palace", + "pan flute", + "paper towel", + "parachute", + "parallel bars", + "park bench", + "parking meter", + "passenger car", + "patio", + "payphone", + "pedestal", + "pencil case", + "pencil sharpener", + "perfume", + "Petri dish", + "photocopier", + "plectrum", + "Pickelhaube", + "picket fence", + "pickup truck", + "pier", + "piggy bank", + "pill bottle", + "pillow", + "ping-pong ball", + "pinwheel", + "pirate ship", + "pitcher", + "hand plane", + "planetarium", + "plastic bag", + "plate rack", + "plow", + "plunger", + "Polaroid camera", + "pole", + "police van", + "poncho", + "billiard table", + "soda bottle", + "pot", + "potter's wheel", + "power drill", + "prayer rug", + "printer", + "prison", + "projectile", + "projector", + "hockey puck", + "punching bag", + "purse", + "quill", + "quilt", + "race car", + "racket", + "radiator", + "radio", + "radio telescope", + "rain barrel", + "recreational vehicle", + "reel", + "reflex camera", + "refrigerator", + "remote control", + "restaurant", + "revolver", + "rifle", + "rocking chair", + "rotisserie", + "eraser", + "rugby ball", + "ruler", + "running shoe", + "safe", + "safety pin", + "salt shaker", + "sandal", + "sarong", + "saxophone", + "scabbard", + "weighing scale", + "school bus", + "schooner", + "scoreboard", + "CRT screen", + "screw", + "screwdriver", + "seat belt", + "sewing machine", + "shield", + "shoe store", + "shoji", + "shopping basket", + "shopping cart", + "shovel", + "shower cap", + "shower curtain", + "ski", + "ski mask", + "sleeping bag", + "slide rule", + "sliding door", + "slot machine", + "snorkel", + "snowmobile", + "snowplow", + "soap dispenser", + "soccer ball", + "sock", + "solar thermal collector", + "sombrero", + "soup bowl", + "space bar", + "space heater", + "space shuttle", + "spatula", + "motorboat", + "spider web", + "spindle", + "sports car", + "spotlight", + "stage", + "steam locomotive", + "through arch bridge", + "steel drum", + "stethoscope", + "scarf", + "stone wall", + "stopwatch", + "stove", + "strainer", + "tram", + "stretcher", + "couch", + "stupa", + "submarine", + "suit", + "sundial", + "sunglass", + "sunglasses", + "sunscreen", + "suspension bridge", + "mop", + "sweatshirt", + "swimsuit", + "swing", + "switch", + "syringe", + "table lamp", + "tank", + "tape player", + "teapot", + "teddy bear", + "television", + "tennis ball", + "thatched roof", + "front curtain", + "thimble", + "threshing machine", + "throne", + "tile roof", + "toaster", + "tobacco shop", + "toilet seat", + "torch", + "totem pole", + "tow truck", + "toy store", + "tractor", + "semi-trailer truck", + "tray", + "trench coat", + "tricycle", + "trimaran", + "tripod", + "triumphal arch", + "trolleybus", + "trombone", + "tub", + "turnstile", + "typewriter keyboard", + "umbrella", + "unicycle", + "upright piano", + "vacuum cleaner", + "vase", + "vault", + "velvet", + "vending machine", + "vestment", + "viaduct", + "violin", + "volleyball", + "waffle iron", + "wall clock", + "wallet", + "wardrobe", + "military aircraft", + "sink", + "washing machine", + "water bottle", + "water jug", + "water tower", + "whiskey jug", + "whistle", + "wig", + "window screen", + "window shade", + "Windsor tie", + "wine bottle", + "wing", + "wok", + "wooden spoon", + "wool", + "split-rail fence", + "shipwreck", + "yawl", + "yurt", + "website", + "comic book", + "crossword", + "traffic sign", + "traffic light", + "dust jacket", + "menu", + "plate", + "guacamole", + "consomme", + "hot pot", + "trifle", + "ice cream", + "ice pop", + "baguette", + "bagel", + "pretzel", + "cheeseburger", + "hot dog", + "mashed potato", + "cabbage", + "broccoli", + "cauliflower", + "zucchini", + "spaghetti squash", + "acorn squash", + "butternut squash", + "cucumber", + "artichoke", + "bell pepper", + "cardoon", + "mushroom", + "Granny Smith", + "strawberry", + "orange", + "lemon", + "fig", + "pineapple", + "banana", + "jackfruit", + "custard apple", + "pomegranate", + "hay", + "carbonara", + "chocolate syrup", + "dough", + "meatloaf", + "pizza", + "pot pie", + "burrito", + "red wine", + "espresso", + "cup", + "eggnog", + "alp", + "bubble", + "cliff", + "coral reef", + "geyser", + "lakeshore", + "promontory", + "shoal", + "seashore", + "valley", + "volcano", + "baseball player", + "bridegroom", + "scuba diver", + "rapeseed", + "daisy", + "yellow lady's slipper", + "corn", + "acorn", + "rose hip", + "horse chestnut seed", + "coral fungus", + "agaric", + "gyromitra", + "stinkhorn mushroom", + "earth star", + "hen-of-the-woods", + "bolete", + "ear", + "toilet paper"] \ No newline at end of file diff --git a/demo/image_classifier/requirements.txt b/demo/image_classifier/requirements.txt new file mode 100644 index 0000000..7947059 --- /dev/null +++ b/demo/image_classifier/requirements.txt @@ -0,0 +1,3 @@ +numpy +tensorflow +requests diff --git a/demo/image_classifier/run.py b/demo/image_classifier/run.py new file mode 100644 index 0000000..3403f6d --- /dev/null +++ b/demo/image_classifier/run.py @@ -0,0 +1,36 @@ +import requests +import tensorflow as tf # type: ignore + +import gradio as gr +# get_image() returns the file path to sample images included with Gradio +from gradio.media import get_image + +inception_net = tf.keras.applications.MobileNetV2() # load the model + +# Download human-readable labels for ImageNet. +response = requests.get("https://git.io/JJkYN") +labels = response.text.split("\n") + +def classify_image(inp): + inp = inp.reshape((-1, 224, 224, 3)) + inp = tf.keras.applications.mobilenet_v2.preprocess_input(inp) + prediction = inception_net.predict(inp).flatten() + return {labels[i]: float(prediction[i]) for i in range(1000)} + +image = gr.Image() +label = gr.Label(num_top_classes=3) + +demo = gr.Interface( + fn=classify_image, + inputs=image, + outputs=label, + examples=[ + get_image("cheetah1.jpg"), + get_image("lion.jpg") + ], + api_name="predict" + ) + +if __name__ == "__main__": + demo.launch() + diff --git a/demo/image_classifier/screenshot.gif b/demo/image_classifier/screenshot.gif new file mode 100644 index 0000000..8f4a3a2 Binary files /dev/null and b/demo/image_classifier/screenshot.gif differ diff --git a/demo/image_classifier/screenshot.png b/demo/image_classifier/screenshot.png new file mode 100644 index 0000000..306825e Binary files /dev/null and b/demo/image_classifier/screenshot.png differ diff --git a/demo/image_classifier_2/requirements.txt b/demo/image_classifier_2/requirements.txt new file mode 100644 index 0000000..0396385 --- /dev/null +++ b/demo/image_classifier_2/requirements.txt @@ -0,0 +1,4 @@ +pillow +torch +torchvision +requests diff --git a/demo/image_classifier_2/run.py b/demo/image_classifier_2/run.py new file mode 100644 index 0000000..9e0b893 --- /dev/null +++ b/demo/image_classifier_2/run.py @@ -0,0 +1,27 @@ +import requests +import torch +from PIL import Image +from torchvision import transforms # type: ignore + +import gradio as gr + +model = torch.hub.load("pytorch/vision:v0.6.0", "resnet18", pretrained=True).eval() + +# Download human-readable labels for ImageNet. +response = requests.get("https://git.io/JJkYN") +labels = response.text.split("\n") + +def predict(inp): + inp = Image.fromarray(inp.astype("uint8"), "RGB") + inp = transforms.ToTensor()(inp).unsqueeze(0) + with torch.no_grad(): + prediction = torch.nn.functional.softmax(model(inp)[0], dim=0) + return {labels[i]: float(prediction[i]) for i in range(1000)} + +inputs = gr.Image() +outputs = gr.Label(num_top_classes=3) + +demo = gr.Interface(fn=predict, inputs=inputs, outputs=outputs) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_classifier_interface_load/run.py b/demo/image_classifier_interface_load/run.py new file mode 100644 index 0000000..e026e05 --- /dev/null +++ b/demo/image_classifier_interface_load/run.py @@ -0,0 +1,30 @@ +import gradio as gr +import pathlib + +current_dir = pathlib.Path(__file__).parent + +images = [str(current_dir / "cheetah1.jpeg"), str(current_dir / "cheetah1.jpg"), str(current_dir / "lion.jpg")] + +img_classifier = gr.load( + "models/google/vit-base-patch16-224", examples=images, cache_examples=False +) + +def func(img, text): + return img_classifier(img), text + +using_img_classifier_as_function = gr.Interface( + func, + [gr.Image(type="filepath"), "text"], + ["label", "text"], + examples=[ + [str(current_dir / "cheetah1.jpeg"), None], + [str(current_dir / "cheetah1.jpg"), "cheetah"], + [str(current_dir / "lion.jpg"), "lion"], + ], + cache_examples=False, + api_name="predict" +) +demo = gr.TabbedInterface([using_img_classifier_as_function, img_classifier]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_component/run.py b/demo/image_component/run.py new file mode 100644 index 0000000..cf779aa --- /dev/null +++ b/demo/image_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Image() + +demo.launch() diff --git a/demo/image_component_events/run.py b/demo/image_component_events/run.py new file mode 100644 index 0000000..80fb02a --- /dev/null +++ b/demo/image_component_events/run.py @@ -0,0 +1,30 @@ +import gradio as gr + +def test_select_is_defined(n, evt: gr.SelectData): + assert isinstance(evt.index, list) + assert isinstance(evt.index[0], int) + return n + 1 + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + input_img = gr.Image(type="filepath", label="Input Image", sources=["upload", "clipboard"]) + with gr.Column(): + output_img = gr.Image(type="filepath", label="Output Image", sources=["upload", "clipboard"]) + with gr.Column(): + num_change = gr.Number(label="# Change Events", value=0) + num_input = gr.Number(label="# Input Events", value=0) + num_load = gr.Number(label="# Upload Events", value=0) + num_change_o = gr.Number(label="# Change Events Output", value=0) + num_clear = gr.Number(label="# Clear Events", value=0) + num_select = gr.Number(label="# Select Events", value=0) + + input_img.upload(lambda s, n: (s, n + 1), [input_img, num_load], [output_img, num_load]) + input_img.input(lambda n: n + 1, num_input, num_input) + input_img.change(lambda n: n + 1, num_change, num_change) + input_img.clear(lambda n: n + 1, num_clear, num_clear) + output_img.change(lambda n: n + 1, num_change_o, num_change_o) + output_img.select(test_select_is_defined, num_select, num_select) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_editor/run.py b/demo/image_editor/run.py new file mode 100644 index 0000000..e860c9c --- /dev/null +++ b/demo/image_editor/run.py @@ -0,0 +1,30 @@ +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") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_editor_canvas_size/run.py b/demo/image_editor_canvas_size/run.py new file mode 100644 index 0000000..5cb1929 --- /dev/null +++ b/demo/image_editor_canvas_size/run.py @@ -0,0 +1,25 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + image = gr.ImageEditor(label="Default Canvas. Not fixed", elem_id="default") + get_image = gr.Button("Get Default") + with gr.Column(): + custom_canvas = gr.ImageEditor(label="Custom Canvas, not fixed", canvas_size=(300, 300), + elem_id="small") + get_small = gr.Button("Get Small") + with gr.Column(): + custom_canvas_fixed = gr.ImageEditor(label="Custom Canvas,fixed", canvas_size=(500, 500), fixed_canvas=True, + elem_id="fixed") + get_fixed = gr.Button("Get Fixed") + with gr.Column(): + width = gr.Number(label="Width") + height = gr.Number(label="Height") + + get_image.click(lambda x: x["composite"].shape, outputs=[height, width], inputs=image) + get_small.click(lambda x: x["composite"].shape, outputs=[height, width], inputs=custom_canvas) + get_fixed.click(lambda x: x["composite"].shape, outputs=[height, width], inputs=custom_canvas_fixed) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/image_editor_events/run.py b/demo/image_editor_events/run.py new file mode 100644 index 0000000..a0407f4 --- /dev/null +++ b/demo/image_editor_events/run.py @@ -0,0 +1,68 @@ +import gradio as gr +import numpy as np + + +def predict(im): + return im["composite"] + + +def verify_clear(im): + print(im) + return int(not np.any(im["composite"])), im["composite"] + + +with gr.Blocks() as demo: + with gr.Group(): + with gr.Row(): + im = gr.ImageEditor( + type="numpy", + elem_id="image_editor", + ) + im_preview = gr.Image() + with gr.Group(): + with gr.Row(): + + n_upload = gr.Label( + 0, + label="upload", + elem_id="upload", + ) + n_change = gr.Label( + 0, + label="change", + elem_id="change", + ) + n_input = gr.Label( + 0, + label="input", + elem_id="input", + ) + n_apply = gr.Label( + 0, + label="apply", + elem_id="apply", + ) + cleared_properly = gr.Number(label="cleared properly") + clear_btn = gr.Button("Clear Button", elem_id="clear") + + im.upload( + lambda x: int(x) + 1, outputs=n_upload, inputs=n_upload, show_progress="hidden" + ) + im.change( + lambda x: int(x) + 1, outputs=n_change, inputs=n_change, show_progress="hidden" + ) + im.input( + lambda x: int(x) + 1, outputs=n_input, inputs=n_input, show_progress="hidden" + ) + im.apply( + lambda x: int(x) + 1, outputs=n_apply, inputs=n_apply, show_progress="hidden" + ) + im.change(predict, outputs=im_preview, inputs=im, show_progress="hidden") + clear_btn.click( + lambda: None, + None, + im, + ).then(verify_clear, inputs=im, outputs=[cleared_properly, im]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_editor_inpainting/run.py b/demo/image_editor_inpainting/run.py new file mode 100644 index 0000000..5b81a13 --- /dev/null +++ b/demo/image_editor_inpainting/run.py @@ -0,0 +1,33 @@ +import gradio as gr +import numpy as np + +def average_inpainting_colors(image): + mask = image["layers"][0] + img = image["background"] + mask_bool = mask[:,:,3] > 0 + if not np.any(mask_bool): + return img + selected_pixels = img[mask_bool] + avg_color = np.mean(selected_pixels, axis=0) + result = img.copy() + result[mask_bool] = avg_color + return result + +with gr.Blocks() as demo: + with gr.Row(): + image_editor = gr.ImageEditor( + type="numpy", + label="Input Image with Inpainting Layers", + interactive=True, + buttons=["fullscreen"] + ) + + output_image = gr.Image( + type="numpy", + label="Averaged Inpainting Layers" + ) + + image_editor.change(fn=average_inpainting_colors, inputs=image_editor, outputs=output_image) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/image_editor_layers/layer1.png b/demo/image_editor_layers/layer1.png new file mode 100644 index 0000000..57d0c40 Binary files /dev/null and b/demo/image_editor_layers/layer1.png differ diff --git a/demo/image_editor_layers/run.py b/demo/image_editor_layers/run.py new file mode 100644 index 0000000..88919cd --- /dev/null +++ b/demo/image_editor_layers/run.py @@ -0,0 +1,88 @@ +import gradio as gr +from pathlib import Path + +dir_ = Path(__file__).parent + + +def predict(im): + print(im) + return im, len(im["layers"]) + + +with gr.Blocks() as demo: + with gr.Row(): + im = gr.ImageEditor( + type="numpy", + interactive=True, + ) + im_preview = gr.ImageEditor( + interactive=True, + ) + + layer_updates = gr.Textbox(value="", label="Layer Updates") + num_layers = gr.Number(value=0, label="Num Layers") + example_ran = gr.Number(value=0, label="Example Ran") + + set_background = gr.Button("Set Background") + set_background.click( + lambda: { + "background": str(dir_ / "cheetah.jpg"), + "layers": None, + "composite": None, + }, + None, + im, + show_progress="hidden", + ) + set_layers = gr.Button("Set Layers") + set_layers.click( + lambda: { + "background": None, + "layers": [str(dir_ / "cheetah.jpg")], + "composite": None, + }, + None, + im, + show_progress="hidden", + ) + im.change( + lambda x: len(x["layers"]), + inputs=im, + outputs=layer_updates, + ) + set_composite = gr.Button("Set Composite") + set_composite.click( + lambda: { + "background": None, + "layers": None, + "composite": "https://huggingface.co/datasets/freddyaboulton/bucket/resolve/main/cheetah-003.jpg", + }, + None, + im, + show_progress="hidden", + ) + get_layers = gr.Button("Get Layers") + + get_layers.click( + predict, + outputs=[im_preview, num_layers], + inputs=im, + ) + + gr.Examples( + examples=[ + str(dir_ / "cheetah.jpg"), + { + "background": str(dir_ / "cheetah.jpg"), + "layers": [str(dir_ / "layer1.png")], + "composite": None, + }, + ], + inputs=im, + outputs=[example_ran], + fn=lambda x: 1, + run_on_click=True, + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_editor_many/run.py b/demo/image_editor_many/run.py new file mode 100644 index 0000000..7810d2b --- /dev/null +++ b/demo/image_editor_many/run.py @@ -0,0 +1,201 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Tab("Default ImageEditor"): + im = gr.ImageEditor(value="./cheetah.jpg", interactive=True, fixed_canvas=True) + with gr.Tab("Brush Options"): + with gr.Row(): + with gr.Column(): + image_editor = gr.ImageEditor( + value="./cheetah.jpg", + interactive=True, + fixed_canvas=True, + brush=gr.Brush( + default_size=10, + default_color="#000000", + colors=[ + "#FFA50050", + "#FF0000", + ("#00FF00", 0.5), + ("rgba(255, 255, 0, 0.5)", 0.5), + "rgba(255, 0, 0, 0.5)", + "pink", + "hsla(300, 100%, 50%, 0.5)", + ("hsl(300, 100%, 50%)", 0.8), + ("crimson", 0.5), + ("#000080", 1), + ], + ), + eraser=gr.Eraser(default_size=10), + ) + image_editor.apply(fn=lambda x: print(x), inputs=[image_editor]) + with gr.Column(): + with gr.Group(): + brush_size = gr.Slider( + minimum=1, + maximum=100, + value=10, + label="Brush Size", + ) + brush_opacity = gr.Slider( + minimum=0, + maximum=100, + value=50, + label="Brush Opacity", + ) + brush_color = gr.ColorPicker(value="#000000", label="Brush Color") + + with gr.Group(): + eraser_size = gr.Slider( + minimum=1, + maximum=100, + value=10, + label="Eraser Size", + ) + + update_btn = gr.Button("Apply Settings") + + status_msg = gr.Markdown( + value="**Adjust settings and click Apply.**", + ) + + def update_editor_settings(size, opacity, color, e_size): + + return ( + gr.ImageEditor( + brush=gr.Brush( + default_size=size, + default_color=(color, opacity / 100), + colors=[ + "#FFA50050", + "#FF0000", + ("#00FF00", 0.5), + ("rgba(255, 255, 0, 0.5)", 0.5), + "rgba(255, 0, 0, 0.5)", + "pink", + "hsla(300, 100%, 50%, 0.5)", + ("hsl(300, 100%, 50%)", 0.8), + ("crimson", 0.5), + ("#000080", 1), + ], + ), + eraser=gr.Eraser(default_size=e_size), + ), + f'### Settings applied:\n\n- `Brush.default_size={size}`\n- `Brush.default_color=("{color}", {opacity / 100})`\n- `Eraser.default_size={e_size}`', + ) + + update_btn.click( + update_editor_settings, + inputs=[brush_size, brush_opacity, brush_color, eraser_size], + outputs=[image_editor, status_msg], + ) + + def update_status(size=None, opacity=None, color=None, e_size=None): + changes = [] + if size is not None: + changes.append(f"`Brush.default_size={size}`") + if opacity is not None: + changes.append(f"`Brush.default_color=('{color}', {opacity / 100})`") + if color is not None: + if opacity is not None: + changes.append(f"`Brush.default_color=('{color}', {opacity / 100})`") + else: + changes.append(f"`Brush.default_color='{color}'`") + if e_size is not None: + changes.append(f"`Eraser.default_size={e_size}`") + + if changes: + return ( + "Settings changed:\n\n" + + "- " + + "\n- ".join(changes) + + "\n\n**Click Apply to update.**" + ) + return "" + + brush_size.change( + fn=update_status, + inputs=[brush_size, brush_opacity, brush_color, eraser_size], + outputs=status_msg, + ) + brush_opacity.change( + fn=update_status, + inputs=[brush_size, brush_opacity, brush_color, eraser_size], + outputs=status_msg, + ) + brush_color.change( + fn=update_status, + inputs=[brush_size, brush_opacity, brush_color, eraser_size], + outputs=status_msg, + ) + eraser_size.change( + fn=update_status, + inputs=[brush_size, brush_opacity, brush_color, eraser_size], + outputs=status_msg, + ) + with gr.Tab("Layer Options"): + with gr.Row(): + with gr.Column(): + im = gr.ImageEditor( + value="./cheetah.jpg", + interactive=True, + layers=gr.LayerOptions( + allow_additional_layers=False, layers=["Mask"] + ), + ) + with gr.Column(): + disable_layers = gr.Checkbox(value=False, label="Disable Layers") + allow_additional_layers = gr.Checkbox( + value=True, label="Allow Additional Layers" + ) + layer = gr.Dropdown( + choices=["Mask", "Mask 2"], + value="Mask", + label="Layer", + multiselect=True, + interactive=True, + allow_custom_value=True, + ) + gr.Checkbox(value=True, label="Allow Additional Layers") + update_layer_btn = gr.Button("Update Layer Options") + status_msg = gr.Markdown(value="**Adjust settings and click Apply.**") + + def update_layer_options( + disable_layers, allow_additional_layers, layer + ): + print( + "update_layer_options", + disable_layers, + allow_additional_layers, + layer, + ) + return ( + gr.ImageEditor( + value="./cheetah.jpg", + interactive=True, + layers=( + False + if disable_layers + else gr.LayerOptions( + allow_additional_layers=allow_additional_layers, + layers=layer, + ) + ), + ), + f"### Settings applied:\n\n- `LayerOptions=False`\n- `LayerOptions.allow_additional_layers={allow_additional_layers}`\n- `LayerOptions.layers={layer}`", + ) + + update_layer_btn.click( + update_layer_options, + inputs=[disable_layers, allow_additional_layers, layer], + outputs=[im, status_msg], + ) + + + with gr.Tab("ImageEditor Templates"): + gr.ImageMask(value="./cheetah.jpg", interactive=True) + gr.Paint(interactive=True) + gr.Sketchpad(interactive=True) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_editor_sketchpad/run.py b/demo/image_editor_sketchpad/run.py new file mode 100644 index 0000000..e999037 --- /dev/null +++ b/demo/image_editor_sketchpad/run.py @@ -0,0 +1,25 @@ +import gradio as gr +import numpy as np + +def percent_of_pixels_selected(image): + mask = image["layers"][0] + mask_bool = mask[:,:,3] > 0 + return f"{round(np.sum(mask_bool) / mask_bool.size * 100, 2)}%" + +image_editor = gr.Sketchpad( + type="numpy", +) +output_image = gr.Label( + label="Percent of Pixels Selected" +) + +demo = gr.Interface( + fn=percent_of_pixels_selected, + inputs=image_editor, + outputs=output_image, + live=True, + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/image_editor_story/run.py b/demo/image_editor_story/run.py new file mode 100644 index 0000000..adfbe52 --- /dev/null +++ b/demo/image_editor_story/run.py @@ -0,0 +1,34 @@ +import gradio as gr +from pathlib import Path +import subprocess + + +def predict(im): + path = str(Path(__file__).parent / "output-image.png") + with open(path, "wb") as f: + f.write(Path(im["composite"]).read_bytes()) + print("Writing to ", path) + return path + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + im = gr.ImageEditor(interactive=True, elem_id="image_editor", + canvas_size=(800, 600), + brush=gr.Brush(colors=["#ff0000", "#00ff00", "#0000ff"]), + type="filepath", + value={"background": "https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg", + "layers": [], + "composite": None}) + get = gr.Button("Get") + + with gr.Column(): + output = gr.Image(value=None, elem_id="output") + + get.click(predict, inputs=im, outputs=output) + +if __name__ == "__main__": + app, _, _ = demo.launch(prevent_thread_lock=True) + subprocess.call(["node", "js/storybook/ie_automation.js"]) + demo.close() diff --git a/demo/image_editor_webcam/run.py b/demo/image_editor_webcam/run.py new file mode 100644 index 0000000..5e5953e --- /dev/null +++ b/demo/image_editor_webcam/run.py @@ -0,0 +1,21 @@ +import gradio as gr + + +def predict(im): + return im["composite"] + + +with gr.Blocks() as demo: + with gr.Row(): + im = gr.ImageEditor( + canvas_size=(1024, 1024), + fixed_canvas=True, + webcam_options=gr.WebcamOptions( + constraints={"video": {"width": 1024, "height": 1024}} + ), + ) + im_preview = gr.Image() + im.change(predict, outputs=im_preview, inputs=im, show_progress="hidden") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_mod/run.py b/demo/image_mod/run.py new file mode 100644 index 0000000..705bde6 --- /dev/null +++ b/demo/image_mod/run.py @@ -0,0 +1,30 @@ +import gradio as gr +from gradio.media import get_image + +def image_mod(image): + return image.rotate(45) + +# get_image() returns file paths to sample media included with Gradio +new_samples = [ + [get_image("logo.png")], + [get_image("tower.jpg")], +] + +with gr.Blocks() as demo: + interface = gr.Interface( + image_mod, + gr.Image(type="pil"), + "image", + flagging_options=["blurry", "incorrect", "other"], + examples=[ + get_image("cheetah1.jpg"), + get_image("lion.jpg"), + ], + api_name="predict", + ) + + btn = gr.Button("Update Examples") + btn.click(lambda : gr.Dataset(samples=new_samples), None, interface.examples_handler.dataset) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_mod/screenshot.png b/demo/image_mod/screenshot.png new file mode 100644 index 0000000..c8af753 Binary files /dev/null and b/demo/image_mod/screenshot.png differ diff --git a/demo/image_mod_default_image/run.py b/demo/image_mod_default_image/run.py new file mode 100644 index 0000000..86ed666 --- /dev/null +++ b/demo/image_mod_default_image/run.py @@ -0,0 +1,18 @@ +import gradio as gr +from gradio.media import get_image + +def image_mod(image): + return image.rotate(45) + +# get_image() returns file paths to sample media included with Gradio +cheetah = get_image("cheetah1.jpg") + +demo = gr.Interface(image_mod, gr.Image(type="pil", value=cheetah), "image", + api_name="predict", + flagging_options=["blurry", "incorrect", "other"], examples=[ + get_image("lion.jpg"), + get_image("logo.png") + ]) + +if __name__ == "__main__": + demo.launch(max_file_size="70kb") diff --git a/demo/image_remote_url/run.py b/demo/image_remote_url/run.py new file mode 100644 index 0000000..f050d47 --- /dev/null +++ b/demo/image_remote_url/run.py @@ -0,0 +1,18 @@ +import gradio as gr + +gr.set_static_paths(paths=["test/test_files/bus.png", "test/test_files/cheetah1.jpg"]) + + +def fn(im): + return im, "test/test_files/cheetah1.jpg" + + +demo = gr.Interface( + fn=fn, + inputs=gr.Image("test/test_files/bus.png", label="InputImage"), + outputs=[gr.Image(label="Loopback"), gr.Image(label="RemoteImage")], + examples=[["test/test_files/bus.png"]], +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_segmentation/DESCRIPTION.md b/demo/image_segmentation/DESCRIPTION.md new file mode 100644 index 0000000..dbba2ae --- /dev/null +++ b/demo/image_segmentation/DESCRIPTION.md @@ -0,0 +1 @@ +Simple image segmentation using gradio's AnnotatedImage component. \ No newline at end of file diff --git a/demo/image_segmentation/requirements.txt b/demo/image_segmentation/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/image_segmentation/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/image_segmentation/run.py b/demo/image_segmentation/run.py new file mode 100644 index 0000000..af3793f --- /dev/null +++ b/demo/image_segmentation/run.py @@ -0,0 +1,61 @@ +import gradio as gr +import numpy as np +import random + +with gr.Blocks() as demo: + section_labels = [ + "apple", + "banana", + "carrot", + "donut", + "eggplant", + "fish", + "grapes", + "hamburger", + "ice cream", + "juice", + ] + + with gr.Row(): + num_boxes = gr.Slider(0, 5, 2, step=1, label="Number of boxes") + num_segments = gr.Slider(0, 5, 1, step=1, label="Number of segments") + + with gr.Row(): + img_input = gr.Image() + img_output = gr.AnnotatedImage( + color_map={"banana": "#a89a00", "carrot": "#ffae00"} + ) + + section_btn = gr.Button("Identify Sections") + selected_section = gr.Textbox(label="Selected Section") + + def section(img, num_boxes, num_segments): + sections = [] + for a in range(num_boxes): + x = random.randint(0, img.shape[1]) + y = random.randint(0, img.shape[0]) + w = random.randint(0, img.shape[1] - x) + h = random.randint(0, img.shape[0] - y) + sections.append(((x, y, x + w, y + h), section_labels[a])) + for b in range(num_segments): + x = random.randint(0, img.shape[1]) + y = random.randint(0, img.shape[0]) + r = random.randint(0, min(x, y, img.shape[1] - x, img.shape[0] - y)) + mask = np.zeros(img.shape[:2]) + for i in range(img.shape[0]): + for j in range(img.shape[1]): + dist_square = (i - y) ** 2 + (j - x) ** 2 + if dist_square < r**2: + mask[i, j] = round((r**2 - dist_square) / r**2 * 4) / 4 + sections.append((mask, section_labels[b + num_boxes])) + return (img, sections) + + section_btn.click(section, [img_input, num_boxes, num_segments], img_output) + + def select_section(evt: gr.SelectData): + return section_labels[evt.index] + + img_output.select(select_section, None, selected_section) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_selections/requirements.txt b/demo/image_selections/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/image_selections/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/image_selections/run.py b/demo/image_selections/run.py new file mode 100644 index 0000000..d8fbbeb --- /dev/null +++ b/demo/image_selections/run.py @@ -0,0 +1,45 @@ +import gradio as gr +import numpy as np + +with gr.Blocks() as demo: + tolerance = gr.Slider(label="Tolerance", info="How different colors can be in a segment.", minimum=0, maximum=256*3, value=50) + with gr.Row(): + input_img = gr.Image(label="Input") + output_img = gr.Image(label="Selected Segment") + + def get_select_coords(img, tolerance, evt: gr.SelectData): + visited_pixels = set() + pixels_in_queue = set() + pixels_in_segment = set() + start_pixel = img[evt.index[1], evt.index[0]] + pixels_in_queue.add((evt.index[1], evt.index[0])) + while len(pixels_in_queue) > 0: + pixel = pixels_in_queue.pop() + visited_pixels.add(pixel) + neighbors = [] + if pixel[0] > 0: + neighbors.append((pixel[0] - 1, pixel[1])) + if pixel[0] < img.shape[0] - 1: + neighbors.append((pixel[0] + 1, pixel[1])) + if pixel[1] > 0: + neighbors.append((pixel[0], pixel[1] - 1)) + if pixel[1] < img.shape[1] - 1: + neighbors.append((pixel[0], pixel[1] + 1)) + for neighbor in neighbors: + if neighbor in visited_pixels: + continue + neighbor_pixel = img[neighbor[0], neighbor[1]] + if np.abs(neighbor_pixel - start_pixel).sum() < tolerance: + pixels_in_queue.add(neighbor) + pixels_in_segment.add(neighbor) + + out = img.copy() * 0.2 + out = out.astype(np.uint8) + for pixel in pixels_in_segment: + out[pixel[0], pixel[1]] = img[pixel[0], pixel[1]] + return out + + input_img.select(get_select_coords, [input_img, tolerance], output_img) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/image_watermark/files/bird.bmp b/demo/image_watermark/files/bird.bmp new file mode 100644 index 0000000..98efb0c Binary files /dev/null and b/demo/image_watermark/files/bird.bmp differ diff --git a/demo/image_watermark/files/logo_nontrans.png b/demo/image_watermark/files/logo_nontrans.png new file mode 100644 index 0000000..c218870 Binary files /dev/null and b/demo/image_watermark/files/logo_nontrans.png differ diff --git a/demo/image_watermark/run.py b/demo/image_watermark/run.py new file mode 100644 index 0000000..6050102 --- /dev/null +++ b/demo/image_watermark/run.py @@ -0,0 +1,22 @@ +import gradio as gr +import os +from gradio.media import get_image + +# get_image() returns file paths to sample media included with Gradio +base_a = get_image("groot.jpeg") +base_b = os.path.join(os.path.dirname(__file__), "files/bird.bmp") + +watermark_a = get_image("hf-logo_transpng.png") +watermark_b = os.path.join(os.path.dirname(__file__), "files/logo_nontrans.png") +watermark_c = get_image("logo.png") + +def generate_image(original_image, watermark_image): + return gr.Image(original_image, watermark=gr.WatermarkOptions(watermark=watermark_image, position='bottom-left')) + + +demo = gr.Interface(generate_image, [gr.Image(image_mode=None), gr.Image(image_mode=None)], gr.Image(), + api_name="predict", + examples=[[base_a, watermark_a], [base_b, watermark_b], [base_a, watermark_c], [base_a, watermark_c]]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/imageeditor_component/run.py b/demo/imageeditor_component/run.py new file mode 100644 index 0000000..1b4d52a --- /dev/null +++ b/demo/imageeditor_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.ImageEditor() + +demo.launch() diff --git a/demo/imageslider/run.py b/demo/imageslider/run.py new file mode 100644 index 0000000..456caa1 --- /dev/null +++ b/demo/imageslider/run.py @@ -0,0 +1,40 @@ +import gradio as gr +from PIL import ImageFilter + + +def img_to_slider(im): + if not im: + return im + return (im, im.filter(filter=ImageFilter.GaussianBlur(radius=10))) + + +def slider_to_self(im): + if not im or not im[0]: + return im + return (im[0], im[0].filter(filter=ImageFilter.GaussianBlur(radius=10))) + + +def slider_to_self_two(im): + return im + + +def position_to_slider(pos): + return gr.ImageSlider(slider_position=pos) + + +with gr.Blocks() as demo: + gr.Markdown("## img to image slider") + with gr.Row(): + img1 = gr.Image(label="Blur image", type="pil") + img2 = gr.ImageSlider(label="Blur image", type="pil") + btn = gr.Button("Blur image") + btn.click(img_to_slider, inputs=img1, outputs=img2) + gr.Markdown("## unified image slider") + with gr.Row(): + img3 = gr.ImageSlider(label="Blur image", type="pil") + img3.upload(slider_to_self, inputs=img3, outputs=img3) + pos = gr.Slider(label="Position", value=50, minimum=0, maximum=100, step=0.01) + pos.change(position_to_slider, inputs=pos, outputs=img3, show_progress="hidden") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/input_output/run.py b/demo/input_output/run.py new file mode 100644 index 0000000..394d834 --- /dev/null +++ b/demo/input_output/run.py @@ -0,0 +1,14 @@ +import gradio as gr + +def image_mod(text): + return text[::-1] + +demo = gr.Blocks() + +with demo: + text = gr.Textbox(label="Input-Output") + btn = gr.Button("Run") + btn.click(image_mod, text, text) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/interface_random_slider/run.py b/demo/interface_random_slider/run.py new file mode 100644 index 0000000..0a9ba70 --- /dev/null +++ b/demo/interface_random_slider/run.py @@ -0,0 +1,21 @@ +import gradio as gr + +def func(slider_1, slider_2, *args): + return slider_1 + slider_2 * 5 + +demo = gr.Interface( + func, + [ + gr.Slider(minimum=1.5, maximum=250000.89, randomize=True, label="Random Big Range"), + gr.Slider(minimum=-1, maximum=1, randomize=True, step=0.05, label="Random only multiple of 0.05 allowed"), + gr.Slider(minimum=0, maximum=1, randomize=True, step=0.25, label="Random only multiples of 0.25 allowed"), + gr.Slider(minimum=-100, maximum=100, randomize=True, step=3, label="Random between -100 and 100 step 3"), + gr.Slider(minimum=-100, maximum=100, randomize=True, label="Random between -100 and 100"), + gr.Slider(value=0.25, minimum=5, maximum=30, step=-1), + ], + "number", + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/interface_state/run.py b/demo/interface_state/run.py new file mode 100644 index 0000000..af209ea --- /dev/null +++ b/demo/interface_state/run.py @@ -0,0 +1,18 @@ +import gradio as gr + +def store_message(message: str, history: list[str]): # type: ignore + output = { + "Current messages": message, + "Previous messages": history[::-1] + } + history.append(message) + return output, history + +demo = gr.Interface(fn=store_message, + inputs=["textbox", gr.State(value=[])], + outputs=["json", gr.State()], + api_name="predict" + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/interface_with_additional_inputs/run.py b/demo/interface_with_additional_inputs/run.py new file mode 100644 index 0000000..be5e05d --- /dev/null +++ b/demo/interface_with_additional_inputs/run.py @@ -0,0 +1,19 @@ +import gradio as gr + +def generate_fake_image(prompt, seed, initial_image=None): + return f"Used seed: {seed}", "https://dummyimage.com/300/09f.png" + +demo = gr.Interface( + generate_fake_image, + inputs=["textbox"], + outputs=["textbox", "image"], + additional_inputs=[ + gr.Slider(0, 1000), + "image" + ], + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() + diff --git a/demo/invisible_textbox/run.py b/demo/invisible_textbox/run.py new file mode 100644 index 0000000..4c584d7 --- /dev/null +++ b/demo/invisible_textbox/run.py @@ -0,0 +1,49 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Tabs(): + with gr.Tab("Invisible Textbox Demo"): + textbox = gr.Textbox(visible=False, interactive=True, elem_id="test-textbox") + + with gr.Row(): + make_visible_btn = gr.Button("Show") + hide = gr.Button("Hide") + make_invisible_btn = gr.Button("Make Invisible") + + def show(): + return gr.Textbox(visible=True) + make_visible_btn.click(fn=show, outputs=textbox) + hide.click(lambda: gr.Textbox(visible=False), outputs=textbox) + make_invisible_btn.click(lambda: gr.Textbox(visible="hidden"), outputs=textbox) + with gr.Tab("Another Tab"): + msg = gr.Markdown("This is another tab to demonstrate that invisible components work across tabs.", visible=False) + show_message = gr.Button("Show Message") + show_message.click(lambda: gr.Markdown(visible=True), outputs=msg) + with gr.Tab("Third Tab"): + with gr.Accordion("Third Tab Accordion", open=True, visible=False) as acc: + third_msg = gr.Textbox(label="Visible Textbox", interactive=True, visible=True) + hidden_number = gr.Number(visible=False, label="Hidden Number", value=100, elem_id="hidden-number") + show_number_btn = gr.Button("Show Number") + hide_number_btn = gr.Button("Hide Number") + show_number_btn.click(lambda: gr.Number(visible=True), outputs=hidden_number) + hide_number_btn.click(lambda: gr.Number(visible=False), outputs=hidden_number) + + show_third_message = gr.Button("Show Accordion") + show_third_message.click(lambda: gr.Accordion(visible=True), outputs=acc) + hide_third_message = gr.Button("Hide Accordion") + hide_third_message.click(lambda: gr.Accordion(visible=False), outputs=acc) + with gr.Tab("Sliders Tab"): + slider1 = gr.Slider(0, 1, value=0, visible=False, elem_id="slider-1") + slider2 = gr.Slider(0, 1, value=0, visible=False, elem_id="slider-2") + show_sliders_btn = gr.Button("Show Sliders") + hide_sliders_btn = gr.Button("Hide Sliders") + show_sliders_btn.click( + lambda: (gr.Slider(visible=True), gr.Slider(visible=True)), + outputs=[slider1, slider2], + ) + hide_sliders_btn.click( + lambda: (gr.Slider(visible=False), gr.Slider(visible=False)), + outputs=[slider1, slider2], + ) +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/json_component/requirements.txt b/demo/json_component/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/json_component/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/json_component/run.py b/demo/json_component/run.py new file mode 100644 index 0000000..315a663 --- /dev/null +++ b/demo/json_component/run.py @@ -0,0 +1,24 @@ +import gradio as gr +import numpy as np + +with gr.Blocks() as demo: + inp = gr.JSON( + label="InputJSON", + value={ + "Key 1": "Value 1", + "Key 2": {"Key 3": "Value 2", "Key 4": "Value 3"}, + "Key 5": ["Item 1", "Item 2", "Item 3"], + "Key 6": 123, + "Key 7": 123.456, + "Key 8": True, + "Key 9": False, + "Key 10": None, + "Key 11": np.array([1, 2, 3]), + } + ) + out = gr.JSON(label="OutputJSON") + btn = gr.Button("Submit") + btn.click(lambda x: x, inp, out) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/kitchen_sink/requirements.txt b/demo/kitchen_sink/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/kitchen_sink/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/kitchen_sink/run.py b/demo/kitchen_sink/run.py new file mode 100755 index 0000000..ca060c6 --- /dev/null +++ b/demo/kitchen_sink/run.py @@ -0,0 +1,166 @@ +import os +import json + +import numpy as np + +import gradio as gr +from gradio.media import get_image, get_video, get_audio, get_file + +CHOICES = ["foo", "bar", "baz"] +JSONOBJ = """{"items":{"item":[{"id": "0001","type": null,"is_good": false,"ppu": 0.55,"batters":{"batter":[{ "id": "1001", "type": "Regular" },{ "id": "1002", "type": "Chocolate" },{ "id": "1003", "type": "Blueberry" },{ "id": "1004", "type": "Devil's Food" }]},"topping":[{ "id": "5001", "type": "None" },{ "id": "5002", "type": "Glazed" },{ "id": "5005", "type": "Sugar" },{ "id": "5007", "type": "Powdered Sugar" },{ "id": "5006", "type": "Chocolate with Sprinkles" },{ "id": "5003", "type": "Chocolate" },{ "id": "5004", "type": "Maple" }]}]}}""" + +def fn( + text1, + text2, + num, + slider1, + slider2, + single_checkbox, + checkboxes, + radio, + dropdown, + multi_dropdown, + im1, + # im2, + # im3, + im4, + video, + audio1, + audio2, + file, + df1, + time, +): + return ( + (text1 if single_checkbox else text2) + + ", selected:" + + ", ".join(checkboxes), # Text + { + "positive": num / (num + slider1 + slider2), + "negative": slider1 / (num + slider1 + slider2), + "neutral": slider2 / (num + slider1 + slider2), + }, # Label + (audio1[0], np.flipud(audio1[1])) + if audio1 is not None + else get_audio("cantina.wav"), # Audio + np.flipud(im1) + if im1 is not None + else get_image("cheetah1.jpg"), # Image + video + if video is not None + else get_video("world.mp4"), # Video + [ + ("The", "art"), + ("quick brown", "adj"), + ("fox", "nn"), + ("jumped", "vrb"), + ("testing testing testing", None), + ("over", "prp"), + ("the", "art"), + ("testing", None), + ("lazy", "adj"), + ("dogs", "nn"), + (".", "punc"), + ] + + [(f"test {x}", f"test {x}") for x in range(10)], # HighlightedText + # [("The testing testing testing", None), ("quick brown", 0.2), ("fox", 1), ("jumped", -1), ("testing testing testing", 0), ("over", 0), ("the", 0), ("testing", 0), ("lazy", 1), ("dogs", 0), (".", 1)] + [(f"test {x}", x/10) for x in range(-10, 10)], # HighlightedText + [ + ("The testing testing testing", None), + ("over", 0.6), + ("the", 0.2), + ("testing", None), + ("lazy", -0.1), + ("dogs", 0.4), + (".", 0), + ] + + [("test", x / 10) for x in range(-10, 10)], # HighlightedText + json.loads(JSONOBJ), # JSON + "", # HTML + get_file("titanic.csv"), # File + df1, # Dataframe + np.random.randint(0, 10, (4, 4)), # Dataframe + time, # DateTime + ) + +demo = gr.Interface( + fn, + inputs=[ + gr.Textbox(value="Lorem ipsum", label="Textbox"), + gr.Textbox(lines=3, placeholder="Type here..", label="Textbox 2"), + gr.Number(label="Number", value=42), + gr.Slider(10, 20, value=15, label="Slider: 10 - 20"), + gr.Slider(maximum=20, step=0.04, label="Slider: step @ 0.04"), + gr.Checkbox(label="Checkbox"), + gr.CheckboxGroup(label="CheckboxGroup", choices=CHOICES, value=CHOICES[0:2]), + gr.Radio(label="Radio", choices=CHOICES, value=CHOICES[2]), + gr.Dropdown(label="Dropdown", choices=CHOICES), + gr.Dropdown( + label="Multiselect Dropdown (Max choice: 2)", + choices=CHOICES, + multiselect=True, + max_choices=2, + ), + gr.Image(label="Image"), + # gr.Image(label="Image w/ Cropper", tool="select"), + # gr.Image(label="Sketchpad", source="canvas"), + gr.Image(label="Webcam", sources=["webcam"]), + gr.Video(label="Video"), + gr.Audio(label="Audio"), + gr.Audio(label="Microphone", sources=["microphone"]), + gr.File(label="File"), + gr.Dataframe(label="Dataframe", headers=["Name", "Age", "Gender"]), + gr.DateTime(label="DateTime"), + ], + outputs=[ + gr.Textbox(label="Textbox"), + gr.Label(label="Label"), + gr.Audio(label="Audio"), + gr.Image(label="Image", elem_id="output-img"), + gr.Video(label="Video"), + gr.HighlightedText( + label="HighlightedText", color_map={"punc": "pink", "test 0": "blue"} + ), + gr.HighlightedText(label="HighlightedText", show_legend=True), + gr.JSON(label="JSON", show_indices=True), + gr.HTML(label="HTML"), + gr.File(label="File"), + gr.Dataframe(label="Dataframe"), + gr.Dataframe(label="Numpy"), + gr.DateTime(label="DateTime"), + ], + examples=[ + [ + "the quick brown fox", + "jumps over the lazy dog", + 10, + 12, + 4, + True, + ["foo", "baz"], + "baz", + "bar", + ["foo", "bar"], + get_image("cheetah1.jpg"), + # get_image("cheetah1.jpg"), + # get_image("cheetah1.jpg"), + get_image("cheetah1.jpg"), + get_video("world.mp4"), + get_audio("cantina.wav"), + get_audio("cantina.wav"), + get_file("titanic.csv"), + [[1, 2, 3, 4], [4, 5, 6, 7], [8, 9, 1, 2], [3, 4, 5, 6]], + "2025-06-10 12:00:00", + ] + ] + * 3, + title="Kitchen Sink", + description="Try out all the components!", + article="Learn more about [Gradio](http://gradio.app)", + cache_examples=True, + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/kitchen_sink_random/__init__.py b/demo/kitchen_sink_random/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/kitchen_sink_random/constants.py b/demo/kitchen_sink_random/constants.py new file mode 100644 index 0000000..239bb2f --- /dev/null +++ b/demo/kitchen_sink_random/constants.py @@ -0,0 +1,59 @@ +import numpy as np +import matplotlib.pyplot as plt +import random +from gradio.media import get_model3d + +def random_plot(): + start_year = 2020 + x = np.arange(start_year, start_year + random.randint(0, 10)) + year_count = x.shape[0] + plt_format = "-" + fig = plt.figure() + ax = fig.add_subplot(111) + series = np.arange(0, year_count, dtype=float) + series = series**2 + series += np.random.rand(year_count) + ax.plot(x, series, plt_format) + return fig + +highlighted_text_output_1 = [ + { + "entity": "I-LOC", + "score": 0.9988978, + "index": 2, + "word": "Chicago", + "start": 5, + "end": 12, + }, + { + "entity": "I-MISC", + "score": 0.9958592, + "index": 5, + "word": "Pakistani", + "start": 22, + "end": 31, + }, +] +highlighted_text_output_2 = [ + { + "entity": "I-LOC", + "score": 0.9988978, + "index": 2, + "word": "Chicago", + "start": 5, + "end": 12, + }, + { + "entity": "I-LOC", + "score": 0.9958592, + "index": 5, + "word": "Pakistan", + "start": 22, + "end": 30, + }, +] + +highlighted_text = "Does Chicago have any Pakistani restaurants" + +def random_model3d(): + return get_model3d() diff --git a/demo/kitchen_sink_random/requirements.txt b/demo/kitchen_sink_random/requirements.txt new file mode 100644 index 0000000..babdd14 --- /dev/null +++ b/demo/kitchen_sink_random/requirements.txt @@ -0,0 +1,2 @@ +matplotlib +pandas diff --git a/demo/kitchen_sink_random/run.py b/demo/kitchen_sink_random/run.py new file mode 100644 index 0000000..298991b --- /dev/null +++ b/demo/kitchen_sink_random/run.py @@ -0,0 +1,93 @@ +import gradio as gr +from datetime import datetime +import random +import string +import pandas as pd + +# get_audio(), get_video(), get_image(), get_model3d(), get_file() return file paths to sample media included with Gradio +from gradio.media import get_audio, get_video, get_image, get_model3d, get_file + +from constants import ( # type: ignore + highlighted_text, + highlighted_text_output_2, + highlighted_text_output_1, + random_plot, +) + +demo = gr.Interface( + lambda *args: args[0], + inputs=[ + gr.Textbox(value=lambda: datetime.now(), label="Current Time"), + gr.Number(value=lambda: random.random(), label="Ranom Percentage"), + gr.Slider(minimum=-1, maximum=1, randomize=True, label="Slider with randomize"), + gr.Slider( + minimum=0, + maximum=1, + value=lambda: random.random(), + label="Slider with value func", + ), + gr.Checkbox(value=lambda: random.random() > 0.5, label="Random Checkbox"), + gr.CheckboxGroup( + choices=["a", "b", "c", "d"], + value=lambda: random.choice(["a", "b", "c", "d"]), + label="Random CheckboxGroup", + ), + gr.Radio( + choices=list(string.ascii_lowercase), + value=lambda: random.choice(string.ascii_lowercase), + ), + gr.Dropdown( + choices=["a", "b", "c", "d", "e"], + value=lambda: random.choice(["a", "b", "c"]), + ), + gr.Image( + value=lambda: get_image() + ), + gr.Video(value=lambda: get_video("world.mp4")), + gr.Audio(value=lambda: get_audio("cantina.wav")), + gr.File( + value=lambda: get_file("titanic.csv") + ), + gr.Dataframe( + value=lambda: pd.DataFrame( + {"random_number_rows": range(random.randint(0, 10))} + ) + ), + gr.State(value=lambda: random.choice(string.ascii_lowercase)), + gr.ColorPicker(value=lambda: random.choice(["#000000", "#ff0000", "#0000FF"])), + gr.Label(value=lambda: random.choice(["Pedestrian", "Car", "Cyclist"])), + gr.HighlightedText( + value=lambda: random.choice( + [ + {"text": highlighted_text, "entities": highlighted_text_output_1}, + {"text": highlighted_text, "entities": highlighted_text_output_2}, + ] + ), + ), + gr.JSON(value=lambda: random.choice([{"a": 1}, {"b": 2}])), + gr.HTML( + value=lambda: random.choice( + [ + '

I am red

', + '

I am blue

', + ] + ) + ), + gr.Gallery( + value=lambda: [get_image() for _ in range(3)] + ), + gr.Chatbot( + value=lambda: random.choice([[("hello", "hi!")], [("bye", "goodbye!")]]) + ), + gr.Model3D(value=lambda: get_model3d()), + gr.Plot(value=random_plot), + gr.Markdown(value=lambda: f"### {random.choice(['Hello', 'Hi', 'Goodbye!'])}"), + ], + outputs=[ + gr.State(value=lambda: random.choice(string.ascii_lowercase)) + ], + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/label_component/run.py b/demo/label_component/run.py new file mode 100644 index 0000000..939732f --- /dev/null +++ b/demo/label_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Label(value={"First Label": 0.7, "Second Label": 0.2, "Third Label": 0.1}) + +demo.launch() diff --git a/demo/latex/run.py b/demo/latex/run.py new file mode 100644 index 0000000..abf27dc --- /dev/null +++ b/demo/latex/run.py @@ -0,0 +1,26 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown( + r""" + # Hello World! $\frac{\sqrt{x + y}}{4}$ is today's lesson + + ## the $\sqrt{x + y}$ is first + + Start with $\frac{\frac{x+1}{x+2}}{x+3}$ then we get $ 2+x $ and $3$. + + There are three formulas to know: + + the first is $\gamma^2 + \theta^2 = \omega^2$ + + $\sqrt{x^2+1}$ is next + + Integral $\int_{a}^{b} x^2 \,dx$ is last + + Start typing below to see the output. + + I spent $5 at the grocery store. Then I bought a $2.50 ice cream cone. + """) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/letter_counter/run.py b/demo/letter_counter/run.py new file mode 100644 index 0000000..ba94b47 --- /dev/null +++ b/demo/letter_counter/run.py @@ -0,0 +1,29 @@ +import gradio as gr + +def letter_counter(word, letter): + """ + Count the number of occurrences of a letter in a word or text. + + Args: + word (str): The input text to search through + letter (str): The letter to search for + + Returns: + str: A message indicating how many times the letter appears + """ + word = word.lower() + letter = letter.lower() + count = word.count(letter) + return count + +demo = gr.Interface( + fn=letter_counter, + inputs=[gr.Textbox("strawberry"), gr.Textbox("r")], + outputs=[gr.Number()], + title="Letter Counter", + description="Enter text and a letter to count how many times the letter appears in the text.", + api_name="predict" +) + +if __name__ == "__main__": + demo.launch(mcp_server=True) diff --git a/demo/line_plot/requirements.txt b/demo/line_plot/requirements.txt new file mode 100644 index 0000000..7e0e60e --- /dev/null +++ b/demo/line_plot/requirements.txt @@ -0,0 +1,2 @@ +vega_datasets +pandas \ No newline at end of file diff --git a/demo/line_plot/run.py b/demo/line_plot/run.py new file mode 100644 index 0000000..17f5b11 --- /dev/null +++ b/demo/line_plot/run.py @@ -0,0 +1,95 @@ +import gradio as gr +from vega_datasets import data + +stocks = data.stocks() +gapminder = data.gapminder() +gapminder = gapminder.loc[ + gapminder.country.isin(["Argentina", "Australia", "Afghanistan"]) +] +climate = data.climate() +seattle_weather = data.seattle_weather() + +## Or generate your own fake data, here's an example for stocks: +# +# import pandas as pd +# import random +# +# stocks = pd.DataFrame( +# { +# "symbol": [ +# random.choice( +# [ +# "MSFT", +# "AAPL", +# "AMZN", +# "IBM", +# "GOOG", +# ] +# ) +# for _ in range(120) +# ], +# "date": [ +# pd.Timestamp(year=2000 + i, month=j, day=1) +# for i in range(10) +# for j in range(1, 13) +# ], +# "price": [random.randint(10, 200) for _ in range(120)], +# } +# ) + +def line_plot_fn(dataset): + if dataset == "stocks": + return gr.LinePlot( + stocks, + x="date", + y="price", + color="symbol", + title="Stock Prices", + tooltip=["date", "price", "symbol"], + height=300, + ) + elif dataset == "climate": + return gr.LinePlot( + climate, + x="DATE", + y="HLY-TEMP-NORMAL", + y_lim=[250, 500], + title="Climate", + tooltip=["DATE", "HLY-TEMP-NORMAL"], + height=300, + ) + elif dataset == "seattle_weather": + return gr.LinePlot( + seattle_weather, + x="date", + y="temp_min", + tooltip=["weather", "date"], + title="Seattle Weather", + height=300, + ) + elif dataset == "gapminder": + return gr.LinePlot( + gapminder, + x="year", + y="life_expect", + color="country", + title="Life expectancy for countries", + x_lim=[1950, 2010], + tooltip=["country", "life_expect"], + height=300, + ) + +with gr.Blocks() as line_plot: + with gr.Row(): + with gr.Column(): + dataset = gr.Dropdown( + choices=["stocks", "climate", "seattle_weather", "gapminder"], + value="stocks", + ) + with gr.Column(): + plot = gr.LinePlot() + dataset.change(line_plot_fn, inputs=dataset, outputs=plot) + line_plot.load(fn=line_plot_fn, inputs=dataset, outputs=plot) + +if __name__ == "__main__": + line_plot.launch() diff --git a/demo/line_plot_demo/requirements.txt b/demo/line_plot_demo/requirements.txt new file mode 100644 index 0000000..fb6c7ed --- /dev/null +++ b/demo/line_plot_demo/requirements.txt @@ -0,0 +1 @@ +pandas diff --git a/demo/line_plot_demo/run.py b/demo/line_plot_demo/run.py new file mode 100644 index 0000000..f227c53 --- /dev/null +++ b/demo/line_plot_demo/run.py @@ -0,0 +1,86 @@ +import pandas as pd +from random import randint, random +import gradio as gr + + +temp_sensor_data = pd.DataFrame( + { + "time": pd.date_range("2021-01-01", end="2021-01-05", periods=200), + "temperature": [randint(50 + 10 * (i % 2), 65 + 15 * (i % 2)) for i in range(200)], + "humidity": [randint(50 + 10 * (i % 2), 65 + 15 * (i % 2)) for i in range(200)], + "location": ["indoor", "outdoor"] * 100, + } +) + +food_rating_data = pd.DataFrame( + { + "cuisine": [["Italian", "Mexican", "Chinese"][i % 3] for i in range(100)], + "rating": [random() * 4 + 0.5 * (i % 3) for i in range(100)], + "price": [randint(10, 50) + 4 * (i % 3) for i in range(100)], + "wait": [random() for i in range(100)], + } +) + +with gr.Blocks() as line_plots: + with gr.Row(): + start = gr.DateTime("2021-01-01 00:00:00", label="Start") + end = gr.DateTime("2021-01-05 00:00:00", label="End") + apply_btn = gr.Button("Apply", scale=0) + with gr.Row(): + group_by = gr.Radio(["None", "30m", "1h", "4h", "1d"], value="None", label="Group by") + aggregate = gr.Radio(["sum", "mean", "median", "min", "max"], value="sum", label="Aggregation") + + temp_by_time = gr.LinePlot( + temp_sensor_data, + x="time", + y="temperature", + ) + temp_by_time_location = gr.LinePlot( + temp_sensor_data, + x="time", + y="temperature", + color="location", + ) + + time_graphs = [temp_by_time, temp_by_time_location] + group_by.change( + lambda group: [gr.LinePlot(x_bin=None if group == "None" else group)] * len(time_graphs), + group_by, + time_graphs + ) + aggregate.change( + lambda aggregate: [gr.LinePlot(y_aggregate=aggregate)] * len(time_graphs), + aggregate, + time_graphs + ) + + def rescale(select: gr.SelectData): + return select.index + rescale_evt = gr.on([plot.select for plot in time_graphs], rescale, None, [start, end]) + + for trigger in [apply_btn.click, rescale_evt.then]: + trigger( + lambda start, end: [gr.LinePlot(x_lim=[start, end])] * len(time_graphs), [start, end], time_graphs + ) + + price_by_cuisine = gr.LinePlot( + food_rating_data, + x="cuisine", + y="price", + ) + with gr.Row(): + price_by_rating = gr.LinePlot( + food_rating_data, + x="rating", + y="price", + ) + price_by_rating_color = gr.LinePlot( + food_rating_data, + x="rating", + y="price", + color="cuisine", + color_map={"Italian": "red", "Mexican": "green", "Chinese": "blue"}, + ) + +if __name__ == "__main__": + line_plots.launch() diff --git a/demo/lineplot_component/requirements.txt b/demo/lineplot_component/requirements.txt new file mode 100644 index 0000000..d1c8a7a --- /dev/null +++ b/demo/lineplot_component/requirements.txt @@ -0,0 +1 @@ +vega_datasets \ No newline at end of file diff --git a/demo/lineplot_component/run.py b/demo/lineplot_component/run.py new file mode 100644 index 0000000..1fbc6f2 --- /dev/null +++ b/demo/lineplot_component/run.py @@ -0,0 +1,17 @@ +import gradio as gr +from vega_datasets import data + +with gr.Blocks() as demo: + gr.LinePlot( + data.stocks(), + x="date", + y="price", + color="symbol", + title="Stock Prices", + tooltip=["date", "price", "symbol"], + height=300, + container=False, + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/live_dashboard/DESCRIPTION.md b/demo/live_dashboard/DESCRIPTION.md new file mode 100644 index 0000000..4d2bfeb --- /dev/null +++ b/demo/live_dashboard/DESCRIPTION.md @@ -0,0 +1,3 @@ +This demo shows how you can build a live interactive dashboard with gradio. +The current time is refreshed every second and the plot every half second by using the 'every' keyword in the event handler. +Changing the value of the slider will control the period of the sine curve (the distance between peaks). \ No newline at end of file diff --git a/demo/live_dashboard/requirements.txt b/demo/live_dashboard/requirements.txt new file mode 100644 index 0000000..38c5515 --- /dev/null +++ b/demo/live_dashboard/requirements.txt @@ -0,0 +1,3 @@ +numpy +pandas +plotly diff --git a/demo/live_dashboard/run.py b/demo/live_dashboard/run.py new file mode 100644 index 0000000..16052c7 --- /dev/null +++ b/demo/live_dashboard/run.py @@ -0,0 +1,51 @@ +import math + +import pandas as pd + +import gradio as gr +import datetime +import numpy as np + +def get_time(): + return datetime.datetime.now() + +plot_end = 2 * math.pi + +def get_plot(period=1): + global plot_end + x = np.arange(plot_end - 2 * math.pi, plot_end, 0.02) + y = np.sin(2 * math.pi * period * x) + update = gr.LinePlot( + value=pd.DataFrame({"x": x, "y": y}), + x="x", + y="y", + title="Plot (updates every second)", + height=350, + ) + plot_end += 0.1 + return update + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + c_time2 = gr.Textbox(label="Current Time refreshed every second") + period = gr.Slider( + label="Period of plot", value=1, minimum=0, maximum=10 + ) + plot = gr.LinePlot(show_label=False) + with gr.Column(): + start_time = gr.Textbox(label="Start Time") + end_time = gr.Textbox(label="End Time") + + timer = gr.Timer(1) + + timer.tick(lambda: datetime.datetime.now(), None, c_time2) + timer.tick(get_plot, period, plot) + + def select(selection_range: gr.SelectData): + return gr.LinePlot(x_lim=selection_range.index), selection_range.index[0], selection_range.index[1] + plot.select(select, None, [plot, start_time, end_time]) + plot.double_click(lambda: gr.LinePlot(x_lim=None), None, plot) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/live_with_vars/run.py b/demo/live_with_vars/run.py new file mode 100644 index 0000000..736b165 --- /dev/null +++ b/demo/live_with_vars/run.py @@ -0,0 +1,9 @@ +import gradio as gr + +demo = gr.Interface( + lambda x, y: (x + y if y is not None else x, x + y if y is not None else x), + ["textbox", "state"], + ["textbox", "state"], live=True, api_name="predict") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/llm_claude/requirements.txt b/demo/llm_claude/requirements.txt new file mode 100644 index 0000000..3ceaffc --- /dev/null +++ b/demo/llm_claude/requirements.txt @@ -0,0 +1 @@ +openai>=1.0.0 \ No newline at end of file diff --git a/demo/llm_claude/run.py b/demo/llm_claude/run.py new file mode 100644 index 0000000..a18f510 --- /dev/null +++ b/demo/llm_claude/run.py @@ -0,0 +1,41 @@ +# This is a simple 20 questions-style game built on top of the Anthropic API. +# Before running this, make sure you have exported your Anthropic API key as an environment variable: +# export ANTHROPIC_API_KEY="your-anthropic-api-key" + +import anthropic # type: ignore +import gradio as gr + +client = anthropic.Anthropic() + +def predict(message, history): + keys_to_keep = ["role", "content"] + history = [{k: d[k] for k in keys_to_keep if k in d} for d in history] + history.append({"role": "user", "content": message}) + if len(history) > 20: + history.append({"role": "user", "content": "DONE"}) + output = client.messages.create( + messages=history, # type: ignore + model="claude-3-5-sonnet-20241022", + max_tokens=1000, + system="You are guessing an object that the user is thinking of. You can ask 10 yes/no questions. Keep asking questions until the user says DONE" + ) + return { + "role": "assistant", + "content": output.content[0].text, # type: ignore + "options": [{"value": "Yes"}, {"value": "No"}] + } + +placeholder = """ +

10 Questions


Think of a person, place, or thing. I'll ask you 10 yes/no questions to try and guess it. +
+""" + +demo = gr.ChatInterface( + predict, + examples=["Start!"], + chatbot=gr.Chatbot(placeholder=placeholder), + api_name="chat", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/llm_hf_transformers/requirements.txt b/demo/llm_hf_transformers/requirements.txt new file mode 100644 index 0000000..fdc264a --- /dev/null +++ b/demo/llm_hf_transformers/requirements.txt @@ -0,0 +1,2 @@ +transformers>=4.46.0 +torch>=2.3.1 diff --git a/demo/llm_hf_transformers/run.py b/demo/llm_hf_transformers/run.py new file mode 100644 index 0000000..b0ea5c3 --- /dev/null +++ b/demo/llm_hf_transformers/run.py @@ -0,0 +1,24 @@ +# type: ignore +import gradio as gr +from transformers import AutoModelForCausalLM, AutoTokenizer + +checkpoint = "HuggingFaceTB/SmolLM2-135M-Instruct" +device = "cpu" # "cuda" or "cpu" +tokenizer = AutoTokenizer.from_pretrained(checkpoint) +model = AutoModelForCausalLM.from_pretrained(checkpoint).to(device) + + +def predict(message, history): + messages = history + [{"role": "user", "content": message}] + input_text = tokenizer.apply_chat_template(messages, tokenize=False) + inputs = tokenizer.encode(input_text, return_tensors="pt").to(device) # type: ignore + outputs = model.generate(inputs, max_new_tokens=100, temperature=0.2, top_p=0.9, do_sample=True) + decoded = tokenizer.decode(outputs[0]) + response = decoded.split("<|im_start|>assistant\n")[-1].split("<|im_end|>")[0] + return response + + +demo = gr.ChatInterface(predict, api_name="chat") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/llm_hyperbolic/requirements.txt b/demo/llm_hyperbolic/requirements.txt new file mode 100644 index 0000000..aa2b704 --- /dev/null +++ b/demo/llm_hyperbolic/requirements.txt @@ -0,0 +1 @@ +openai>=1.0.0 diff --git a/demo/llm_hyperbolic/run.py b/demo/llm_hyperbolic/run.py new file mode 100644 index 0000000..d525cc4 --- /dev/null +++ b/demo/llm_hyperbolic/run.py @@ -0,0 +1,28 @@ +# This is a simple general-purpose chatbot built on top of Hyperbolic API. +# Before running this, make sure you have exported your Hyperbolic API key as an environment variable: +# export HYPERBOLIC_API_KEY="your-hyperbolic-api-key" + +import os +import gradio as gr +from openai import OpenAI + +api_key = os.getenv("HYPERBOLIC_API_KEY") + +client = OpenAI( + base_url="https://api.hyperbolic.xyz/v1/", + api_key=api_key, +) + +def predict(message, history): + history.append({"role": "user", "content": message}) + stream = client.chat.completions.create(messages=history, model="gpt-4o-mini", stream=True) + chunks = [] + for chunk in stream: + chunks.append(chunk.choices[0].delta.content or "") + yield "".join(chunks) + +demo = gr.ChatInterface(predict, api_name="chat") + +if __name__ == "__main__": + demo.launch() + diff --git a/demo/llm_langchain/requirements.txt b/demo/llm_langchain/requirements.txt new file mode 100644 index 0000000..66e69c3 --- /dev/null +++ b/demo/llm_langchain/requirements.txt @@ -0,0 +1,3 @@ +langchain +langchain-openai +pytz diff --git a/demo/llm_langchain/run.py b/demo/llm_langchain/run.py new file mode 100644 index 0000000..fce3cfd --- /dev/null +++ b/demo/llm_langchain/run.py @@ -0,0 +1,30 @@ +# This is a simple general-purpose chatbot built on top of LangChain and Gradio. +# Before running this, make sure you have exported your OpenAI API key as an environment variable: +# export OPENAI_API_KEY="your-openai-api-key" + +import gradio as gr +from langchain.messages import AIMessage, HumanMessage # type: ignore +from langchain_openai import ChatOpenAI # type: ignore + +model = ChatOpenAI(model="gpt-4o-mini") + + +def predict(message, history): + history_langchain_format = [] + for msg in history: + if msg["role"] == "user": + history_langchain_format.append(HumanMessage(content=msg["content"])) + elif msg["role"] == "assistant": + history_langchain_format.append(AIMessage(content=msg["content"])) + history_langchain_format.append(HumanMessage(content=message)) + gpt_response = model.invoke(history_langchain_format) + return gpt_response.content + + +demo = gr.ChatInterface( + predict, + api_name="chat", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/llm_llamaindex/paul_graham.txt b/demo/llm_llamaindex/paul_graham.txt new file mode 100644 index 0000000..6ebd858 --- /dev/null +++ b/demo/llm_llamaindex/paul_graham.txt @@ -0,0 +1,353 @@ + + +What I Worked On + +February 2021 + +Before college the two main things I worked on, outside of school, were writing and programming. I didn't write essays. I wrote what beginning writers were supposed to write then, and probably still are: short stories. My stories were awful. They had hardly any plot, just characters with strong feelings, which I imagined made them deep. + +The first programs I tried writing were on the IBM 1401 that our school district used for what was then called "data processing." This was in 9th grade, so I was 13 or 14. The school district's 1401 happened to be in the basement of our junior high school, and my friend Rich Draves and I got permission to use it. It was like a mini Bond villain's lair down there, with all these alien-looking machines — CPU, disk drives, printer, card reader — sitting up on a raised floor under bright fluorescent lights. + +The language we used was an early version of Fortran. You had to type programs on punch cards, then stack them in the card reader and press a button to load the program into memory and run it. The result would ordinarily be to print something on the spectacularly loud printer. + +I was puzzled by the 1401. I couldn't figure out what to do with it. And in retrospect there's not much I could have done with it. The only form of input to programs was data stored on punched cards, and I didn't have any data stored on punched cards. The only other option was to do things that didn't rely on any input, like calculate approximations of pi, but I didn't know enough math to do anything interesting of that type. So I'm not surprised I can't remember any programs I wrote, because they can't have done much. My clearest memory is of the moment I learned it was possible for programs not to terminate, when one of mine didn't. On a machine without time-sharing, this was a social as well as a technical error, as the data center manager's expression made clear. + +With microcomputers, everything changed. Now you could have a computer sitting right in front of you, on a desk, that could respond to your keystrokes as it was running instead of just churning through a stack of punch cards and then stopping. [1] + +The first of my friends to get a microcomputer built it himself. It was sold as a kit by Heathkit. I remember vividly how impressed and envious I felt watching him sitting in front of it, typing programs right into the computer. + +Computers were expensive in those days and it took me years of nagging before I convinced my father to buy one, a TRS-80, in about 1980. The gold standard then was the Apple II, but a TRS-80 was good enough. This was when I really started programming. I wrote simple games, a program to predict how high my model rockets would fly, and a word processor that my father used to write at least one book. There was only room in memory for about 2 pages of text, so he'd write 2 pages at a time and then print them out, but it was a lot better than a typewriter. + +Though I liked programming, I didn't plan to study it in college. In college I was going to study philosophy, which sounded much more powerful. It seemed, to my naive high school self, to be the study of the ultimate truths, compared to which the things studied in other fields would be mere domain knowledge. What I discovered when I got to college was that the other fields took up so much of the space of ideas that there wasn't much left for these supposed ultimate truths. All that seemed left for philosophy were edge cases that people in other fields felt could safely be ignored. + +I couldn't have put this into words when I was 18. All I knew at the time was that I kept taking philosophy courses and they kept being boring. So I decided to switch to AI. + +AI was in the air in the mid 1980s, but there were two things especially that made me want to work on it: a novel by Heinlein called The Moon is a Harsh Mistress, which featured an intelligent computer called Mike, and a PBS documentary that showed Terry Winograd using SHRDLU. I haven't tried rereading The Moon is a Harsh Mistress, so I don't know how well it has aged, but when I read it I was drawn entirely into its world. It seemed only a matter of time before we'd have Mike, and when I saw Winograd using SHRDLU, it seemed like that time would be a few years at most. All you had to do was teach SHRDLU more words. + +There weren't any classes in AI at Cornell then, not even graduate classes, so I started trying to teach myself. Which meant learning Lisp, since in those days Lisp was regarded as the language of AI. The commonly used programming languages then were pretty primitive, and programmers' ideas correspondingly so. The default language at Cornell was a Pascal-like language called PL/I, and the situation was similar elsewhere. Learning Lisp expanded my concept of a program so fast that it was years before I started to have a sense of where the new limits were. This was more like it; this was what I had expected college to do. It wasn't happening in a class, like it was supposed to, but that was ok. For the next couple years I was on a roll. I knew what I was going to do. + +For my undergraduate thesis, I reverse-engineered SHRDLU. My God did I love working on that program. It was a pleasing bit of code, but what made it even more exciting was my belief — hard to imagine now, but not unique in 1985 — that it was already climbing the lower slopes of intelligence. + +I had gotten into a program at Cornell that didn't make you choose a major. You could take whatever classes you liked, and choose whatever you liked to put on your degree. I of course chose "Artificial Intelligence." When I got the actual physical diploma, I was dismayed to find that the quotes had been included, which made them read as scare-quotes. At the time this bothered me, but now it seems amusingly accurate, for reasons I was about to discover. + +I applied to 3 grad schools: MIT and Yale, which were renowned for AI at the time, and Harvard, which I'd visited because Rich Draves went there, and was also home to Bill Woods, who'd invented the type of parser I used in my SHRDLU clone. Only Harvard accepted me, so that was where I went. + +I don't remember the moment it happened, or if there even was a specific moment, but during the first year of grad school I realized that AI, as practiced at the time, was a hoax. By which I mean the sort of AI in which a program that's told "the dog is sitting on the chair" translates this into some formal representation and adds it to the list of things it knows. + +What these programs really showed was that there's a subset of natural language that's a formal language. But a very proper subset. It was clear that there was an unbridgeable gap between what they could do and actually understanding natural language. It was not, in fact, simply a matter of teaching SHRDLU more words. That whole way of doing AI, with explicit data structures representing concepts, was not going to work. Its brokenness did, as so often happens, generate a lot of opportunities to write papers about various band-aids that could be applied to it, but it was never going to get us Mike. + +So I looked around to see what I could salvage from the wreckage of my plans, and there was Lisp. I knew from experience that Lisp was interesting for its own sake and not just for its association with AI, even though that was the main reason people cared about it at the time. So I decided to focus on Lisp. In fact, I decided to write a book about Lisp hacking. It's scary to think how little I knew about Lisp hacking when I started writing that book. But there's nothing like writing a book about something to help you learn it. The book, On Lisp, wasn't published till 1993, but I wrote much of it in grad school. + +Computer Science is an uneasy alliance between two halves, theory and systems. The theory people prove things, and the systems people build things. I wanted to build things. I had plenty of respect for theory — indeed, a sneaking suspicion that it was the more admirable of the two halves — but building things seemed so much more exciting. + +The problem with systems work, though, was that it didn't last. Any program you wrote today, no matter how good, would be obsolete in a couple decades at best. People might mention your software in footnotes, but no one would actually use it. And indeed, it would seem very feeble work. Only people with a sense of the history of the field would even realize that, in its time, it had been good. + +There were some surplus Xerox Dandelions floating around the computer lab at one point. Anyone who wanted one to play around with could have one. I was briefly tempted, but they were so slow by present standards; what was the point? No one else wanted one either, so off they went. That was what happened to systems work. + +I wanted not just to build things, but to build things that would last. + +In this dissatisfied state I went in 1988 to visit Rich Draves at CMU, where he was in grad school. One day I went to visit the Carnegie Institute, where I'd spent a lot of time as a kid. While looking at a painting there I realized something that might seem obvious, but was a big surprise to me. There, right on the wall, was something you could make that would last. Paintings didn't become obsolete. Some of the best ones were hundreds of years old. + +And moreover this was something you could make a living doing. Not as easily as you could by writing software, of course, but I thought if you were really industrious and lived really cheaply, it had to be possible to make enough to survive. And as an artist you could be truly independent. You wouldn't have a boss, or even need to get research funding. + +I had always liked looking at paintings. Could I make them? I had no idea. I'd never imagined it was even possible. I knew intellectually that people made art — that it didn't just appear spontaneously — but it was as if the people who made it were a different species. They either lived long ago or were mysterious geniuses doing strange things in profiles in Life magazine. The idea of actually being able to make art, to put that verb before that noun, seemed almost miraculous. + +That fall I started taking art classes at Harvard. Grad students could take classes in any department, and my advisor, Tom Cheatham, was very easy going. If he even knew about the strange classes I was taking, he never said anything. + +So now I was in a PhD program in computer science, yet planning to be an artist, yet also genuinely in love with Lisp hacking and working away at On Lisp. In other words, like many a grad student, I was working energetically on multiple projects that were not my thesis. + +I didn't see a way out of this situation. I didn't want to drop out of grad school, but how else was I going to get out? I remember when my friend Robert Morris got kicked out of Cornell for writing the internet worm of 1988, I was envious that he'd found such a spectacular way to get out of grad school. + +Then one day in April 1990 a crack appeared in the wall. I ran into professor Cheatham and he asked if I was far enough along to graduate that June. I didn't have a word of my dissertation written, but in what must have been the quickest bit of thinking in my life, I decided to take a shot at writing one in the 5 weeks or so that remained before the deadline, reusing parts of On Lisp where I could, and I was able to respond, with no perceptible delay "Yes, I think so. I'll give you something to read in a few days." + +I picked applications of continuations as the topic. In retrospect I should have written about macros and embedded languages. There's a whole world there that's barely been explored. But all I wanted was to get out of grad school, and my rapidly written dissertation sufficed, just barely. + +Meanwhile I was applying to art schools. I applied to two: RISD in the US, and the Accademia di Belli Arti in Florence, which, because it was the oldest art school, I imagined would be good. RISD accepted me, and I never heard back from the Accademia, so off to Providence I went. + +I'd applied for the BFA program at RISD, which meant in effect that I had to go to college again. This was not as strange as it sounds, because I was only 25, and art schools are full of people of different ages. RISD counted me as a transfer sophomore and said I had to do the foundation that summer. The foundation means the classes that everyone has to take in fundamental subjects like drawing, color, and design. + +Toward the end of the summer I got a big surprise: a letter from the Accademia, which had been delayed because they'd sent it to Cambridge England instead of Cambridge Massachusetts, inviting me to take the entrance exam in Florence that fall. This was now only weeks away. My nice landlady let me leave my stuff in her attic. I had some money saved from consulting work I'd done in grad school; there was probably enough to last a year if I lived cheaply. Now all I had to do was learn Italian. + +Only stranieri (foreigners) had to take this entrance exam. In retrospect it may well have been a way of excluding them, because there were so many stranieri attracted by the idea of studying art in Florence that the Italian students would otherwise have been outnumbered. I was in decent shape at painting and drawing from the RISD foundation that summer, but I still don't know how I managed to pass the written exam. I remember that I answered the essay question by writing about Cezanne, and that I cranked up the intellectual level as high as I could to make the most of my limited vocabulary. [2] + +I'm only up to age 25 and already there are such conspicuous patterns. Here I was, yet again about to attend some august institution in the hopes of learning about some prestigious subject, and yet again about to be disappointed. The students and faculty in the painting department at the Accademia were the nicest people you could imagine, but they had long since arrived at an arrangement whereby the students wouldn't require the faculty to teach anything, and in return the faculty wouldn't require the students to learn anything. And at the same time all involved would adhere outwardly to the conventions of a 19th century atelier. We actually had one of those little stoves, fed with kindling, that you see in 19th century studio paintings, and a nude model sitting as close to it as possible without getting burned. Except hardly anyone else painted her besides me. The rest of the students spent their time chatting or occasionally trying to imitate things they'd seen in American art magazines. + +Our model turned out to live just down the street from me. She made a living from a combination of modelling and making fakes for a local antique dealer. She'd copy an obscure old painting out of a book, and then he'd take the copy and maltreat it to make it look old. [3] + +While I was a student at the Accademia I started painting still lives in my bedroom at night. These paintings were tiny, because the room was, and because I painted them on leftover scraps of canvas, which was all I could afford at the time. Painting still lives is different from painting people, because the subject, as its name suggests, can't move. People can't sit for more than about 15 minutes at a time, and when they do they don't sit very still. So the traditional m.o. for painting people is to know how to paint a generic person, which you then modify to match the specific person you're painting. Whereas a still life you can, if you want, copy pixel by pixel from what you're seeing. You don't want to stop there, of course, or you get merely photographic accuracy, and what makes a still life interesting is that it's been through a head. You want to emphasize the visual cues that tell you, for example, that the reason the color changes suddenly at a certain point is that it's the edge of an object. By subtly emphasizing such things you can make paintings that are more realistic than photographs not just in some metaphorical sense, but in the strict information-theoretic sense. [4] + +I liked painting still lives because I was curious about what I was seeing. In everyday life, we aren't consciously aware of much we're seeing. Most visual perception is handled by low-level processes that merely tell your brain "that's a water droplet" without telling you details like where the lightest and darkest points are, or "that's a bush" without telling you the shape and position of every leaf. This is a feature of brains, not a bug. In everyday life it would be distracting to notice every leaf on every bush. But when you have to paint something, you have to look more closely, and when you do there's a lot to see. You can still be noticing new things after days of trying to paint something people usually take for granted, just as you can after days of trying to write an essay about something people usually take for granted. + +This is not the only way to paint. I'm not 100% sure it's even a good way to paint. But it seemed a good enough bet to be worth trying. + +Our teacher, professor Ulivi, was a nice guy. He could see I worked hard, and gave me a good grade, which he wrote down in a sort of passport each student had. But the Accademia wasn't teaching me anything except Italian, and my money was running out, so at the end of the first year I went back to the US. + +I wanted to go back to RISD, but I was now broke and RISD was very expensive, so I decided to get a job for a year and then return to RISD the next fall. I got one at a company called Interleaf, which made software for creating documents. You mean like Microsoft Word? Exactly. That was how I learned that low end software tends to eat high end software. But Interleaf still had a few years to live yet. [5] + +Interleaf had done something pretty bold. Inspired by Emacs, they'd added a scripting language, and even made the scripting language a dialect of Lisp. Now they wanted a Lisp hacker to write things in it. This was the closest thing I've had to a normal job, and I hereby apologize to my boss and coworkers, because I was a bad employee. Their Lisp was the thinnest icing on a giant C cake, and since I didn't know C and didn't want to learn it, I never understood most of the software. Plus I was terribly irresponsible. This was back when a programming job meant showing up every day during certain working hours. That seemed unnatural to me, and on this point the rest of the world is coming around to my way of thinking, but at the time it caused a lot of friction. Toward the end of the year I spent much of my time surreptitiously working on On Lisp, which I had by this time gotten a contract to publish. + +The good part was that I got paid huge amounts of money, especially by art student standards. In Florence, after paying my part of the rent, my budget for everything else had been $7 a day. Now I was getting paid more than 4 times that every hour, even when I was just sitting in a meeting. By living cheaply I not only managed to save enough to go back to RISD, but also paid off my college loans. + +I learned some useful things at Interleaf, though they were mostly about what not to do. I learned that it's better for technology companies to be run by product people than sales people (though sales is a real skill and people who are good at it are really good at it), that it leads to bugs when code is edited by too many people, that cheap office space is no bargain if it's depressing, that planned meetings are inferior to corridor conversations, that big, bureaucratic customers are a dangerous source of money, and that there's not much overlap between conventional office hours and the optimal time for hacking, or conventional offices and the optimal place for it. + +But the most important thing I learned, and which I used in both Viaweb and Y Combinator, is that the low end eats the high end: that it's good to be the "entry level" option, even though that will be less prestigious, because if you're not, someone else will be, and will squash you against the ceiling. Which in turn means that prestige is a danger sign. + +When I left to go back to RISD the next fall, I arranged to do freelance work for the group that did projects for customers, and this was how I survived for the next several years. When I came back to visit for a project later on, someone told me about a new thing called HTML, which was, as he described it, a derivative of SGML. Markup language enthusiasts were an occupational hazard at Interleaf and I ignored him, but this HTML thing later became a big part of my life. + +In the fall of 1992 I moved back to Providence to continue at RISD. The foundation had merely been intro stuff, and the Accademia had been a (very civilized) joke. Now I was going to see what real art school was like. But alas it was more like the Accademia than not. Better organized, certainly, and a lot more expensive, but it was now becoming clear that art school did not bear the same relationship to art that medical school bore to medicine. At least not the painting department. The textile department, which my next door neighbor belonged to, seemed to be pretty rigorous. No doubt illustration and architecture were too. But painting was post-rigorous. Painting students were supposed to express themselves, which to the more worldly ones meant to try to cook up some sort of distinctive signature style. + +A signature style is the visual equivalent of what in show business is known as a "schtick": something that immediately identifies the work as yours and no one else's. For example, when you see a painting that looks like a certain kind of cartoon, you know it's by Roy Lichtenstein. So if you see a big painting of this type hanging in the apartment of a hedge fund manager, you know he paid millions of dollars for it. That's not always why artists have a signature style, but it's usually why buyers pay a lot for such work. [6] + +There were plenty of earnest students too: kids who "could draw" in high school, and now had come to what was supposed to be the best art school in the country, to learn to draw even better. They tended to be confused and demoralized by what they found at RISD, but they kept going, because painting was what they did. I was not one of the kids who could draw in high school, but at RISD I was definitely closer to their tribe than the tribe of signature style seekers. + +I learned a lot in the color class I took at RISD, but otherwise I was basically teaching myself to paint, and I could do that for free. So in 1993 I dropped out. I hung around Providence for a bit, and then my college friend Nancy Parmet did me a big favor. A rent-controlled apartment in a building her mother owned in New York was becoming vacant. Did I want it? It wasn't much more than my current place, and New York was supposed to be where the artists were. So yes, I wanted it! [7] + +Asterix comics begin by zooming in on a tiny corner of Roman Gaul that turns out not to be controlled by the Romans. You can do something similar on a map of New York City: if you zoom in on the Upper East Side, there's a tiny corner that's not rich, or at least wasn't in 1993. It's called Yorkville, and that was my new home. Now I was a New York artist — in the strictly technical sense of making paintings and living in New York. + +I was nervous about money, because I could sense that Interleaf was on the way down. Freelance Lisp hacking work was very rare, and I didn't want to have to program in another language, which in those days would have meant C++ if I was lucky. So with my unerring nose for financial opportunity, I decided to write another book on Lisp. This would be a popular book, the sort of book that could be used as a textbook. I imagined myself living frugally off the royalties and spending all my time painting. (The painting on the cover of this book, ANSI Common Lisp, is one that I painted around this time.) + +The best thing about New York for me was the presence of Idelle and Julian Weber. Idelle Weber was a painter, one of the early photorealists, and I'd taken her painting class at Harvard. I've never known a teacher more beloved by her students. Large numbers of former students kept in touch with her, including me. After I moved to New York I became her de facto studio assistant. + +She liked to paint on big, square canvases, 4 to 5 feet on a side. One day in late 1994 as I was stretching one of these monsters there was something on the radio about a famous fund manager. He wasn't that much older than me, and was super rich. The thought suddenly occurred to me: why don't I become rich? Then I'll be able to work on whatever I want. + +Meanwhile I'd been hearing more and more about this new thing called the World Wide Web. Robert Morris showed it to me when I visited him in Cambridge, where he was now in grad school at Harvard. It seemed to me that the web would be a big deal. I'd seen what graphical user interfaces had done for the popularity of microcomputers. It seemed like the web would do the same for the internet. + +If I wanted to get rich, here was the next train leaving the station. I was right about that part. What I got wrong was the idea. I decided we should start a company to put art galleries online. I can't honestly say, after reading so many Y Combinator applications, that this was the worst startup idea ever, but it was up there. Art galleries didn't want to be online, and still don't, not the fancy ones. That's not how they sell. I wrote some software to generate web sites for galleries, and Robert wrote some to resize images and set up an http server to serve the pages. Then we tried to sign up galleries. To call this a difficult sale would be an understatement. It was difficult to give away. A few galleries let us make sites for them for free, but none paid us. + +Then some online stores started to appear, and I realized that except for the order buttons they were identical to the sites we'd been generating for galleries. This impressive-sounding thing called an "internet storefront" was something we already knew how to build. + +So in the summer of 1995, after I submitted the camera-ready copy of ANSI Common Lisp to the publishers, we started trying to write software to build online stores. At first this was going to be normal desktop software, which in those days meant Windows software. That was an alarming prospect, because neither of us knew how to write Windows software or wanted to learn. We lived in the Unix world. But we decided we'd at least try writing a prototype store builder on Unix. Robert wrote a shopping cart, and I wrote a new site generator for stores — in Lisp, of course. + +We were working out of Robert's apartment in Cambridge. His roommate was away for big chunks of time, during which I got to sleep in his room. For some reason there was no bed frame or sheets, just a mattress on the floor. One morning as I was lying on this mattress I had an idea that made me sit up like a capital L. What if we ran the software on the server, and let users control it by clicking on links? Then we'd never have to write anything to run on users' computers. We could generate the sites on the same server we'd serve them from. Users wouldn't need anything more than a browser. + +This kind of software, known as a web app, is common now, but at the time it wasn't clear that it was even possible. To find out, we decided to try making a version of our store builder that you could control through the browser. A couple days later, on August 12, we had one that worked. The UI was horrible, but it proved you could build a whole store through the browser, without any client software or typing anything into the command line on the server. + +Now we felt like we were really onto something. I had visions of a whole new generation of software working this way. You wouldn't need versions, or ports, or any of that crap. At Interleaf there had been a whole group called Release Engineering that seemed to be at least as big as the group that actually wrote the software. Now you could just update the software right on the server. + +We started a new company we called Viaweb, after the fact that our software worked via the web, and we got $10,000 in seed funding from Idelle's husband Julian. In return for that and doing the initial legal work and giving us business advice, we gave him 10% of the company. Ten years later this deal became the model for Y Combinator's. We knew founders needed something like this, because we'd needed it ourselves. + +At this stage I had a negative net worth, because the thousand dollars or so I had in the bank was more than counterbalanced by what I owed the government in taxes. (Had I diligently set aside the proper proportion of the money I'd made consulting for Interleaf? No, I had not.) So although Robert had his graduate student stipend, I needed that seed funding to live on. + +We originally hoped to launch in September, but we got more ambitious about the software as we worked on it. Eventually we managed to build a WYSIWYG site builder, in the sense that as you were creating pages, they looked exactly like the static ones that would be generated later, except that instead of leading to static pages, the links all referred to closures stored in a hash table on the server. + +It helped to have studied art, because the main goal of an online store builder is to make users look legit, and the key to looking legit is high production values. If you get page layouts and fonts and colors right, you can make a guy running a store out of his bedroom look more legit than a big company. + +(If you're curious why my site looks so old-fashioned, it's because it's still made with this software. It may look clunky today, but in 1996 it was the last word in slick.) + +In September, Robert rebelled. "We've been working on this for a month," he said, "and it's still not done." This is funny in retrospect, because he would still be working on it almost 3 years later. But I decided it might be prudent to recruit more programmers, and I asked Robert who else in grad school with him was really good. He recommended Trevor Blackwell, which surprised me at first, because at that point I knew Trevor mainly for his plan to reduce everything in his life to a stack of notecards, which he carried around with him. But Rtm was right, as usual. Trevor turned out to be a frighteningly effective hacker. + +It was a lot of fun working with Robert and Trevor. They're the two most independent-minded people I know, and in completely different ways. If you could see inside Rtm's brain it would look like a colonial New England church, and if you could see inside Trevor's it would look like the worst excesses of Austrian Rococo. + +We opened for business, with 6 stores, in January 1996. It was just as well we waited a few months, because although we worried we were late, we were actually almost fatally early. There was a lot of talk in the press then about ecommerce, but not many people actually wanted online stores. [8] + +There were three main parts to the software: the editor, which people used to build sites and which I wrote, the shopping cart, which Robert wrote, and the manager, which kept track of orders and statistics, and which Trevor wrote. In its time, the editor was one of the best general-purpose site builders. I kept the code tight and didn't have to integrate with any other software except Robert's and Trevor's, so it was quite fun to work on. If all I'd had to do was work on this software, the next 3 years would have been the easiest of my life. Unfortunately I had to do a lot more, all of it stuff I was worse at than programming, and the next 3 years were instead the most stressful. + +There were a lot of startups making ecommerce software in the second half of the 90s. We were determined to be the Microsoft Word, not the Interleaf. Which meant being easy to use and inexpensive. It was lucky for us that we were poor, because that caused us to make Viaweb even more inexpensive than we realized. We charged $100 a month for a small store and $300 a month for a big one. This low price was a big attraction, and a constant thorn in the sides of competitors, but it wasn't because of some clever insight that we set the price low. We had no idea what businesses paid for things. $300 a month seemed like a lot of money to us. + +We did a lot of things right by accident like that. For example, we did what's now called "doing things that don't scale," although at the time we would have described it as "being so lame that we're driven to the most desperate measures to get users." The most common of which was building stores for them. This seemed particularly humiliating, since the whole raison d'etre of our software was that people could use it to make their own stores. But anything to get users. + +We learned a lot more about retail than we wanted to know. For example, that if you could only have a small image of a man's shirt (and all images were small then by present standards), it was better to have a closeup of the collar than a picture of the whole shirt. The reason I remember learning this was that it meant I had to rescan about 30 images of men's shirts. My first set of scans were so beautiful too. + +Though this felt wrong, it was exactly the right thing to be doing. Building stores for users taught us about retail, and about how it felt to use our software. I was initially both mystified and repelled by "business" and thought we needed a "business person" to be in charge of it, but once we started to get users, I was converted, in much the same way I was converted to fatherhood once I had kids. Whatever users wanted, I was all theirs. Maybe one day we'd have so many users that I couldn't scan their images for them, but in the meantime there was nothing more important to do. + +Another thing I didn't get at the time is that growth rate is the ultimate test of a startup. Our growth rate was fine. We had about 70 stores at the end of 1996 and about 500 at the end of 1997. I mistakenly thought the thing that mattered was the absolute number of users. And that is the thing that matters in the sense that that's how much money you're making, and if you're not making enough, you might go out of business. But in the long term the growth rate takes care of the absolute number. If we'd been a startup I was advising at Y Combinator, I would have said: Stop being so stressed out, because you're doing fine. You're growing 7x a year. Just don't hire too many more people and you'll soon be profitable, and then you'll control your own destiny. + +Alas I hired lots more people, partly because our investors wanted me to, and partly because that's what startups did during the Internet Bubble. A company with just a handful of employees would have seemed amateurish. So we didn't reach breakeven until about when Yahoo bought us in the summer of 1998. Which in turn meant we were at the mercy of investors for the entire life of the company. And since both we and our investors were noobs at startups, the result was a mess even by startup standards. + +It was a huge relief when Yahoo bought us. In principle our Viaweb stock was valuable. It was a share in a business that was profitable and growing rapidly. But it didn't feel very valuable to me; I had no idea how to value a business, but I was all too keenly aware of the near-death experiences we seemed to have every few months. Nor had I changed my grad student lifestyle significantly since we started. So when Yahoo bought us it felt like going from rags to riches. Since we were going to California, I bought a car, a yellow 1998 VW GTI. I remember thinking that its leather seats alone were by far the most luxurious thing I owned. + +The next year, from the summer of 1998 to the summer of 1999, must have been the least productive of my life. I didn't realize it at the time, but I was worn out from the effort and stress of running Viaweb. For a while after I got to California I tried to continue my usual m.o. of programming till 3 in the morning, but fatigue combined with Yahoo's prematurely aged culture and grim cube farm in Santa Clara gradually dragged me down. After a few months it felt disconcertingly like working at Interleaf. + +Yahoo had given us a lot of options when they bought us. At the time I thought Yahoo was so overvalued that they'd never be worth anything, but to my astonishment the stock went up 5x in the next year. I hung on till the first chunk of options vested, then in the summer of 1999 I left. It had been so long since I'd painted anything that I'd half forgotten why I was doing this. My brain had been entirely full of software and men's shirts for 4 years. But I had done this to get rich so I could paint, I reminded myself, and now I was rich, so I should go paint. + +When I said I was leaving, my boss at Yahoo had a long conversation with me about my plans. I told him all about the kinds of pictures I wanted to paint. At the time I was touched that he took such an interest in me. Now I realize it was because he thought I was lying. My options at that point were worth about $2 million a month. If I was leaving that kind of money on the table, it could only be to go and start some new startup, and if I did, I might take people with me. This was the height of the Internet Bubble, and Yahoo was ground zero of it. My boss was at that moment a billionaire. Leaving then to start a new startup must have seemed to him an insanely, and yet also plausibly, ambitious plan. + +But I really was quitting to paint, and I started immediately. There was no time to lose. I'd already burned 4 years getting rich. Now when I talk to founders who are leaving after selling their companies, my advice is always the same: take a vacation. That's what I should have done, just gone off somewhere and done nothing for a month or two, but the idea never occurred to me. + +So I tried to paint, but I just didn't seem to have any energy or ambition. Part of the problem was that I didn't know many people in California. I'd compounded this problem by buying a house up in the Santa Cruz Mountains, with a beautiful view but miles from anywhere. I stuck it out for a few more months, then in desperation I went back to New York, where unless you understand about rent control you'll be surprised to hear I still had my apartment, sealed up like a tomb of my old life. Idelle was in New York at least, and there were other people trying to paint there, even though I didn't know any of them. + +When I got back to New York I resumed my old life, except now I was rich. It was as weird as it sounds. I resumed all my old patterns, except now there were doors where there hadn't been. Now when I was tired of walking, all I had to do was raise my hand, and (unless it was raining) a taxi would stop to pick me up. Now when I walked past charming little restaurants I could go in and order lunch. It was exciting for a while. Painting started to go better. I experimented with a new kind of still life where I'd paint one painting in the old way, then photograph it and print it, blown up, on canvas, and then use that as the underpainting for a second still life, painted from the same objects (which hopefully hadn't rotted yet). + +Meanwhile I looked for an apartment to buy. Now I could actually choose what neighborhood to live in. Where, I asked myself and various real estate agents, is the Cambridge of New York? Aided by occasional visits to actual Cambridge, I gradually realized there wasn't one. Huh. + +Around this time, in the spring of 2000, I had an idea. It was clear from our experience with Viaweb that web apps were the future. Why not build a web app for making web apps? Why not let people edit code on our server through the browser, and then host the resulting applications for them? [9] You could run all sorts of services on the servers that these applications could use just by making an API call: making and receiving phone calls, manipulating images, taking credit card payments, etc. + +I got so excited about this idea that I couldn't think about anything else. It seemed obvious that this was the future. I didn't particularly want to start another company, but it was clear that this idea would have to be embodied as one, so I decided to move to Cambridge and start it. I hoped to lure Robert into working on it with me, but there I ran into a hitch. Robert was now a postdoc at MIT, and though he'd made a lot of money the last time I'd lured him into working on one of my schemes, it had also been a huge time sink. So while he agreed that it sounded like a plausible idea, he firmly refused to work on it. + +Hmph. Well, I'd do it myself then. I recruited Dan Giffin, who had worked for Viaweb, and two undergrads who wanted summer jobs, and we got to work trying to build what it's now clear is about twenty companies and several open source projects worth of software. The language for defining applications would of course be a dialect of Lisp. But I wasn't so naive as to assume I could spring an overt Lisp on a general audience; we'd hide the parentheses, like Dylan did. + +By then there was a name for the kind of company Viaweb was, an "application service provider," or ASP. This name didn't last long before it was replaced by "software as a service," but it was current for long enough that I named this new company after it: it was going to be called Aspra. + +I started working on the application builder, Dan worked on network infrastructure, and the two undergrads worked on the first two services (images and phone calls). But about halfway through the summer I realized I really didn't want to run a company — especially not a big one, which it was looking like this would have to be. I'd only started Viaweb because I needed the money. Now that I didn't need money anymore, why was I doing this? If this vision had to be realized as a company, then screw the vision. I'd build a subset that could be done as an open source project. + +Much to my surprise, the time I spent working on this stuff was not wasted after all. After we started Y Combinator, I would often encounter startups working on parts of this new architecture, and it was very useful to have spent so much time thinking about it and even trying to write some of it. + +The subset I would build as an open source project was the new Lisp, whose parentheses I now wouldn't even have to hide. A lot of Lisp hackers dream of building a new Lisp, partly because one of the distinctive features of the language is that it has dialects, and partly, I think, because we have in our minds a Platonic form of Lisp that all existing dialects fall short of. I certainly did. So at the end of the summer Dan and I switched to working on this new dialect of Lisp, which I called Arc, in a house I bought in Cambridge. + +The following spring, lightning struck. I was invited to give a talk at a Lisp conference, so I gave one about how we'd used Lisp at Viaweb. Afterward I put a postscript file of this talk online, on paulgraham.com, which I'd created years before using Viaweb but had never used for anything. In one day it got 30,000 page views. What on earth had happened? The referring urls showed that someone had posted it on Slashdot. [10] + +Wow, I thought, there's an audience. If I write something and put it on the web, anyone can read it. That may seem obvious now, but it was surprising then. In the print era there was a narrow channel to readers, guarded by fierce monsters known as editors. The only way to get an audience for anything you wrote was to get it published as a book, or in a newspaper or magazine. Now anyone could publish anything. + +This had been possible in principle since 1993, but not many people had realized it yet. I had been intimately involved with building the infrastructure of the web for most of that time, and a writer as well, and it had taken me 8 years to realize it. Even then it took me several years to understand the implications. It meant there would be a whole new generation of essays. [11] + +In the print era, the channel for publishing essays had been vanishingly small. Except for a few officially anointed thinkers who went to the right parties in New York, the only people allowed to publish essays were specialists writing about their specialties. There were so many essays that had never been written, because there had been no way to publish them. Now they could be, and I was going to write them. [12] + +I've worked on several different things, but to the extent there was a turning point where I figured out what to work on, it was when I started publishing essays online. From then on I knew that whatever else I did, I'd always write essays too. + +I knew that online essays would be a marginal medium at first. Socially they'd seem more like rants posted by nutjobs on their GeoCities sites than the genteel and beautifully typeset compositions published in The New Yorker. But by this point I knew enough to find that encouraging instead of discouraging. + +One of the most conspicuous patterns I've noticed in my life is how well it has worked, for me at least, to work on things that weren't prestigious. Still life has always been the least prestigious form of painting. Viaweb and Y Combinator both seemed lame when we started them. I still get the glassy eye from strangers when they ask what I'm writing, and I explain that it's an essay I'm going to publish on my web site. Even Lisp, though prestigious intellectually in something like the way Latin is, also seems about as hip. + +It's not that unprestigious types of work are good per se. But when you find yourself drawn to some kind of work despite its current lack of prestige, it's a sign both that there's something real to be discovered there, and that you have the right kind of motives. Impure motives are a big danger for the ambitious. If anything is going to lead you astray, it will be the desire to impress people. So while working on things that aren't prestigious doesn't guarantee you're on the right track, it at least guarantees you're not on the most common type of wrong one. + +Over the next several years I wrote lots of essays about all kinds of different topics. O'Reilly reprinted a collection of them as a book, called Hackers & Painters after one of the essays in it. I also worked on spam filters, and did some more painting. I used to have dinners for a group of friends every thursday night, which taught me how to cook for groups. And I bought another building in Cambridge, a former candy factory (and later, twas said, porn studio), to use as an office. + +One night in October 2003 there was a big party at my house. It was a clever idea of my friend Maria Daniels, who was one of the thursday diners. Three separate hosts would all invite their friends to one party. So for every guest, two thirds of the other guests would be people they didn't know but would probably like. One of the guests was someone I didn't know but would turn out to like a lot: a woman called Jessica Livingston. A couple days later I asked her out. + +Jessica was in charge of marketing at a Boston investment bank. This bank thought it understood startups, but over the next year, as she met friends of mine from the startup world, she was surprised how different reality was. And how colorful their stories were. So she decided to compile a book of interviews with startup founders. + +When the bank had financial problems and she had to fire half her staff, she started looking for a new job. In early 2005 she interviewed for a marketing job at a Boston VC firm. It took them weeks to make up their minds, and during this time I started telling her about all the things that needed to be fixed about venture capital. They should make a larger number of smaller investments instead of a handful of giant ones, they should be funding younger, more technical founders instead of MBAs, they should let the founders remain as CEO, and so on. + +One of my tricks for writing essays had always been to give talks. The prospect of having to stand up in front of a group of people and tell them something that won't waste their time is a great spur to the imagination. When the Harvard Computer Society, the undergrad computer club, asked me to give a talk, I decided I would tell them how to start a startup. Maybe they'd be able to avoid the worst of the mistakes we'd made. + +So I gave this talk, in the course of which I told them that the best sources of seed funding were successful startup founders, because then they'd be sources of advice too. Whereupon it seemed they were all looking expectantly at me. Horrified at the prospect of having my inbox flooded by business plans (if I'd only known), I blurted out "But not me!" and went on with the talk. But afterward it occurred to me that I should really stop procrastinating about angel investing. I'd been meaning to since Yahoo bought us, and now it was 7 years later and I still hadn't done one angel investment. + +Meanwhile I had been scheming with Robert and Trevor about projects we could work on together. I missed working with them, and it seemed like there had to be something we could collaborate on. + +As Jessica and I were walking home from dinner on March 11, at the corner of Garden and Walker streets, these three threads converged. Screw the VCs who were taking so long to make up their minds. We'd start our own investment firm and actually implement the ideas we'd been talking about. I'd fund it, and Jessica could quit her job and work for it, and we'd get Robert and Trevor as partners too. [13] + +Once again, ignorance worked in our favor. We had no idea how to be angel investors, and in Boston in 2005 there were no Ron Conways to learn from. So we just made what seemed like the obvious choices, and some of the things we did turned out to be novel. + +There are multiple components to Y Combinator, and we didn't figure them all out at once. The part we got first was to be an angel firm. In those days, those two words didn't go together. There were VC firms, which were organized companies with people whose job it was to make investments, but they only did big, million dollar investments. And there were angels, who did smaller investments, but these were individuals who were usually focused on other things and made investments on the side. And neither of them helped founders enough in the beginning. We knew how helpless founders were in some respects, because we remembered how helpless we'd been. For example, one thing Julian had done for us that seemed to us like magic was to get us set up as a company. We were fine writing fairly difficult software, but actually getting incorporated, with bylaws and stock and all that stuff, how on earth did you do that? Our plan was not only to make seed investments, but to do for startups everything Julian had done for us. + +YC was not organized as a fund. It was cheap enough to run that we funded it with our own money. That went right by 99% of readers, but professional investors are thinking "Wow, that means they got all the returns." But once again, this was not due to any particular insight on our part. We didn't know how VC firms were organized. It never occurred to us to try to raise a fund, and if it had, we wouldn't have known where to start. [14] + +The most distinctive thing about YC is the batch model: to fund a bunch of startups all at once, twice a year, and then to spend three months focusing intensively on trying to help them. That part we discovered by accident, not merely implicitly but explicitly due to our ignorance about investing. We needed to get experience as investors. What better way, we thought, than to fund a whole bunch of startups at once? We knew undergrads got temporary jobs at tech companies during the summer. Why not organize a summer program where they'd start startups instead? We wouldn't feel guilty for being in a sense fake investors, because they would in a similar sense be fake founders. So while we probably wouldn't make much money out of it, we'd at least get to practice being investors on them, and they for their part would probably have a more interesting summer than they would working at Microsoft. + +We'd use the building I owned in Cambridge as our headquarters. We'd all have dinner there once a week — on tuesdays, since I was already cooking for the thursday diners on thursdays — and after dinner we'd bring in experts on startups to give talks. + +We knew undergrads were deciding then about summer jobs, so in a matter of days we cooked up something we called the Summer Founders Program, and I posted an announcement on my site, inviting undergrads to apply. I had never imagined that writing essays would be a way to get "deal flow," as investors call it, but it turned out to be the perfect source. [15] We got 225 applications for the Summer Founders Program, and we were surprised to find that a lot of them were from people who'd already graduated, or were about to that spring. Already this SFP thing was starting to feel more serious than we'd intended. + +We invited about 20 of the 225 groups to interview in person, and from those we picked 8 to fund. They were an impressive group. That first batch included reddit, Justin Kan and Emmett Shear, who went on to found Twitch, Aaron Swartz, who had already helped write the RSS spec and would a few years later become a martyr for open access, and Sam Altman, who would later become the second president of YC. I don't think it was entirely luck that the first batch was so good. You had to be pretty bold to sign up for a weird thing like the Summer Founders Program instead of a summer job at a legit place like Microsoft or Goldman Sachs. + +The deal for startups was based on a combination of the deal we did with Julian ($10k for 10%) and what Robert said MIT grad students got for the summer ($6k). We invested $6k per founder, which in the typical two-founder case was $12k, in return for 6%. That had to be fair, because it was twice as good as the deal we ourselves had taken. Plus that first summer, which was really hot, Jessica brought the founders free air conditioners. [16] + +Fairly quickly I realized that we had stumbled upon the way to scale startup funding. Funding startups in batches was more convenient for us, because it meant we could do things for a lot of startups at once, but being part of a batch was better for the startups too. It solved one of the biggest problems faced by founders: the isolation. Now you not only had colleagues, but colleagues who understood the problems you were facing and could tell you how they were solving them. + +As YC grew, we started to notice other advantages of scale. The alumni became a tight community, dedicated to helping one another, and especially the current batch, whose shoes they remembered being in. We also noticed that the startups were becoming one another's customers. We used to refer jokingly to the "YC GDP," but as YC grows this becomes less and less of a joke. Now lots of startups get their initial set of customers almost entirely from among their batchmates. + +I had not originally intended YC to be a full-time job. I was going to do three things: hack, write essays, and work on YC. As YC grew, and I grew more excited about it, it started to take up a lot more than a third of my attention. But for the first few years I was still able to work on other things. + +In the summer of 2006, Robert and I started working on a new version of Arc. This one was reasonably fast, because it was compiled into Scheme. To test this new Arc, I wrote Hacker News in it. It was originally meant to be a news aggregator for startup founders and was called Startup News, but after a few months I got tired of reading about nothing but startups. Plus it wasn't startup founders we wanted to reach. It was future startup founders. So I changed the name to Hacker News and the topic to whatever engaged one's intellectual curiosity. + +HN was no doubt good for YC, but it was also by far the biggest source of stress for me. If all I'd had to do was select and help founders, life would have been so easy. And that implies that HN was a mistake. Surely the biggest source of stress in one's work should at least be something close to the core of the work. Whereas I was like someone who was in pain while running a marathon not from the exertion of running, but because I had a blister from an ill-fitting shoe. When I was dealing with some urgent problem during YC, there was about a 60% chance it had to do with HN, and a 40% chance it had do with everything else combined. [17] + +As well as HN, I wrote all of YC's internal software in Arc. But while I continued to work a good deal in Arc, I gradually stopped working on Arc, partly because I didn't have time to, and partly because it was a lot less attractive to mess around with the language now that we had all this infrastructure depending on it. So now my three projects were reduced to two: writing essays and working on YC. + +YC was different from other kinds of work I've done. Instead of deciding for myself what to work on, the problems came to me. Every 6 months there was a new batch of startups, and their problems, whatever they were, became our problems. It was very engaging work, because their problems were quite varied, and the good founders were very effective. If you were trying to learn the most you could about startups in the shortest possible time, you couldn't have picked a better way to do it. + +There were parts of the job I didn't like. Disputes between cofounders, figuring out when people were lying to us, fighting with people who maltreated the startups, and so on. But I worked hard even at the parts I didn't like. I was haunted by something Kevin Hale once said about companies: "No one works harder than the boss." He meant it both descriptively and prescriptively, and it was the second part that scared me. I wanted YC to be good, so if how hard I worked set the upper bound on how hard everyone else worked, I'd better work very hard. + +One day in 2010, when he was visiting California for interviews, Robert Morris did something astonishing: he offered me unsolicited advice. I can only remember him doing that once before. One day at Viaweb, when I was bent over double from a kidney stone, he suggested that it would be a good idea for him to take me to the hospital. That was what it took for Rtm to offer unsolicited advice. So I remember his exact words very clearly. "You know," he said, "you should make sure Y Combinator isn't the last cool thing you do." + +At the time I didn't understand what he meant, but gradually it dawned on me that he was saying I should quit. This seemed strange advice, because YC was doing great. But if there was one thing rarer than Rtm offering advice, it was Rtm being wrong. So this set me thinking. It was true that on my current trajectory, YC would be the last thing I did, because it was only taking up more of my attention. It had already eaten Arc, and was in the process of eating essays too. Either YC was my life's work or I'd have to leave eventually. And it wasn't, so I would. + +In the summer of 2012 my mother had a stroke, and the cause turned out to be a blood clot caused by colon cancer. The stroke destroyed her balance, and she was put in a nursing home, but she really wanted to get out of it and back to her house, and my sister and I were determined to help her do it. I used to fly up to Oregon to visit her regularly, and I had a lot of time to think on those flights. On one of them I realized I was ready to hand YC over to someone else. + +I asked Jessica if she wanted to be president, but she didn't, so we decided we'd try to recruit Sam Altman. We talked to Robert and Trevor and we agreed to make it a complete changing of the guard. Up till that point YC had been controlled by the original LLC we four had started. But we wanted YC to last for a long time, and to do that it couldn't be controlled by the founders. So if Sam said yes, we'd let him reorganize YC. Robert and I would retire, and Jessica and Trevor would become ordinary partners. + +When we asked Sam if he wanted to be president of YC, initially he said no. He wanted to start a startup to make nuclear reactors. But I kept at it, and in October 2013 he finally agreed. We decided he'd take over starting with the winter 2014 batch. For the rest of 2013 I left running YC more and more to Sam, partly so he could learn the job, and partly because I was focused on my mother, whose cancer had returned. + +She died on January 15, 2014. We knew this was coming, but it was still hard when it did. + +I kept working on YC till March, to help get that batch of startups through Demo Day, then I checked out pretty completely. (I still talk to alumni and to new startups working on things I'm interested in, but that only takes a few hours a week.) + +What should I do next? Rtm's advice hadn't included anything about that. I wanted to do something completely different, so I decided I'd paint. I wanted to see how good I could get if I really focused on it. So the day after I stopped working on YC, I started painting. I was rusty and it took a while to get back into shape, but it was at least completely engaging. [18] + +I spent most of the rest of 2014 painting. I'd never been able to work so uninterruptedly before, and I got to be better than I had been. Not good enough, but better. Then in November, right in the middle of a painting, I ran out of steam. Up till that point I'd always been curious to see how the painting I was working on would turn out, but suddenly finishing this one seemed like a chore. So I stopped working on it and cleaned my brushes and haven't painted since. So far anyway. + +I realize that sounds rather wimpy. But attention is a zero sum game. If you can choose what to work on, and you choose a project that's not the best one (or at least a good one) for you, then it's getting in the way of another project that is. And at 50 there was some opportunity cost to screwing around. + +I started writing essays again, and wrote a bunch of new ones over the next few months. I even wrote a couple that weren't about startups. Then in March 2015 I started working on Lisp again. + +The distinctive thing about Lisp is that its core is a language defined by writing an interpreter in itself. It wasn't originally intended as a programming language in the ordinary sense. It was meant to be a formal model of computation, an alternative to the Turing machine. If you want to write an interpreter for a language in itself, what's the minimum set of predefined operators you need? The Lisp that John McCarthy invented, or more accurately discovered, is an answer to that question. [19] + +McCarthy didn't realize this Lisp could even be used to program computers till his grad student Steve Russell suggested it. Russell translated McCarthy's interpreter into IBM 704 machine language, and from that point Lisp started also to be a programming language in the ordinary sense. But its origins as a model of computation gave it a power and elegance that other languages couldn't match. It was this that attracted me in college, though I didn't understand why at the time. + +McCarthy's 1960 Lisp did nothing more than interpret Lisp expressions. It was missing a lot of things you'd want in a programming language. So these had to be added, and when they were, they weren't defined using McCarthy's original axiomatic approach. That wouldn't have been feasible at the time. McCarthy tested his interpreter by hand-simulating the execution of programs. But it was already getting close to the limit of interpreters you could test that way — indeed, there was a bug in it that McCarthy had overlooked. To test a more complicated interpreter, you'd have had to run it, and computers then weren't powerful enough. + +Now they are, though. Now you could continue using McCarthy's axiomatic approach till you'd defined a complete programming language. And as long as every change you made to McCarthy's Lisp was a discoveredness-preserving transformation, you could, in principle, end up with a complete language that had this quality. Harder to do than to talk about, of course, but if it was possible in principle, why not try? So I decided to take a shot at it. It took 4 years, from March 26, 2015 to October 12, 2019. It was fortunate that I had a precisely defined goal, or it would have been hard to keep at it for so long. + +I wrote this new Lisp, called Bel, in itself in Arc. That may sound like a contradiction, but it's an indication of the sort of trickery I had to engage in to make this work. By means of an egregious collection of hacks I managed to make something close enough to an interpreter written in itself that could actually run. Not fast, but fast enough to test. + +I had to ban myself from writing essays during most of this time, or I'd never have finished. In late 2015 I spent 3 months writing essays, and when I went back to working on Bel I could barely understand the code. Not so much because it was badly written as because the problem is so convoluted. When you're working on an interpreter written in itself, it's hard to keep track of what's happening at what level, and errors can be practically encrypted by the time you get them. + +So I said no more essays till Bel was done. But I told few people about Bel while I was working on it. So for years it must have seemed that I was doing nothing, when in fact I was working harder than I'd ever worked on anything. Occasionally after wrestling for hours with some gruesome bug I'd check Twitter or HN and see someone asking "Does Paul Graham still code?" + +Working on Bel was hard but satisfying. I worked on it so intensively that at any given time I had a decent chunk of the code in my head and could write more there. I remember taking the boys to the coast on a sunny day in 2015 and figuring out how to deal with some problem involving continuations while I watched them play in the tide pools. It felt like I was doing life right. I remember that because I was slightly dismayed at how novel it felt. The good news is that I had more moments like this over the next few years. + +In the summer of 2016 we moved to England. We wanted our kids to see what it was like living in another country, and since I was a British citizen by birth, that seemed the obvious choice. We only meant to stay for a year, but we liked it so much that we still live there. So most of Bel was written in England. + +In the fall of 2019, Bel was finally finished. Like McCarthy's original Lisp, it's a spec rather than an implementation, although like McCarthy's Lisp it's a spec expressed as code. + +Now that I could write essays again, I wrote a bunch about topics I'd had stacked up. I kept writing essays through 2020, but I also started to think about other things I could work on. How should I choose what to do? Well, how had I chosen what to work on in the past? I wrote an essay for myself to answer that question, and I was surprised how long and messy the answer turned out to be. If this surprised me, who'd lived it, then I thought perhaps it would be interesting to other people, and encouraging to those with similarly messy lives. So I wrote a more detailed version for others to read, and this is the last sentence of it. + + + + + + + + + +Notes + +[1] My experience skipped a step in the evolution of computers: time-sharing machines with interactive OSes. I went straight from batch processing to microcomputers, which made microcomputers seem all the more exciting. + +[2] Italian words for abstract concepts can nearly always be predicted from their English cognates (except for occasional traps like polluzione). It's the everyday words that differ. So if you string together a lot of abstract concepts with a few simple verbs, you can make a little Italian go a long way. + +[3] I lived at Piazza San Felice 4, so my walk to the Accademia went straight down the spine of old Florence: past the Pitti, across the bridge, past Orsanmichele, between the Duomo and the Baptistery, and then up Via Ricasoli to Piazza San Marco. I saw Florence at street level in every possible condition, from empty dark winter evenings to sweltering summer days when the streets were packed with tourists. + +[4] You can of course paint people like still lives if you want to, and they're willing. That sort of portrait is arguably the apex of still life painting, though the long sitting does tend to produce pained expressions in the sitters. + +[5] Interleaf was one of many companies that had smart people and built impressive technology, and yet got crushed by Moore's Law. In the 1990s the exponential growth in the power of commodity (i.e. Intel) processors rolled up high-end, special-purpose hardware and software companies like a bulldozer. + +[6] The signature style seekers at RISD weren't specifically mercenary. In the art world, money and coolness are tightly coupled. Anything expensive comes to be seen as cool, and anything seen as cool will soon become equally expensive. + +[7] Technically the apartment wasn't rent-controlled but rent-stabilized, but this is a refinement only New Yorkers would know or care about. The point is that it was really cheap, less than half market price. + +[8] Most software you can launch as soon as it's done. But when the software is an online store builder and you're hosting the stores, if you don't have any users yet, that fact will be painfully obvious. So before we could launch publicly we had to launch privately, in the sense of recruiting an initial set of users and making sure they had decent-looking stores. + +[9] We'd had a code editor in Viaweb for users to define their own page styles. They didn't know it, but they were editing Lisp expressions underneath. But this wasn't an app editor, because the code ran when the merchants' sites were generated, not when shoppers visited them. + +[10] This was the first instance of what is now a familiar experience, and so was what happened next, when I read the comments and found they were full of angry people. How could I claim that Lisp was better than other languages? Weren't they all Turing complete? People who see the responses to essays I write sometimes tell me how sorry they feel for me, but I'm not exaggerating when I reply that it has always been like this, since the very beginning. It comes with the territory. An essay must tell readers things they don't already know, and some people dislike being told such things. + +[11] People put plenty of stuff on the internet in the 90s of course, but putting something online is not the same as publishing it online. Publishing online means you treat the online version as the (or at least a) primary version. + +[12] There is a general lesson here that our experience with Y Combinator also teaches: Customs continue to constrain you long after the restrictions that caused them have disappeared. Customary VC practice had once, like the customs about publishing essays, been based on real constraints. Startups had once been much more expensive to start, and proportionally rare. Now they could be cheap and common, but the VCs' customs still reflected the old world, just as customs about writing essays still reflected the constraints of the print era. + +Which in turn implies that people who are independent-minded (i.e. less influenced by custom) will have an advantage in fields affected by rapid change (where customs are more likely to be obsolete). + +Here's an interesting point, though: you can't always predict which fields will be affected by rapid change. Obviously software and venture capital will be, but who would have predicted that essay writing would be? + +[13] Y Combinator was not the original name. At first we were called Cambridge Seed. But we didn't want a regional name, in case someone copied us in Silicon Valley, so we renamed ourselves after one of the coolest tricks in the lambda calculus, the Y combinator. + +I picked orange as our color partly because it's the warmest, and partly because no VC used it. In 2005 all the VCs used staid colors like maroon, navy blue, and forest green, because they were trying to appeal to LPs, not founders. The YC logo itself is an inside joke: the Viaweb logo had been a white V on a red circle, so I made the YC logo a white Y on an orange square. + +[14] YC did become a fund for a couple years starting in 2009, because it was getting so big I could no longer afford to fund it personally. But after Heroku got bought we had enough money to go back to being self-funded. + +[15] I've never liked the term "deal flow," because it implies that the number of new startups at any given time is fixed. This is not only false, but it's the purpose of YC to falsify it, by causing startups to be founded that would not otherwise have existed. + +[16] She reports that they were all different shapes and sizes, because there was a run on air conditioners and she had to get whatever she could, but that they were all heavier than she could carry now. + +[17] Another problem with HN was a bizarre edge case that occurs when you both write essays and run a forum. When you run a forum, you're assumed to see if not every conversation, at least every conversation involving you. And when you write essays, people post highly imaginative misinterpretations of them on forums. Individually these two phenomena are tedious but bearable, but the combination is disastrous. You actually have to respond to the misinterpretations, because the assumption that you're present in the conversation means that not responding to any sufficiently upvoted misinterpretation reads as a tacit admission that it's correct. But that in turn encourages more; anyone who wants to pick a fight with you senses that now is their chance. + +[18] The worst thing about leaving YC was not working with Jessica anymore. We'd been working on YC almost the whole time we'd known each other, and we'd neither tried nor wanted to separate it from our personal lives, so leaving was like pulling up a deeply rooted tree. + +[19] One way to get more precise about the concept of invented vs discovered is to talk about space aliens. Any sufficiently advanced alien civilization would certainly know about the Pythagorean theorem, for example. I believe, though with less certainty, that they would also know about the Lisp in McCarthy's 1960 paper. + +But if so there's no reason to suppose that this is the limit of the language that might be known to them. Presumably aliens need numbers and errors and I/O too. So it seems likely there exists at least one path out of McCarthy's Lisp along which discoveredness is preserved. + + + +Thanks to Trevor Blackwell, John Collison, Patrick Collison, Daniel Gackle, Ralph Hazell, Jessica Livingston, Robert Morris, and Harj Taggar for reading drafts of this. diff --git a/demo/llm_llamaindex/requirements.txt b/demo/llm_llamaindex/requirements.txt new file mode 100644 index 0000000..7f2a421 --- /dev/null +++ b/demo/llm_llamaindex/requirements.txt @@ -0,0 +1,2 @@ +openai +llama-index diff --git a/demo/llm_llamaindex/run.py b/demo/llm_llamaindex/run.py new file mode 100644 index 0000000..547c4d7 --- /dev/null +++ b/demo/llm_llamaindex/run.py @@ -0,0 +1,31 @@ +# This is a simple RAG chatbot built on top of Llama Index and Gradio. It allows you to upload any text or PDF files and ask questions about them! +# Before running this, make sure you have exported your OpenAI API key as an environment variable: +# export OPENAI_API_KEY="your-openai-api-key" + +from llama_index.core import VectorStoreIndex, SimpleDirectoryReader # type: ignore +import gradio as gr + +def answer(message, history): + files = [] + for msg in history: + if msg['role'] == "user" and isinstance(msg['content'], tuple): + files.append(msg['content'][0]) + for file in message["files"]: + files.append(file) + + documents = SimpleDirectoryReader(input_files=files).load_data() + index = VectorStoreIndex.from_documents(documents) + query_engine = index.as_query_engine() + return str(query_engine.query(message["text"])) + +demo = gr.ChatInterface( + answer, + title="Llama Index RAG Chatbot", + description="Upload any text or pdf files and ask questions about them!", + textbox=gr.MultimodalTextbox(file_types=[".pdf", ".txt"]), + multimodal=True, + api_name="chat", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/llm_minimax/requirements.txt b/demo/llm_minimax/requirements.txt new file mode 100644 index 0000000..aa2b704 --- /dev/null +++ b/demo/llm_minimax/requirements.txt @@ -0,0 +1 @@ +openai>=1.0.0 diff --git a/demo/llm_minimax/run.py b/demo/llm_minimax/run.py new file mode 100644 index 0000000..1ee9a50 --- /dev/null +++ b/demo/llm_minimax/run.py @@ -0,0 +1,27 @@ +# This is a simple general-purpose chatbot built on top of the MiniMax API. +# Before running this, make sure you have exported your MiniMax API key as an environment variable: +# export MINIMAX_API_KEY="your-minimax-api-key" + +import os +import gradio as gr +from openai import OpenAI + +api_key = os.getenv("MINIMAX_API_KEY") + +client = OpenAI( + base_url="https://api.minimax.io/v1", + api_key=api_key, +) + +def predict(message, history): + history.append({"role": "user", "content": message}) + stream = client.chat.completions.create(messages=history, model="MiniMax-M3", stream=True) + chunks = [] + for chunk in stream: + chunks.append(chunk.choices[0].delta.content or "") + yield "".join(chunks) + +demo = gr.ChatInterface(predict, api_name="chat") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/llm_openai/requirements.txt b/demo/llm_openai/requirements.txt new file mode 100644 index 0000000..3ceaffc --- /dev/null +++ b/demo/llm_openai/requirements.txt @@ -0,0 +1 @@ +openai>=1.0.0 \ No newline at end of file diff --git a/demo/llm_openai/run.py b/demo/llm_openai/run.py new file mode 100644 index 0000000..e6c4b60 --- /dev/null +++ b/demo/llm_openai/run.py @@ -0,0 +1,21 @@ +# This is a simple general-purpose chatbot built on top of OpenAI API. +# Before running this, make sure you have exported your OpenAI API key as an environment variable: +# export OPENAI_API_KEY="your-openai-api-key" + +from openai import OpenAI +import gradio as gr + +client = OpenAI() + +def predict(message, history): + history.append({"role": "user", "content": message}) + stream = client.chat.completions.create(messages=history, model="gpt-4o-mini", stream=True) + chunks = [] + for chunk in stream: + chunks.append(chunk.choices[0].delta.content or "") + yield "".join(chunks) + +demo = gr.ChatInterface(predict, api_name="chat") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/llm_sambanova/requirements.txt b/demo/llm_sambanova/requirements.txt new file mode 100644 index 0000000..aa2b704 --- /dev/null +++ b/demo/llm_sambanova/requirements.txt @@ -0,0 +1 @@ +openai>=1.0.0 diff --git a/demo/llm_sambanova/run.py b/demo/llm_sambanova/run.py new file mode 100644 index 0000000..0e466e5 --- /dev/null +++ b/demo/llm_sambanova/run.py @@ -0,0 +1,28 @@ +# This is a simple general-purpose chatbot built on top of SambaNova API. +# Before running this, make sure you have exported your SambaNova API key as an environment variable: +# export SAMBANOVA_API_KEY="your-sambanova-api-key" + +import os +import gradio as gr +from openai import OpenAI + +api_key = os.getenv("SAMBANOVA_API_KEY") + +client = OpenAI( + base_url="https://api.sambanova.ai/v1/", + api_key=api_key, +) + +def predict(message, history): + history.append({"role": "user", "content": message}) + stream = client.chat.completions.create(messages=history, model="Meta-Llama-3.1-70B-Instruct-8k", stream=True) + chunks = [] + for chunk in stream: + chunks.append(chunk.choices[0].delta.content or "") + yield "".join(chunks) + +demo = gr.ChatInterface(predict, api_name="chat") + +if __name__ == "__main__": + demo.launch() + diff --git a/demo/load_model_with_token/run.py b/demo/load_model_with_token/run.py new file mode 100644 index 0000000..9d13b57 --- /dev/null +++ b/demo/load_model_with_token/run.py @@ -0,0 +1,7 @@ +import gradio as gr + +# This demo requires a Hugging Face PRO token. +demo = gr.load("meta-llama/Meta-Llama-3-8B-Instruct", src="models", accept_token=True) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/load_openapi_spec/run.py b/demo/load_openapi_spec/run.py new file mode 100644 index 0000000..6fc37a3 --- /dev/null +++ b/demo/load_openapi_spec/run.py @@ -0,0 +1,11 @@ +import gradio as gr + +demo = gr.load_openapi( + openapi_spec="https://petstore3.swagger.io/api/v3/openapi.json", + base_url="https://petstore3.swagger.io/api/v3", + paths=["/pet.*"], + methods=["get", "post"], +) + +if __name__ == "__main__": + demo.launch(mcp_server=True) diff --git a/demo/load_space/run.py b/demo/load_space/run.py new file mode 100644 index 0000000..a2b79df --- /dev/null +++ b/demo/load_space/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +demo = gr.load("gradio/test-gr-load", src="spaces") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/login_with_huggingface/requirements.txt b/demo/login_with_huggingface/requirements.txt new file mode 100644 index 0000000..6b964cc --- /dev/null +++ b/demo/login_with_huggingface/requirements.txt @@ -0,0 +1 @@ +huggingface_hub diff --git a/demo/login_with_huggingface/run.py b/demo/login_with_huggingface/run.py new file mode 100644 index 0000000..980dd2b --- /dev/null +++ b/demo/login_with_huggingface/run.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import gradio as gr +from huggingface_hub import whoami + +def hello(profile: gr.OAuthProfile | None) -> str: + if profile is None: + return "I don't know you." + return f"Hello {profile.name}" + +def list_organizations(oauth_token: gr.OAuthToken | None) -> str: + if oauth_token is None: + return "Please deploy this on Spaces and log in to list organizations." + org_names = [org["name"] for org in whoami(oauth_token.token)["orgs"]] + return f"You belong to {', '.join(org_names)}." + +with gr.Blocks() as demo: + gr.LoginButton() + m1 = gr.Markdown() + m2 = gr.Markdown() + demo.load(hello, inputs=None, outputs=m1) + demo.load(list_organizations, inputs=None, outputs=m2) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/loginbutton_component/requirements.txt b/demo/loginbutton_component/requirements.txt new file mode 100644 index 0000000..f7359a0 --- /dev/null +++ b/demo/loginbutton_component/requirements.txt @@ -0,0 +1 @@ +gradio[oauth] \ No newline at end of file diff --git a/demo/loginbutton_component/run.py b/demo/loginbutton_component/run.py new file mode 100644 index 0000000..43a61fd --- /dev/null +++ b/demo/loginbutton_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.LoginButton() + +demo.launch() diff --git a/demo/longest_word/run.py b/demo/longest_word/run.py new file mode 100644 index 0000000..3885a9d --- /dev/null +++ b/demo/longest_word/run.py @@ -0,0 +1,16 @@ +import gradio as gr + +def longest_word(text): + words = text.split(" ") + lengths = [len(word) for word in words] + return max(lengths) + +ex = "The quick brown fox jumped over the lazy dog." + +demo = gr.Interface( + longest_word, "textbox", "label", examples=[[ex]], + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/magic_8_ball/requirements.txt b/demo/magic_8_ball/requirements.txt new file mode 100644 index 0000000..e5ff487 --- /dev/null +++ b/demo/magic_8_ball/requirements.txt @@ -0,0 +1,7 @@ +git+https://github.com/huggingface/parler-tts.git +accelerate +spaces +torch +pydub +transformers +huggingface_hub diff --git a/demo/magic_8_ball/run.py b/demo/magic_8_ball/run.py new file mode 100644 index 0000000..e0d6573 --- /dev/null +++ b/demo/magic_8_ball/run.py @@ -0,0 +1,169 @@ +import io +from threading import Thread +import random +import os + +import numpy as np +import spaces # type: ignore +import gradio as gr +import torch + +from parler_tts import ParlerTTSForConditionalGeneration # type: ignore +from pydub import AudioSegment +from transformers import AutoTokenizer, AutoFeatureExtractor, set_seed +from huggingface_hub import InferenceClient +from streamer import ParlerTTSStreamer # type: ignore +import time + + +device = ( + "cuda:0" + if torch.cuda.is_available() + else "mps" + if torch.backends.mps.is_available() + else "cpu" +) +torch_dtype = torch.float16 if device != "cpu" else torch.float32 + +repo_id = "parler-tts/parler_tts_mini_v0.1" + +jenny_repo_id = "ylacombe/parler-tts-mini-jenny-30H" + +model = ParlerTTSForConditionalGeneration.from_pretrained( + jenny_repo_id, torch_dtype=torch_dtype, low_cpu_mem_usage=True +).to(device) + +client = InferenceClient(token=os.getenv("HF_TOKEN")) + +tokenizer = AutoTokenizer.from_pretrained(repo_id) +feature_extractor = AutoFeatureExtractor.from_pretrained(repo_id) + +SAMPLE_RATE = feature_extractor.sampling_rate +SEED = 42 + + +def numpy_to_mp3(audio_array, sampling_rate): + # Normalize audio_array if it's floating-point + if np.issubdtype(audio_array.dtype, np.floating): + max_val = np.max(np.abs(audio_array)) + audio_array = (audio_array / max_val) * 32767 # Normalize to 16-bit range + audio_array = audio_array.astype(np.int16) + + # Create an audio segment from the numpy array + audio_segment = AudioSegment( + audio_array.tobytes(), + frame_rate=sampling_rate, + sample_width=audio_array.dtype.itemsize, + channels=1, + ) + + # Export the audio segment to MP3 bytes - use a high bitrate to maximise quality + mp3_io = io.BytesIO() + audio_segment.export(mp3_io, format="mp3", bitrate="320k") + + # Get the MP3 bytes + mp3_bytes = mp3_io.getvalue() + mp3_io.close() + + return mp3_bytes + + +sampling_rate = model.audio_encoder.config.sampling_rate +frame_rate = model.audio_encoder.config.frame_rate + + +def generate_response(audio): + gr.Info("Transcribing Audio", duration=5) + question = client.automatic_speech_recognition(audio).text # type: ignore + messages = [ + { + "role": "system", + "content": ( + "You are a magic 8 ball." + "Someone will present to you a situation or question and your job " + "is to answer with a cryptic addage or proverb such as " + "'curiosity killed the cat' or 'The early bird gets the worm'." + "Keep your answers short and do not include the phrase 'Magic 8 Ball' in your response. If the question does not make sense or is off-topic, say 'Foolish questions get foolish answers.'" + "For example, 'Magic 8 Ball, should I get a dog?', 'A dog is ready for you but are you ready for the dog?'" + ), + }, + { + "role": "user", + "content": f"Magic 8 Ball please answer this question - {question}", + }, + ] + + response = client.chat_completion( # type: ignore + messages, + max_tokens=64, + seed=random.randint(1, 5000), + model="mistralai/Mistral-7B-Instruct-v0.3", + ) + response = response.choices[0].message.content.replace("Magic 8 Ball", "") # type: ignore + return response, None, None + + +@spaces.GPU +def read_response(answer): + play_steps_in_s = 2.0 + play_steps = int(frame_rate * play_steps_in_s) + + description = "Jenny speaks at an average pace with a calm delivery in a very confined sounding environment with clear audio quality." + description_tokens = tokenizer(description, return_tensors="pt").to(device) + + streamer = ParlerTTSStreamer(model, device=device, play_steps=play_steps) + prompt = tokenizer(answer, return_tensors="pt").to(device) + + generation_kwargs = dict( # noqa: C408 + input_ids=description_tokens.input_ids, + prompt_input_ids=prompt.input_ids, + streamer=streamer, + do_sample=True, + temperature=1.0, + min_new_tokens=10, + ) + + set_seed(SEED) + thread = Thread(target=model.generate, kwargs=generation_kwargs) + thread.start() + start = time.time() + for new_audio in streamer: + print( + f"Sample of length: {round(new_audio.shape[0] / sampling_rate, 2)} seconds after {time.time() - start} seconds" + ) + yield answer, numpy_to_mp3(new_audio, sampling_rate=sampling_rate) + + +with gr.Blocks() as demo: + gr.HTML( + """ +

Magic 8 Ball 🎱

+

Ask a question and receive wisdom

+

Powered by Parler-TTS + """ + ) + with gr.Group(): + with gr.Row(): + audio_out = gr.Audio( + label="Spoken Answer", streaming=True, autoplay=True, loop=False + ) + answer = gr.Textbox(label="Answer") + state = gr.State() + with gr.Row(): + gr.Markdown( + "Example questions: 'Should I get a dog?', 'What is the meaning of life?'" + ) + audio_in = gr.Audio( + label="Speak you question", sources="microphone", type="filepath" + ) + with gr.Row(): + gr.HTML( + """

Examples: 'What is the meaning of life?', 'Should I get a dog?'

""" + ) + audio_in.stop_recording( + generate_response, audio_in, [state, answer, audio_out] + ).then(fn=read_response, inputs=state, outputs=[answer, audio_out]) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/magic_8_ball/streamer.py b/demo/magic_8_ball/streamer.py new file mode 100644 index 0000000..e74af33 --- /dev/null +++ b/demo/magic_8_ball/streamer.py @@ -0,0 +1,146 @@ +from queue import Queue +from transformers.generation.streamers import BaseStreamer +from typing import Optional +from parler_tts import ParlerTTSForConditionalGeneration # type: ignore +import numpy as np +import math +import torch + + +class ParlerTTSStreamer(BaseStreamer): + def __init__( + self, + model: ParlerTTSForConditionalGeneration, + device: Optional[str] = None, + play_steps: Optional[int] = 10, + stride: Optional[int] = None, + timeout: Optional[float] = None, + ): + """ + Streamer that stores playback-ready audio in a queue, to be used by a downstream application as an iterator. This is + useful for applications that benefit from accessing the generated audio in a non-blocking way (e.g. in an interactive + Gradio demo). + Parameters: + model (`ParlerTTSForConditionalGeneration`): + The Parler-TTS model used to generate the audio waveform. + device (`str`, *optional*): + The torch device on which to run the computation. If `None`, will default to the device of the model. + play_steps (`int`, *optional*, defaults to 10): + The number of generation steps with which to return the generated audio array. Using fewer steps will + mean the first chunk is ready faster, but will require more codec decoding steps overall. This value + should be tuned to your device and latency requirements. + stride (`int`, *optional*): + The window (stride) between adjacent audio samples. Using a stride between adjacent audio samples reduces + the hard boundary between them, giving smoother playback. If `None`, will default to a value equivalent to + play_steps // 6 in the audio space. + timeout (`int`, *optional*): + The timeout for the audio queue. If `None`, the queue will block indefinitely. Useful to handle exceptions + in `.generate()`, when it is called in a separate thread. + """ + self.decoder = model.decoder + self.audio_encoder = model.audio_encoder + self.generation_config = model.generation_config + self.device = device if device is not None else model.device + + # variables used in the streaming process + self.play_steps = play_steps + if stride is not None: + self.stride = stride + else: + hop_length = math.floor( + self.audio_encoder.config.sampling_rate + / self.audio_encoder.config.frame_rate + ) + self.stride = hop_length * (play_steps - self.decoder.num_codebooks) // 6 + self.token_cache = None + self.to_yield = 0 + + # varibles used in the thread process + self.audio_queue = Queue() + self.stop_signal = None + self.timeout = timeout + + def apply_delay_pattern_mask(self, input_ids): + # build the delay pattern mask for offsetting each codebook prediction by 1 (this behaviour is specific to Parler) + _, delay_pattern_mask = self.decoder.build_delay_pattern_mask( + input_ids[:, :1], + bos_token_id=self.generation_config.bos_token_id, + pad_token_id=self.generation_config.decoder_start_token_id, + max_length=input_ids.shape[-1], + ) + # apply the pattern mask to the input ids + input_ids = self.decoder.apply_delay_pattern_mask(input_ids, delay_pattern_mask) + + # revert the pattern delay mask by filtering the pad token id + mask = (delay_pattern_mask != self.generation_config.bos_token_id) & ( + delay_pattern_mask != self.generation_config.pad_token_id + ) + input_ids = input_ids[mask].reshape(1, self.decoder.num_codebooks, -1) + # append the frame dimension back to the audio codes + input_ids = input_ids[None, ...] + + # send the input_ids to the correct device + input_ids = input_ids.to(self.audio_encoder.device) + + decode_sequentially = ( + self.generation_config.bos_token_id in input_ids + or self.generation_config.pad_token_id in input_ids + or self.generation_config.eos_token_id in input_ids + ) + if not decode_sequentially: + output_values = self.audio_encoder.decode( + input_ids, + audio_scales=[None], + ) + else: + sample = input_ids[:, 0] + sample_mask = (sample >= self.audio_encoder.config.codebook_size).sum( + dim=(0, 1) + ) == 0 + sample = sample[:, :, sample_mask] + output_values = self.audio_encoder.decode(sample[None, ...], [None]) + + audio_values = output_values.audio_values[0, 0] + return audio_values.cpu().float().numpy() + + def put(self, value): + batch_size = value.shape[0] // self.decoder.num_codebooks + if batch_size > 1: + raise ValueError("ParlerTTSStreamer only supports batch size 1") + + if self.token_cache is None: + self.token_cache = value + else: + self.token_cache = torch.concatenate( + [self.token_cache, value[:, None]], dim=-1 + ) + + if self.token_cache.shape[-1] % self.play_steps == 0: # type: ignore + audio_values = self.apply_delay_pattern_mask(self.token_cache) + self.on_finalized_audio(audio_values[self.to_yield : -self.stride]) + self.to_yield += len(audio_values) - self.to_yield - self.stride + + def end(self): + """Flushes any remaining cache and appends the stop symbol.""" + if self.token_cache is not None: + audio_values = self.apply_delay_pattern_mask(self.token_cache) + else: + audio_values = np.zeros(self.to_yield) + + self.on_finalized_audio(audio_values[self.to_yield :], stream_end=True) + + def on_finalized_audio(self, audio: np.ndarray, stream_end: bool = False): + """Put the new audio in the queue. If the stream is ending, also put a stop signal in the queue.""" + self.audio_queue.put(audio, timeout=self.timeout) + if stream_end: + self.audio_queue.put(self.stop_signal, timeout=self.timeout) + + def __iter__(self): + return self + + def __next__(self): + value = self.audio_queue.get(timeout=self.timeout) + if not isinstance(value, np.ndarray) and value == self.stop_signal: + raise StopIteration() + else: + return value diff --git a/demo/main_note/requirements.txt b/demo/main_note/requirements.txt new file mode 100644 index 0000000..ca60345 --- /dev/null +++ b/demo/main_note/requirements.txt @@ -0,0 +1,3 @@ +scipy +numpy +matplotlib \ No newline at end of file diff --git a/demo/main_note/run.py b/demo/main_note/run.py new file mode 100644 index 0000000..ebea583 --- /dev/null +++ b/demo/main_note/run.py @@ -0,0 +1,52 @@ +from math import log2, pow + +import numpy as np +from scipy.fftpack import fft # ty: ignore[unresolved-import] + +import gradio as gr +from gradio.media import get_audio + +A4 = 440 +C0 = A4 * pow(2, -4.75) +name = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] + +def get_pitch(freq): + h = round(12 * log2(freq / C0)) + n = h % 12 + return name[n] + +def main_note(audio): + rate, y = audio + if len(y.shape) == 2: + y = y.T[0] + N = len(y) + T = 1.0 / rate + yf = fft(y) + yf2 = 2.0 / N * np.abs(yf[0 : N // 2]) + xf = np.linspace(0.0, 1.0 / (2.0 * T), N // 2) + + volume_per_pitch = {} + total_volume = np.sum(yf2) + for freq, volume in zip(xf, yf2): + if freq == 0: + continue + pitch = get_pitch(freq) + if pitch not in volume_per_pitch: + volume_per_pitch[pitch] = 0 + volume_per_pitch[pitch] += 1.0 * volume / total_volume + volume_per_pitch = {k: float(v) for k, v in volume_per_pitch.items()} + return volume_per_pitch + +demo = gr.Interface( + main_note, + gr.Audio(sources=["microphone"]), + gr.Label(num_top_classes=4), + examples=[ + [get_audio("recording1.wav")], + [get_audio("cantina.wav")], + ], + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/main_note/screenshot.png b/demo/main_note/screenshot.png new file mode 100644 index 0000000..e14d43d Binary files /dev/null and b/demo/main_note/screenshot.png differ diff --git a/demo/many_tabs/run.py b/demo/many_tabs/run.py new file mode 100644 index 0000000..6603795 --- /dev/null +++ b/demo/many_tabs/run.py @@ -0,0 +1,304 @@ +import gradio as gr +import pandas as pd +import numpy as np + +# Sample data for components +sample_df = pd.DataFrame( + { + "Name": ["Alice", "Bob", "Charlie", "Diana", "Eve"], + "Age": [25, 30, 35, 28, 32], + "City": ["New York", "London", "Paris", "Tokyo", "Berlin"], + "Score": [95.5, 87.2, 92.1, 88.9, 91.3], + } +) + + +def process_audio(audio): + if audio is None: + return "No audio uploaded" + return f"Audio file received: {audio}" + + +def process_image(image): + if image is None: + return "No image uploaded" + return "Image processed successfully!" + + +def process_3d_model(model): + if model is None: + return "No 3D model uploaded" + return f"3D model file received: {model}" + + +def process_video(video): + if video is None: + return "No video uploaded" + return f"Video file received: {video}" + + +def chat_response(message, history): + if history is None: + history = [] + response = f"You said: {message}. This is a demo response!" + history.append([message, response]) + return history, "" + + +def update_dataframe(df, action): + if action == "Add Row": + new_row = pd.DataFrame( + {"Name": ["New User"], "Age": [25], "City": ["New City"], "Score": [90.0]} + ) + return pd.concat([df, new_row], ignore_index=True) + elif action == "Clear": + return pd.DataFrame({"Name": [], "Age": [], "City": [], "Score": []}) + return df + + +with gr.Blocks(title="Multi-Component Demo with 10 Tabs") as demo: + gr.Markdown("# 🎛️ Multi-Component Gradio Demo") + gr.Markdown( + "This demo showcases various Gradio components across 10 interactive tabs." + ) + + with gr.Tabs() as main_tabs: + with gr.Tab("📝 Text", id="tab_text"): + gr.Markdown("### Text Processing") + text_input = gr.Textbox( + label="Input Text", + placeholder="Enter your text here...", + lines=3, + interactive=True, + ) + text_area = gr.TextArea( + label="Large Text Area", + placeholder="Enter longer text...", + lines=5, + interactive=True, + ) + text_output = gr.Textbox(label="Processed Text", interactive=True) + + def process_text(text1, text2): + combined = f"Text 1: {text1}\nText 2: {text2}\nTotal characters: {len(text1) + len(text2)}" + return combined + + text_input.change( + process_text, inputs=[text_input, text_area], outputs=text_output + ) + with gr.Tab("📝 Text 2", id="tab_text_2"): + gr.Markdown("### Text Processing") + text_input_2 = gr.Textbox( + label="Input Text", + placeholder="Enter your text here...", + interactive=True, + ) + text_area_2 = gr.TextArea( + label="Large Text Area", + placeholder="Enter longer text...", + interactive=True, + ) + text_output_2 = gr.Textbox(label="Processed Text", interactive=True) + + def process_text(text1, text2): + combined = f"Text 1: {text1}\nText 2: {text2}\nTotal characters: {len(text1) + len(text2)}" + return combined + + text_input_2.change( + process_text, inputs=[text_input_2, text_area_2], outputs=text_output_2 + ) + + # Tab 1: 3D Model Viewer + with gr.Tab("🎯 3D Model", id="tab_3d"): + gr.Markdown("### 3D Model Viewer") + model_input = gr.Model3D( + label="Upload 3D Model", interactive=True, height=400 + ) + model_output = gr.Textbox(label="Model Status", interactive=True) + model_input.change( + process_3d_model, inputs=model_input, outputs=model_output + ) + + # Tab 2: Image Editor + with gr.Tab("🖼️ Image Editor", id="tab_image_editor"): + gr.Markdown("### Image Editor") + image_editor = gr.ImageEditor( + label="Edit Image", + interactive=True, + height=400, + ) + editor_output = gr.Textbox(label="Editor Status", interactive=True) + image_editor.change( + lambda x: "Image edited!" if x else "No image", + inputs=image_editor, + outputs=editor_output, + ) + + # Tab 3: Audio + with gr.Tab("🎵 Audio", id="tab_audio"): + gr.Markdown("### Audio Component") + with gr.Row(): + audio_input = gr.Audio( + label="Upload Audio", interactive=True, type="filepath" + ) + audio_mic = gr.Audio( + label="Record Audio", interactive=True, sources=["microphone"] + ) + audio_output = gr.Textbox(label="Audio Status", interactive=True) + audio_input.change(process_audio, inputs=audio_input, outputs=audio_output) + + # Tab 4: Image + with gr.Tab("📸 Image", id="tab_image"): + gr.Markdown("### Image Component") + with gr.Row(): + image_input = gr.Image( + label="Upload Image", interactive=True, height=300 + ) + image_webcam = gr.Image( + label="Webcam Image", interactive=True, sources=["webcam"] + ) + image_output = gr.Textbox(label="Image Status", interactive=True) + image_input.change(process_image, inputs=image_input, outputs=image_output) + + # Tab 5: Dataframe + with gr.Tab("📊 Dataframe", id="tab_dataframe"): + gr.Markdown("### Interactive Dataframe") + df_component = gr.Dataframe( + value=sample_df, + label="Data Table", + interactive=True, + wrap=True, + ) + with gr.Row(): + add_row_btn = gr.Button("Add Row", interactive=True) + clear_btn = gr.Button("Clear Data", interactive=True) + df_status = gr.Textbox(label="Dataframe Status", interactive=True) + + add_row_btn.click( + lambda df: update_dataframe(df, "Add Row"), + inputs=df_component, + outputs=df_component, + ) + clear_btn.click( + lambda df: update_dataframe(df, "Clear"), + inputs=df_component, + outputs=df_component, + ) + + # Tab 6: Text Processing + + # Tab 7: File Upload + with gr.Tab("📁 Files", id="tab_files"): + gr.Markdown("### File Upload") + file_input = gr.File( + label="Upload Files", + interactive=True, + file_count="multiple", + file_types=["image", "video", "audio", ".pdf", ".txt"], + ) + file_output = gr.Textbox(label="File Status", interactive=True) + + def process_files(files): + if files is None or len(files) == 0: + return "No files uploaded" + file_names = [f.name for f in files] + return f"Uploaded {len(files)} files: {', '.join(file_names)}" + + file_input.change(process_files, inputs=file_input, outputs=file_output) + + # Tab 8: Chatbot + with gr.Tab("💬 Chatbot", id="tab_chatbot"): + gr.Markdown("### Interactive Chatbot") + + def echo(message, history): + return message + + gr.ChatInterface( + fn=echo, + examples=["hello", "hola", "merhaba"], + title="Echo Bot", + ) + + # Tab 9: Gallery + with gr.Tab("🖼️ Gallery", id="tab_gallery"): + gr.Markdown("### Image Gallery") + gallery = gr.Gallery( + label="Image Gallery", + columns=3, + rows=2, + height="400px", + interactive=True, + allow_preview=True, + ) + gallery_input = gr.File( + label="Add Images to Gallery", + file_count="multiple", + file_types=["image"], + interactive=True, + ) + gallery_status = gr.Textbox(label="Gallery Status", interactive=True) + + def update_gallery(files): + if files is None: + return [], "No images uploaded" + return files, f"Gallery updated with {len(files)} images" + + gallery_input.change( + update_gallery, inputs=gallery_input, outputs=[gallery, gallery_status] + ) + + # Tab 10: Video + with gr.Tab("🎬 Video", id="tab_video"): + gr.Markdown("### Video Component") + video_input = gr.Video(label="Upload Video", interactive=True, height=400) + video_webcam = gr.Video( + label="Record Video", interactive=True, sources=["webcam"] + ) + video_output = gr.Textbox(label="Video Status", interactive=True) + video_input.change(process_video, inputs=video_input, outputs=video_output) + + # Global controls + gr.Markdown("---") + with gr.Row(): + selected_tab = gr.Textbox(label="Currently Selected Tab", interactive=True) + tab_counter = gr.Number( + label="Tab Number (1-10)", value=1, minimum=1, maximum=10, interactive=True + ) + + # Tab selection functionality + def get_selected_tab(evt: gr.SelectData): + tab_names = [ + "3D Model", + "Image Editor", + "Audio", + "Image", + "Dataframe", + "Text", + "Files", + "Chatbot", + "Gallery", + "Video", + ] + return f"Selected: {evt.value}" + + # Add select events for all tabs + tabs = [ + main_tabs + ] # You would need to reference individual tabs for this to work properly + + gr.Markdown("### 🎯 Features:") + gr.Markdown(""" + - **Tab 1**: 3D Model viewer with file upload + - **Tab 2**: Image editor with drawing tools + - **Tab 3**: Audio upload and recording + - **Tab 4**: Image upload and webcam capture + - **Tab 5**: Interactive dataframe with CRUD operations + - **Tab 6**: Text processing with multiple input types + - **Tab 7**: Multi-file upload with various formats + - **Tab 8**: Interactive chatbot interface + - **Tab 9**: Image gallery with preview + - **Tab 10**: Video upload and recording + """) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/map_airbnb/DESCRIPTION.md b/demo/map_airbnb/DESCRIPTION.md new file mode 100644 index 0000000..e070c7d --- /dev/null +++ b/demo/map_airbnb/DESCRIPTION.md @@ -0,0 +1 @@ +Display an interactive map of AirBnB locations with Plotly. Data is hosted on HuggingFace Datasets. \ No newline at end of file diff --git a/demo/map_airbnb/requirements.txt b/demo/map_airbnb/requirements.txt new file mode 100644 index 0000000..582e17f --- /dev/null +++ b/demo/map_airbnb/requirements.txt @@ -0,0 +1,2 @@ +plotly +datasets diff --git a/demo/map_airbnb/run.py b/demo/map_airbnb/run.py new file mode 100644 index 0000000..4174486 --- /dev/null +++ b/demo/map_airbnb/run.py @@ -0,0 +1,56 @@ +# type: ignore +import gradio as gr +import plotly.graph_objects as go +from datasets import load_dataset + +dataset = load_dataset("gradio/NYC-Airbnb-Open-Data", split="train") +df = dataset.to_pandas() + +def filter_map(min_price, max_price, boroughs): + + filtered_df = df[(df['neighbourhood_group'].isin(boroughs)) & + (df['price'] > min_price) & (df['price'] < max_price)] + names = filtered_df["name"].tolist() + prices = filtered_df["price"].tolist() + text_list = [(names[i], prices[i]) for i in range(0, len(names))] + fig = go.Figure(go.Scattermapbox( + customdata=text_list, + lat=filtered_df['latitude'].tolist(), + lon=filtered_df['longitude'].tolist(), + mode='markers', + marker=go.scattermapbox.Marker( + size=6 + ), + hoverinfo="text", + hovertemplate='Name: %{customdata[0]}
Price: $%{customdata[1]}' + )) + + fig.update_layout( + mapbox_style="open-street-map", + hovermode='closest', + mapbox=dict( + bearing=0, + center=go.layout.mapbox.Center( + lat=40.67, + lon=-73.90 + ), + pitch=0, + zoom=9 + ), + ) + + return fig + +with gr.Blocks() as demo: + with gr.Column(): + with gr.Row(): + min_price = gr.Number(value=250, label="Minimum Price") + max_price = gr.Number(value=1000, label="Maximum Price") + boroughs = gr.CheckboxGroup(choices=["Queens", "Brooklyn", "Manhattan", "Bronx", "Staten Island"], value=["Queens", "Brooklyn"], label="Select Boroughs:") + btn = gr.Button(value="Update Filter") + map = gr.Plot() + demo.load(filter_map, [min_price, max_price, boroughs], map) + btn.click(filter_map, [min_price, max_price, boroughs], map) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/markdown_component/run.py b/demo/markdown_component/run.py new file mode 100644 index 0000000..71422fb --- /dev/null +++ b/demo/markdown_component/run.py @@ -0,0 +1,8 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown( + value="This _example_ was **written** in [Markdown](https://en.wikipedia.org/wiki/Markdown)\n Markdown" + ) + +demo.launch() diff --git a/demo/markdown_example/run.py b/demo/markdown_example/run.py new file mode 100644 index 0000000..23084b3 --- /dev/null +++ b/demo/markdown_example/run.py @@ -0,0 +1,219 @@ +import gradio as gr + +# sample md stolen from https://dillinger.io/ + +md = """# Dillinger +## _The Last Markdown Editor, Ever_ + +This is some `inline code`, it is good. + +[![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master)](https://travis-ci.org/joemccann/dillinger) + +Dillinger is a cloud-enabled, mobile-ready, offline-storage compatible, +AngularJS-powered HTML5 Markdown editor. + +- Type some Markdown on the left +- See HTML in the right +- ✨Magic ✨ + +## Features + +- Import a HTML file and watch it magically convert to Markdown +- Drag and drop images (requires your Dropbox account be linked) +- Import and save files from GitHub, Dropbox, Google Drive and One Drive +- Drag and drop markdown and HTML files into Dillinger +- Export documents as Markdown, HTML and PDF + +Markdown is a lightweight markup language based on the formatting conventions +that people naturally use in email. +As [John Gruber] writes on the [Markdown site][df1] + +> The overriding design goal for Markdown's +> formatting syntax is to make it as readable +> as possible. The idea is that a +> Markdown-formatted document should be +> publishable as-is, as plain text, without +> looking like it's been marked up with tags +> or formatting instructions. + +This text you see here is *actually- written in Markdown! To get a feel +for Markdown's syntax, type some text into the left window and +watch the results in the right. + +## Tech + +Dillinger uses a number of open source projects to work properly: + +- [AngularJS] - HTML enhanced for web apps! +- [Ace Editor] - awesome web-based text editor +- [markdown-it] - Markdown parser done right. Fast and easy to extend. +- [Twitter Bootstrap] - great UI boilerplate for modern web apps +- [node.js] - evented I/O for the backend +- [Express] - fast node.js network app framework [@tjholowaychuk] +- [Gulp] - the streaming build system +- [Breakdance](https://breakdance.github.io/breakdance/) - HTML +to Markdown converter +- [jQuery] - duh + +And of course Dillinger itself is open source with a [public repository][dill] + on GitHub. + +## Installation + +Dillinger requires [Node.js](https://nodejs.org/) v10+ to run. + +Install the dependencies and devDependencies and start the server. + +```bash +cd dillinger +npm i +node app +``` + +For production environments... + +```bash +npm install --production +NODE_ENV=production node app +``` + +## Plugins + +Dillinger is currently extended with the following plugins. +Instructions on how to use them in your own application are linked below. + +| Plugin | README | +| ------ | ------ | +| Dropbox | [plugins/dropbox/README.md][PlDb] | +| GitHub | [plugins/github/README.md][PlGh] | +| Google Drive | [plugins/googledrive/README.md][PlGd] | +| OneDrive | [plugins/onedrive/README.md][PlOd] | +| Medium | [plugins/medium/README.md][PlMe] | +| Google Analytics | [plugins/googleanalytics/README.md][PlGa] | + +## Development + +Want to contribute? Great! + +Dillinger uses Gulp + Webpack for fast developing. +Make a change in your file and instantaneously see your updates! + +Open your favorite Terminal and run these commands. + +First Tab: + +```bash +node app +``` + +Second Tab: + +```bash +gulp watch +``` + +(optional) Third: + +```bash +karma test +``` + +#### Building for source + +For production release: + +```bash +gulp build --prod +``` + +Generating pre-built zip archives for distribution: + +```bash +gulp build dist --prod +``` + +## Docker + +Dillinger is very easy to install and deploy in a Docker container. + +By default, the Docker will expose port 8080, so change this within the +Dockerfile if necessary. When ready, simply use the Dockerfile to +build the image. + +```bash +cd dillinger +docker build -t /dillinger:${package.json.version} . +``` + +This will create the dillinger image and pull in the necessary dependencies. +Be sure to swap out `${package.json.version}` with the actual +version of Dillinger. + +Once done, run the Docker image and map the port to whatever you wish on +your host. In this example, we simply map port 8000 of the host to +port 8080 of the Docker (or whatever port was exposed in the Dockerfile): + +```bash +docker run -d -p 8000:8080 --restart=always --cap-add=SYS_ADMIN --name=dillinger /dillinger:${package.json.version} +``` + +> Note: `--capt-add=SYS-ADMIN` is required for PDF rendering. + +Verify the deployment by navigating to your server address in +your preferred browser. + +```bash +127.0.0.1:8000 +``` + +```python +import gradio as gr + +gr.Blocks() as demo: + gr.Markdown(value=md) + +demo.launch() +``` + +```js +function fancyAlert(arg) { + if(arg) { + $.facebox({div:'#foo'}) + } +} +``` + +## License + +MIT + +**Free Software, Hell Yeah!** + +[//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax) + + [dill]: + [git-repo-url]: + [john gruber]: + [df1]: + [markdown-it]: + [Ace Editor]: + [node.js]: + [Twitter Bootstrap]: + [jQuery]: + [@tjholowaychuk]: + [express]: + [AngularJS]: + [Gulp]: + + [PlDb]: + [PlGh]: + [PlGd]: + [PlOd]: + [PlMe]: + [PlGa]: + +""" +with gr.Blocks() as demo: + gr.Markdown(value=md, header_links=True, height=400) + +demo.launch() diff --git a/demo/markdown_with_mermaid/run.py b/demo/markdown_with_mermaid/run.py new file mode 100644 index 0000000..963c1d6 --- /dev/null +++ b/demo/markdown_with_mermaid/run.py @@ -0,0 +1,129 @@ +import gradio as gr + +demo = gr.Blocks() + +with demo: + gr.Markdown( + """ + # Project Documentation with Mermaid Diagrams + + This example demonstrates how to combine regular Markdown with Mermaid diagrams for better visualization. + + ## System Architecture + + Below is an overview of our system's architecture: + + ```mermaid + graph LR + User[User] --> Frontend[Web Frontend] + Frontend --> API[API Gateway] + API --> Auth[Authentication Service] + API --> DataService[Data Processing Service] + DataService --> DB[(Database)] + DataService --> ML[Machine Learning Engine] + ML --> Model[(Model Storage)] + + style User fill:#f9f,stroke:#333,stroke-width:2px + style Frontend fill:#bbf,stroke:#333,stroke-width:2px + style API fill:#bfb,stroke:#333,stroke-width:2px + style DataService fill:#fbb,stroke:#333,stroke-width:2px + ``` + + ## Data Processing Workflow + + The data goes through the following steps before reaching the end user: + + 1. Collection from various sources + 2. Cleaning and preprocessing + 3. Feature extraction + 4. Analysis and model application + 5. Results formatting + + ## User Journey + + ```mermaid + sequenceDiagram + participant U as User + participant F as Frontend + participant A as API + participant D as Database + + U->>F: Login Request + F->>A: Authenticate + A->>D: Verify Credentials + D-->>A: User Validated + A-->>F: Auth Token + F-->>U: Login Success + + U->>F: Request Data + F->>A: Get Data (with token) + A->>D: Query Data + D-->>A: Return Results + A-->>F: Formatted Data + F-->>U: Display Results + ``` + + ## Decision Process + + When handling requests, our system follows this decision tree: + + ```mermaid + flowchart TD + A[New Request] --> B{Authenticated?} + B -->|Yes| C{Request Type} + B -->|No| D[Return 401] + + C -->|Data Query| E[Process Query] + C -->|Update| F{Has Permission?} + C -->|Delete| G{Is Admin?} + + F -->|Yes| H[Apply Update] + F -->|No| I[Return 403] + + G -->|Yes| J[Execute Delete] + G -->|No| K[Return 403] + + E --> L[Return Data] + H --> M[Return Success] + J --> N[Return Success] + + style A fill:#98fb98,stroke:#333 + style D fill:#ff9999,stroke:#333 + style I fill:#ff9999,stroke:#333 + style K fill:#ff9999,stroke:#333 + style M fill:#98fb98,stroke:#333 + style N fill:#98fb98,stroke:#333 + ``` + + ## State Diagram + + Our application transitions through these states: + + ```mermaid + stateDiagram-v2 + [*] --> Initialization + Initialization --> Ready + + Ready --> Processing: New Request + Processing --> Error: Exception + Processing --> Complete: Success + + Error --> Ready: Reset + Complete --> Ready: Reset + + Ready --> Shutdown: Exit Command + Shutdown --> [*] + ``` + + ## Additional Resources + + For more information about our system, please check: + + - [API Documentation](https://example.com/api-docs) + - [User Guide](https://example.com/user-guide) + - [Admin Dashboard](https://example.com/admin) + """ + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/matrix_transpose/run.py b/demo/matrix_transpose/run.py new file mode 100644 index 0000000..2127775 --- /dev/null +++ b/demo/matrix_transpose/run.py @@ -0,0 +1,24 @@ +import numpy as np + +import gradio as gr + +def transpose(matrix): + return matrix.T + +demo = gr.Interface( + transpose, + gr.Dataframe(type="numpy", datatype="number", row_count=5, column_count=3, buttons=["fullscreen"]), + "numpy", + examples=[ + [np.zeros((30, 30)).tolist()], + [np.ones((2, 2)).tolist()], + [np.random.randint(0, 10, (3, 10)).tolist()], + [np.random.randint(0, 10, (10, 3)).tolist()], + [np.random.randint(0, 10, (10, 10)).tolist()], + ], + cache_examples=False, + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/matrix_transpose/screenshot.png b/demo/matrix_transpose/screenshot.png new file mode 100644 index 0000000..a9434e9 Binary files /dev/null and b/demo/matrix_transpose/screenshot.png differ diff --git a/demo/mcp_image_app/run.py b/demo/mcp_image_app/run.py new file mode 100644 index 0000000..a494740 --- /dev/null +++ b/demo/mcp_image_app/run.py @@ -0,0 +1,134 @@ +import gradio as gr +import tempfile +from PIL import Image +import numpy as np + + +@gr.mcp.tool( + _meta={ + "openai/outputTemplate": "ui://widget/app.html", + "openai/resultCanProduceWidget": True, + "openai/widgetAccessible": True, + } +) + +def power_law_image(input_path: str, gamma: float = 0.5) -> str: + """ + Applies a power-law (gamma) transformation to an image file and saves + the result to a temporary file. + + Args: + input_path (str): Path to the input image. + gamma (float): Power-law exponent. <1 brightens, >1 darkens. + + Returns: + str: Path to the saved temporary output image. + """ + img = Image.open(input_path).convert("RGB") + arr = np.array(img, dtype=np.float32) / 255.0 + arr = np.power(arr, gamma) + arr = np.clip(arr * 255, 0, 255).astype(np.uint8) + out_img = Image.fromarray(arr) + + tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png") + out_img.save(tmp_file.name) + tmp_file.close() + + return tmp_file.name + + +@gr.mcp.resource("ui://widget/app.html", mime_type="text/html+skybridge") +def app_html(): + visual = """ + +
+ Processed image + +
+ + """ + return visual + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + original_image = gr.Image(label="Original Image", type="filepath") + btn = gr.Button("Brighten Image") + with gr.Column(): + output_image = gr.Image(label="Output Image", type="filepath") + html = gr.Code(language="html", max_lines=20) + + btn.click(power_law_image, inputs=original_image, outputs=original_image) + btn.click(app_html, outputs=html) + +if __name__ == "__main__": + demo.launch(mcp_server=True, share=True) diff --git a/demo/mcp_letter_counter_app/run.py b/demo/mcp_letter_counter_app/run.py new file mode 100644 index 0000000..1200b9b --- /dev/null +++ b/demo/mcp_letter_counter_app/run.py @@ -0,0 +1,90 @@ +import gradio as gr + + +@gr.mcp.tool( + _meta={ + "openai/outputTemplate": "ui://widget/app.html", + "openai/resultCanProduceWidget": True, + "openai/widgetAccessible": True, + } +) +def letter_counter(word: str, letter: str) -> int: + """ + Count the number of letters in a word or phrase. + + Parameters: + word (str): The word or phrase to count the letters of. + letter (str): The letter to count the occurrences of. + """ + return word.count(letter) + + +@gr.mcp.resource("ui://widget/app.html", mime_type="text/html+skybridge") +def app_html(): + visual = """ +
+ + """ + return visual + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + word = gr.Textbox(label="Word") + letter = gr.Textbox(label="Letter") + btn = gr.Button("Count Letters") + with gr.Column(): + count = gr.Number(label="Count") + html = gr.Code(language="html", max_lines=20) + + btn.click(letter_counter, inputs=[word, letter], outputs=count) + btn.click(app_html, outputs=html) + +if __name__ == "__main__": + demo.launch(mcp_server=True, share=True) diff --git a/demo/mcp_progress/run.py b/demo/mcp_progress/run.py new file mode 100644 index 0000000..9c53b41 --- /dev/null +++ b/demo/mcp_progress/run.py @@ -0,0 +1,14 @@ +import gradio as gr +import time + +def slow_text_reverser(text: str, progress=gr.Progress()): + for i in range(len(text)): + progress(i / len(text), desc="Reversing text") + time.sleep(0.3) + return text[::-1] + + +demo = gr.Interface(slow_text_reverser, gr.Textbox("Hello, world!"), gr.Textbox(), api_name="predict") + +if __name__ == "__main__": + demo.launch(mcp_server=True) diff --git a/demo/mcp_resources_and_prompts/fastmcp.py b/demo/mcp_resources_and_prompts/fastmcp.py new file mode 100644 index 0000000..2ccd564 --- /dev/null +++ b/demo/mcp_resources_and_prompts/fastmcp.py @@ -0,0 +1,40 @@ +""" +This is the equivalent of `run.py` but implemented with FastMCP v1. +It is taken directly from the quickstart example from the MCP Python SDK: +https://github.com/modelcontextprotocol/python-sdk +""" + +from mcp.server.fastmcp import FastMCP + +# Create an MCP server +mcp = FastMCP("Demo") + + +# Add an addition tool +@mcp.tool() +def add(a: int, b: int) -> int: + """Add two numbers""" + return a + b + + +# Add a dynamic greeting resource +@mcp.resource("greeting://{name}") +def get_greeting(name: str) -> str: + """Get a personalized greeting""" + return f"Hello, {name}!" + + +# Add a prompt +@mcp.prompt() +def greet_user(name: str, style: str = "friendly") -> str: + """Generate a greeting prompt""" + styles = { + "friendly": "Please write a warm, friendly greeting", + "formal": "Please write a formal, professional greeting", + "casual": "Please write a casual, relaxed greeting", + } + + return f"{styles.get(style, styles['friendly'])} for someone named {name}." + +if __name__ == "__main__": + mcp.run() diff --git a/demo/mcp_resources_and_prompts/run.py b/demo/mcp_resources_and_prompts/run.py new file mode 100644 index 0000000..2ddc3bf --- /dev/null +++ b/demo/mcp_resources_and_prompts/run.py @@ -0,0 +1,46 @@ +""" +Adapts the FastMCP quickstart example to work with Gradio's MCP integration. +""" +import gradio as gr + + +@gr.mcp.tool() # Not needed as functions are registered as tools by default +def add(a: int, b: int) -> int: + """Add two numbers""" + return a + b + + +@gr.mcp.resource("greeting://{name}") +def get_greeting(name: str) -> str: + """Get a personalized greeting""" + return f"Hello, {name}!" + + +@gr.mcp.prompt() +def greet_user(name: str, style: str = "friendly") -> str: + """Generate a greeting prompt""" + styles = { + "friendly": "Please write a warm, friendly greeting", + "formal": "Please write a formal, professional greeting", + "casual": "Please write a casual, relaxed greeting", + } + + return f"{styles.get(style, styles['friendly'])} for someone named {name}." + + +demo = gr.TabbedInterface( + [ + gr.Interface(add, [gr.Number(value=1), gr.Number(value=2)], gr.Number()), + gr.Interface(get_greeting, gr.Textbox("Abubakar"), gr.Textbox()), + gr.Interface(greet_user, [gr.Textbox("Abubakar"), gr.Dropdown(choices=["friendly", "formal", "casual"])], gr.Textbox()), + ], + [ + "Add", + "Get Greeting", + "Greet User", + ] +) + + +if __name__ == "__main__": + demo.launch(mcp_server=True) diff --git a/demo/mcp_tool_only/run.py b/demo/mcp_tool_only/run.py new file mode 100644 index 0000000..632101a --- /dev/null +++ b/demo/mcp_tool_only/run.py @@ -0,0 +1,27 @@ +import gradio as gr + +def slice_list(lst: list, start: int, end: int) -> list: + """ + A tool that slices a list given a start and end index. + Args: + lst: The list to slice. + start: The start index. + end: The end index. + Returns: + The sliced list. + """ + return lst[start:end] + +with gr.Blocks() as demo: + gr.Markdown( + """ + This is a demo of a MCP-only tool. + This tool slices a list. + This tool is MCP-only, so it does not have a UI. + """ + ) + gr.api( + slice_list + ) + +_, url, _ = demo.launch(mcp_server=True) \ No newline at end of file diff --git a/demo/mcp_tools/cheetah.jpg b/demo/mcp_tools/cheetah.jpg new file mode 100644 index 0000000..ca8a7aa Binary files /dev/null and b/demo/mcp_tools/cheetah.jpg differ diff --git a/demo/mcp_tools/run.py b/demo/mcp_tools/run.py new file mode 100644 index 0000000..23685e8 --- /dev/null +++ b/demo/mcp_tools/run.py @@ -0,0 +1,96 @@ +import numpy as np +import gradio as gr +from pathlib import Path +import os +from PIL import Image + +def prime_factors(n: str): + """ + Compute the prime factorization of a positive integer. + + Args: + n (str): The integer to factorize. Must be greater than 1. + """ + n_int = int(n) + if n_int <= 1: + raise ValueError("Input must be an integer greater than 1.") + + factors = [] + while n_int % 2 == 0: + factors.append(2) + n_int //= 2 + + divisor = 3 + while divisor * divisor <= n_int: + while n_int % divisor == 0: + factors.append(divisor) + n_int //= divisor + divisor += 2 + + if n_int > 1: + factors.append(n_int) + + return factors + + +def generate_cheetah_image(): + """ + Generate a cheetah image. + + Returns: + The generated cheetah image. + """ + return Path(os.path.dirname(__file__)) / "cheetah.jpg" + + +def image_orientation(image: Image.Image) -> str: + """ + Returns whether image is portrait or landscape. + + Args: + image (Image.Image): The image to check. + + Returns: + str: "Portrait" if image is portrait, "Landscape" if image is landscape. + """ + return "Portrait" if image.height > image.width else "Landscape" + + +def sepia(input_img): + """ + Apply a sepia filter to the input image. + + Args: + input_img (np.array): The input image to apply the sepia filter to. + + Returns: + The sepia filtered image. + """ + 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.TabbedInterface( + [ + gr.Interface(prime_factors, gr.Textbox("1001"), gr.Textbox()), + gr.Interface(generate_cheetah_image, None, gr.Image(), api_description="Generates a cheetah image. No arguments are required."), + gr.Interface(image_orientation, gr.Image(type="pil"), gr.Textbox(), api_visibility="private"), + gr.Interface(sepia, gr.Image(), gr.Image(), api_description=False), + ], + [ + "Prime Factors", + "Cheetah Image", + "Image Orientation Checker", + "Sepia Filter", + ] +) + +if __name__ == "__main__": + demo.launch(mcp_server=True) diff --git a/demo/mini_leaderboard/assets/__init__.py b/demo/mini_leaderboard/assets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/mini_leaderboard/assets/custom_css.css b/demo/mini_leaderboard/assets/custom_css.css new file mode 100644 index 0000000..91a5ff7 --- /dev/null +++ b/demo/mini_leaderboard/assets/custom_css.css @@ -0,0 +1,87 @@ +/* Hides the final AutoEvalColumn */ +#llm-benchmark-tab-table table td:last-child, +#llm-benchmark-tab-table table th:last-child { + display: none; +} + +/* Limit the width of the first AutoEvalColumn so that names don't expand too much */ +table td:first-child, +table th:first-child { + max-width: 400px; + overflow: auto; + white-space: nowrap; +} + +/* Full width space */ +.gradio-container { + max-width: 95%!important; +} + +/* Text style and margins */ +.markdown-text { + font-size: 16px !important; +} + +#models-to-add-text { + font-size: 18px !important; +} + +#citation-button span { + font-size: 16px !important; +} + +#citation-button textarea { + font-size: 16px !important; +} + +#citation-button > label > button { + margin: 6px; + transform: scale(1.3); +} + +#search-bar-table-box > div:first-child { + background: none; + border: none; +} + +#search-bar { + padding: 0px; +} + +.tab-buttons button { + font-size: 20px; +} + +/* Filters style */ +#filter_type{ + border: 0; + padding-left: 0; + padding-top: 0; +} +#filter_type label { + display: flex; +} +#filter_type label > span{ + margin-top: var(--spacing-lg); + margin-right: 0.5em; +} +#filter_type label > .wrap{ + width: 103px; +} +#filter_type label > .wrap .wrap-inner{ + padding: 2px; +} +#filter_type label > .wrap .wrap-inner input{ + width: 1px +} +#filter-columns-type{ + border:0; + padding:0.5; +} +#filter-columns-size{ + border:0; + padding:0.5; +} +#box-filter > .form{ + border: 0 +} \ No newline at end of file diff --git a/demo/mini_leaderboard/assets/leaderboard_data.json b/demo/mini_leaderboard/assets/leaderboard_data.json new file mode 100644 index 0000000..e0d3af4 --- /dev/null +++ b/demo/mini_leaderboard/assets/leaderboard_data.json @@ -0,0 +1 @@ +{"T":{"0":"\ud83d\udd36","1":"\ud83d\udcac","2":"\ud83d\udd36","3":"\ud83d\udd36","4":"\ud83d\udd36","5":"\ud83d\udd36","6":"\ud83d\udd36","7":"\ud83d\udd36","8":"\ud83d\udcac","9":"\ud83d\udd36","10":"\ud83d\udd36","11":"\ud83d\udd36","12":"\ud83d\udd36","13":"\ud83d\udcac","14":"\ud83d\udd36","15":"\ud83d\udd36","16":"\ud83d\udd36","17":"\ud83d\udd36","18":"\ud83d\udcac","19":"\ud83d\udd36","20":"\ud83d\udd36","21":"\ud83d\udd36","22":"\ud83d\udd36","23":"\ud83d\udd36","24":"\ud83d\udd36","25":"\ud83d\udd36","26":"\ud83d\udd36","27":"\ud83d\udd36","28":"\ud83d\udd36","29":"\ud83d\udd36","30":"\ud83d\udd36","31":"\ud83d\udd36","32":"\ud83d\udd36","33":"\ud83d\udd36","34":"\ud83d\udd36","35":"\ud83d\udd36","36":"\ud83d\udd36","37":"\ud83d\udd36","38":"\ud83d\udd36","39":"\ud83d\udd36","40":"\ud83d\udd36","41":"\ud83d\udd36","42":"\ud83d\udd36","43":"\ud83d\udd36","44":"\ud83d\udd36","45":"\ud83d\udcac","46":"\ud83e\udd1d","47":"\ud83d\udd36","48":"\ud83d\udd36","49":"\ud83d\udd36","50":"\ud83d\udd36","51":"\ud83d\udcac","52":"\ud83d\udd36","53":"\ud83d\udd36","54":"\ud83e\udd1d","55":"\ud83e\udd1d","56":"\ud83d\udd36","57":"\ud83d\udd36","58":"\ud83d\udd36","59":"\ud83d\udd36","60":"\ud83d\udd36","61":"\ud83d\udd36","62":"\ud83d\udd36","63":"\ud83d\udd36","64":"\ud83d\udcac","65":"\ud83d\udd36","66":"\ud83e\udd1d","67":"\ud83d\udd36","68":"\ud83d\udd36","69":"\ud83d\udcac","70":"\ud83d\udd36","71":"\ud83d\udd36","72":"\ud83d\udcac","73":"\ud83d\udcac","74":"\ud83d\udcac","75":"\ud83d\udd36","76":"\ud83d\udd36","77":"\ud83d\udcac","78":"\ud83d\udcac","79":"\ud83d\udcac","80":"\ud83d\udcac","81":"\ud83d\udcac","82":"\ud83d\udcac","83":"\ud83d\udd36","84":"\ud83d\udd36","85":"\ud83d\udd36","86":"\ud83d\udd36","87":"\ud83d\udd36","88":"\ud83d\udcac","89":"\ud83d\udd36","90":"\ud83d\udd36","91":"\ud83d\udcac","92":"\ud83d\udd36","93":"\ud83d\udd36","94":"\ud83d\udd36","95":"\ud83d\udd36","96":"\ud83d\udd36","97":"\ud83d\udd36","98":"\ud83d\udd36","99":"\ud83d\udd36","100":"\ud83d\udd36","101":"\ud83d\udd36","102":"\ud83d\udd36","103":"\ud83d\udd36","104":"\ud83d\udd36","105":"\ud83d\udcac","106":"\ud83d\udd36","107":"\ud83d\udd36","108":"\ud83d\udd36","109":"\ud83d\udd36","110":"\ud83d\udd36","111":"\ud83d\udcac","112":"\ud83d\udd36","113":"\ud83d\udd36","114":"\ud83d\udd36","115":"\ud83d\udd36","116":"\ud83d\udd36","117":"\ud83d\udd36","118":"\ud83d\udd36","119":"\ud83d\udd36","120":"\ud83d\udd36","121":"\ud83d\udd36","122":"\ud83d\udd36","123":"\ud83d\udd36","124":"\ud83d\udcac","125":"\ud83d\udcac","126":"\ud83d\udcac","127":"\ud83d\udcac","128":"\ud83d\udd36","129":"\ud83d\udd36","130":"\ud83d\udcac","131":"\ud83d\udcac","132":"\ud83d\udd36","133":"\ud83d\udcac","134":"\ud83d\udd36","135":"\ud83d\udd36","136":"\ud83d\udd36","137":"\ud83d\udcac","138":"\ud83d\udcac","139":"\ud83d\udd36","140":"\ud83d\udd36","141":"\ud83d\udd36","142":"\ud83d\udd36","143":"\ud83d\udcac","144":"\ud83d\udd36","145":"\ud83d\udcac","146":"\ud83d\udd36","147":"\ud83d\udcac","148":"\ud83d\udd36","149":"\ud83d\udcac","150":"\ud83d\udcac","151":"\ud83d\udd36","152":"\ud83d\udd36","153":"\ud83d\udd36","154":"\ud83d\udcac","155":"\ud83d\udd36","156":"\ud83d\udcac","157":"\ud83d\udd36","158":"\ud83d\udcac","159":"\ud83d\udd36","160":"\ud83d\udd36","161":"\ud83d\udd36","162":"\ud83d\udcac","163":"\ud83d\udcac","164":"\ud83d\udd36","165":"\ud83d\udd36","166":"\ud83e\udd1d","167":"\ud83d\udd36","168":"\ud83d\udcac","169":"\ud83d\udcac","170":"\ud83d\udcac","171":"\ud83d\udcac","172":"\ud83d\udd36","173":"\ud83e\udd1d","174":"\ud83d\udd36","175":"\ud83d\udd36","176":"\ud83d\udcac","177":"\ud83e\udd1d","178":"\ud83d\udd36","179":"\ud83d\udcac","180":"\ud83d\udd36","181":"\ud83d\udd36","182":"\ud83d\udfe2","183":"\ud83d\udd36","184":"\ud83d\udd36","185":"\ud83d\udd36","186":"\ud83d\udd36","187":"\ud83d\udd36","188":"\ud83d\udcac","189":"\ud83d\udcac","190":"\ud83d\udd36","191":"\ud83d\udd36","192":"\ud83d\udd36","193":"\ud83d\udd36","194":"\ud83d\udd36","195":"\ud83e\udd1d","196":"\ud83d\udd36","197":"\ud83d\udd36","198":"\ud83d\udd36","199":"\ud83d\udcac","200":"\ud83d\udd36","201":"\ud83d\udcac","202":"\ud83d\udd36","203":"\ud83d\udcac","204":"\ud83d\udd36","205":"\ud83d\udd36","206":"\ud83d\udcac","207":"\ud83d\udd36","208":"\ud83d\udcac","209":"\ud83d\udd36","210":"\ud83d\udd36","211":"\ud83d\udfe2","212":"\ud83d\udcac","213":"\ud83d\udd36","214":"\ud83d\udd36","215":"\ud83d\udd36","216":"\ud83e\udd1d","217":"\ud83d\udcac","218":"\ud83d\udd36","219":"\ud83d\udd36","220":"\ud83d\udd36","221":"\ud83d\udd36","222":"\ud83d\udd36","223":"\ud83d\udd36","224":"\ud83e\udd1d","225":"\ud83d\udcac","226":"\ud83d\udd36","227":"\ud83d\udcac","228":"\ud83e\udd1d","229":"\ud83d\udcac","230":"\ud83d\udd36","231":"\ud83d\udd36","232":"\ud83d\udcac","233":"\ud83d\udd36","234":"\ud83d\udd36","235":"\ud83d\udd36","236":"\ud83d\udd36","237":"\ud83e\udd1d","238":"\ud83d\udd36","239":"\ud83d\udcac","240":"\ud83d\udd36","241":"\ud83d\udd36","242":"\ud83e\udd1d","243":"\ud83d\udd36","244":"\ud83e\udd1d","245":"\ud83d\udd36","246":"\ud83d\udcac","247":"\ud83d\udd36","248":"\ud83d\udd36","249":"\ud83d\udd36","250":"\ud83e\udd1d","251":"\ud83d\udd36","252":"\ud83d\udd36","253":"\ud83d\udcac","254":"\ud83d\udcac","255":"\ud83d\udcac","256":"\ud83d\udd36","257":"\ud83d\udd36","258":"\ud83d\udcac","259":"\ud83d\udd36","260":"\ud83d\udcac","261":"\ud83d\udcac","262":"\ud83d\udcac","263":"\ud83d\udd36","264":"\ud83d\udd36","265":"\ud83d\udcac","266":"\ud83d\udcac","267":"\ud83d\udcac","268":"\ud83d\udd36","269":"\ud83d\udcac","270":"\ud83e\udd1d","271":"\ud83d\udd36","272":"\ud83d\udcac","273":"\ud83d\udd36","274":"\ud83d\udcac","275":"\ud83d\udcac","276":"\ud83e\udd1d","277":"\ud83d\udd36","278":"\ud83d\udd36","279":"\ud83e\udd1d","280":"\ud83d\udd36","281":"\ud83d\udcac","282":"\ud83d\udd36","283":"\ud83d\udd36","284":"\ud83d\udd36"},"Model":{"0":"davidkim205\/Rhea-72b-v0.5<\/a> \ud83d\udcd1<\/a>","1":"MTSAIR\/MultiVerse_70B<\/a> \ud83d\udcd1<\/a>","2":"MTSAIR\/MultiVerse_70B<\/a> \ud83d\udcd1<\/a>","3":"SF-Foundation\/Ein-72B-v0.11<\/a> \ud83d\udcd1<\/a>","4":"SF-Foundation\/Ein-72B-v0.13<\/a> \ud83d\udcd1<\/a>","5":"SF-Foundation\/Ein-72B-v0.12<\/a> \ud83d\udcd1<\/a>","6":"abacusai\/Smaug-72B-v0.1<\/a> \ud83d\udcd1<\/a>","7":"ibivibiv\/alpaca-dragon-72b-v1<\/a> \ud83d\udcd1<\/a>","8":"moreh\/MoMo-72B-lora-1.8.7-DPO<\/a> \ud83d\udcd1<\/a>","9":"cloudyu\/TomGrc_FusionNet_34Bx2_MoE_v0.1_DPO_f16<\/a> \ud83d\udcd1<\/a>","10":"saltlux\/luxia-21.4b-alignment-v1.0<\/a> \ud83d\udcd1<\/a>","11":"cloudyu\/TomGrc_FusionNet_34Bx2_MoE_v0.1_full_linear_DPO<\/a> \ud83d\udcd1<\/a>","12":"zhengr\/MixTAO-7Bx2-MoE-v8.1<\/a> \ud83d\udcd1<\/a>","13":"yunconglong\/Truthful_DPO_TomGrc_FusionNet_7Bx2_MoE_13B<\/a> \ud83d\udcd1<\/a>","14":"JaeyeonKang\/CCK_Asura_v1<\/a> \ud83d\udcd1<\/a>","15":"fblgit\/UNA-SimpleSmaug-34b-v1beta<\/a> \ud83d\udcd1<\/a>","16":"TomGrc\/FusionNet_34Bx2_MoE_v0.1<\/a> \ud83d\udcd1<\/a>","17":"migtissera\/Tess-72B-v1.5b<\/a> \ud83d\udcd1<\/a>","18":"moreh\/MoMo-72B-lora-1.8.6-DPO<\/a> \ud83d\udcd1<\/a>","19":"abacusai\/Smaug-34B-v0.1<\/a> \ud83d\udcd1<\/a>","20":"cloudyu\/Truthful_DPO_TomGrc_FusionNet_34Bx2_MoE<\/a> \ud83d\udcd1<\/a>","21":"ibivibiv\/orthorus-125b-v2<\/a> \ud83d\udcd1<\/a>","22":"ConvexAI\/Luminex-34B-v0.2<\/a> \ud83d\udcd1<\/a>","23":"yunconglong\/DARE_TIES_13B<\/a> \ud83d\udcd1<\/a>","24":"yunconglong\/13B_MATH_DPO<\/a> \ud83d\udcd1<\/a>","25":"TomGrc\/FusionNet_34Bx2_MoE<\/a> \ud83d\udcd1<\/a>","26":"ConvexAI\/Luminex-34B-v0.1<\/a> \ud83d\udcd1<\/a>","27":"yunconglong\/MoE_13B_DPO<\/a> \ud83d\udcd1<\/a>","28":"JaeyeonKang\/CCK_Asura_v3.0<\/a> \ud83d\udcd1<\/a>","29":"cloudyu\/4bit_quant_TomGrc_FusionNet_34Bx2_MoE_v0.1_DPO<\/a> \ud83d\udcd1<\/a>","30":"yam-peleg\/Experiment26-7B<\/a> \ud83d\udcd1<\/a>","31":"MTSAIR\/multi_verse_model<\/a> \ud83d\udcd1<\/a>","32":"chihoonlee10\/T3Q-Mistral-Orca-Math-DPO<\/a> \ud83d\udcd1<\/a>","33":"yam-peleg\/Experiment26-7B<\/a> \ud83d\udcd1<\/a>","34":"rwitz\/experiment26-truthy-iter-0<\/a> \ud83d\udcd1<\/a>","35":"yam-peleg\/Experiment30-7B<\/a> \ud83d\udcd1<\/a>","36":"yam-peleg\/Experiment28-7B<\/a> \ud83d\udcd1<\/a>","37":"MaziyarPanahi\/Calme-7B-Instruct-v0.2<\/a> \ud83d\udcd1<\/a>","38":"rwitz\/experiment26-truthy-iter-1<\/a> \ud83d\udcd1<\/a>","39":"rwitz\/experiment26-truthy-iter-2<\/a> \ud83d\udcd1<\/a>","40":"chlee10\/T3Q-Merge-Mistral7B<\/a> \ud83d\udcd1<\/a>","41":"LeroyDyer\/Mixtral_AI_Cyber_3.m1<\/a> \ud83d\udcd1<\/a>","42":"yam-peleg\/Experiment31-7B<\/a> \ud83d\udcd1<\/a>","43":"yam-peleg\/Experiment31-7B<\/a> \ud83d\udcd1<\/a>","44":"yam-peleg\/Experiment24-7B<\/a> \ud83d\udcd1<\/a>","45":"zhengr\/MixTAO-7Bx2-MoE-Instruct-v7.0<\/a> \ud83d\udcd1<\/a>","46":"bobofrut\/ladybird-base-7B-v8<\/a> \ud83d\udcd1<\/a>","47":"yam-peleg\/Experiment29-7B<\/a> \ud83d\udcd1<\/a>","48":"yam-peleg\/Experiment30-7B<\/a> \ud83d\udcd1<\/a>","49":"CorticalStack\/pastiche-crown-clown-7b-dare-dpo<\/a> \ud83d\udcd1<\/a>","50":"MaziyarPanahi\/Calme-7B-Instruct-v0.1.1<\/a> \ud83d\udcd1<\/a>","51":"mlabonne\/UltraMerge-7B<\/a> \ud83d\udcd1<\/a>","52":"cloudyu\/Truthful_DPO_cloudyu_Mixtral_34Bx2_MoE_60B<\/a> \ud83d\udcd1<\/a>","53":"yam-peleg\/Experiment27-7B<\/a> \ud83d\udcd1<\/a>","54":"eren23\/ogno-monarch-jaskier-merge-7b-OH-PREF-DPO<\/a> \ud83d\udcd1<\/a>","55":"eren23\/ogno-monarch-jaskier-merge-7b-OH-PREF-DPO-v2<\/a> \ud83d\udcd1<\/a>","56":"cloudyu\/Yi-34Bx2-MoE-60B-DPO<\/a> \ud83d\udcd1<\/a>","57":"chihoonlee10\/T3Q-EN-DPO-Mistral-7B<\/a> \ud83d\udcd1<\/a>","58":"jefferylovely\/AiMaven-Merkaba-7b<\/a> \ud83d\udcd1<\/a>","59":"bardsai\/jaskier-7b-dpo-v5.6<\/a> \ud83d\udcd1<\/a>","60":"JaeyeonKang\/CCK_Asura_v2.1<\/a> \ud83d\udcd1<\/a>","61":"yam-peleg\/Experiment25-7B<\/a> \ud83d\udcd1<\/a>","62":"eren23\/ogno-monarch-jaskier-merge-7b-OH-PREF-DPO-v3<\/a> \ud83d\udcd1<\/a>","63":"CorticalStack\/neurotic-crown-clown-7b-tak-stack-dpo<\/a> \ud83d\udcd1<\/a>","64":"jan-hq\/stealth-v2<\/a> \ud83d\udcd1<\/a>","65":"bardsai\/jaskier-7b-dpo-v6.1<\/a> \ud83d\udcd1<\/a>","66":"eren23\/ogno-monarch-jaskier-merge-7b-OH-PREF-DPO-v4-test<\/a> \ud83d\udcd1<\/a>","67":"chihoonlee10\/T3Q-DPO-Mistral-7B<\/a> \ud83d\udcd1<\/a>","68":"Kukedlc\/Jupiter-k-7B-slerp<\/a> \ud83d\udcd1<\/a>","69":"moreh\/MoMo-72B-lora-1.8.4-DPO<\/a> \ud83d\udcd1<\/a>","70":"TomGrc\/FusionNet_7Bx2_MoE_v0.1<\/a> \ud83d\udcd1<\/a>","71":"macadeliccc\/MBX-7B-v3-DPO<\/a> \ud83d\udcd1<\/a>","72":"vicgalle\/CarbonBeagle-11B-truthy<\/a> \ud83d\udcd1<\/a>","73":"dddsaty\/FusionNet_7Bx2_MoE_Ko_DPO_Adapter_Attach<\/a> \ud83d\udcd1<\/a>","74":"vicgalle\/RoleBeagle-11B<\/a> \ud83d\udcd1<\/a>","75":"MaziyarPanahi\/Calme-7B-Instruct-v0.5<\/a> \ud83d\udcd1<\/a>","76":"FelixChao\/Capricorn-7B-DPO<\/a> \ud83d\udcd1<\/a>","77":"rwitz\/experiment26-SPIN-iter-0<\/a> \ud83d\udcd1<\/a>","78":"Kukedlc\/NeuralKrishna-7B-V2-DPO<\/a> \ud83d\udcd1<\/a>","79":"abideen\/AlphaMonarch-laser<\/a> \ud83d\udcd1<\/a>","80":"touqir\/Cyrax-7B<\/a> \ud83d\udcd1<\/a>","81":"Eric111\/UltraCatunaMayo-DPO<\/a> \ud83d\udcd1<\/a>","82":"abideen\/AlphaMonarch-daser<\/a> \ud83d\udcd1<\/a>","83":"yam-peleg\/Experiment21-7B<\/a> \ud83d\udcd1<\/a>","84":"TomGrc\/FusionNet_7Bx2_MoE_14B<\/a> \ud83d\udcd1<\/a>","85":"yam-peleg\/Experiment22-7B<\/a> \ud83d\udcd1<\/a>","86":"cloudyu\/Yi-34Bx2-MOE-200K<\/a> \ud83d\udcd1<\/a>","87":"NeverSleep\/MiquMaid-v2-2x70B-DPO<\/a> \ud83d\udcd1<\/a>","88":"abideen\/AlphaMonarch-dora<\/a> \ud83d\udcd1<\/a>","89":"AiMavenAi\/Prometheus-1.3<\/a> \ud83d\udcd1<\/a>","90":"FelixChao\/Capricorn-7B<\/a> \ud83d\udcd1<\/a>","91":"yleo\/EmertonMonarch-7B<\/a> \ud83d\udcd1<\/a>","92":"yam-peleg\/Experiment20-7B<\/a> \ud83d\udcd1<\/a>","93":"ibivibiv\/multimaster-7b-v6<\/a> \ud83d\udcd1<\/a>","94":"abacusai\/Smaug-Mixtral-v0.1<\/a> \ud83d\udcd1<\/a>","95":"daxiongshu\/Pluto_24B_DPO_63<\/a> \ud83d\udcd1<\/a>","96":"abacusai\/Smaug-Mixtral-v0.1<\/a> \ud83d\udcd1<\/a>","97":"cloudyu\/Phoenix_DPO_60B<\/a> \ud83d\udcd1<\/a>","98":"Weyaxi\/Helion-4x34B<\/a> \ud83d\udcd1<\/a>","99":"ibivibiv\/multimaster-7b-v4<\/a> \ud83d\udcd1<\/a>","100":"fblgit\/UNA-34BeagleSimpleMath-32K-v1<\/a> \ud83d\udcd1<\/a>","101":"ShinojiResearch\/Senku-70B-Full<\/a> \ud83d\udcd1<\/a>","102":"fblgit\/UNA-34Beagles-32K-v1<\/a> \ud83d\udcd1<\/a>","103":"one-man-army\/UNA-34Beagles-32K-bf16-v1<\/a> \ud83d\udcd1<\/a>","104":"FelixChao\/Scorpio-7B<\/a> \ud83d\udcd1<\/a>","105":"vicgalle\/ConfigurableBeagle-11B<\/a> \ud83d\udcd1<\/a>","106":"Weyaxi\/Cosmosis-3x34B<\/a> \ud83d\udcd1<\/a>","107":"macadeliccc\/WestLake-7B-v2-laser-truthy-dpo<\/a> \ud83d\udcd1<\/a>","108":"yam-peleg\/Experiment19-7B<\/a> \ud83d\udcd1<\/a>","109":"ShinojiResearch\/Senku-70B-Full<\/a> \ud83d\udcd1<\/a>","110":"yam-peleg\/Experiment23-7B<\/a> \ud83d\udcd1<\/a>","111":"maywell\/kiqu-70b<\/a> \ud83d\udcd1<\/a>","112":"FelixChao\/WestSeverus-7B-DPO-v2<\/a> \ud83d\udcd1<\/a>","113":"migtissera\/Tess-70B-v1.6<\/a> \ud83d\udcd1<\/a>","114":"alnrg2arg\/test3_sft_16bit<\/a> \ud83d\udcd1<\/a>","115":"FelixChao\/Faraday-7B<\/a> \ud83d\udcd1<\/a>","116":"Weyaxi\/Astralis-4x34B<\/a> \ud83d\udcd1<\/a>","117":"FelixChao\/Faraday-7B<\/a> \ud83d\udcd1<\/a>","118":"PetroGPT\/WestSeverus-7B-DPO<\/a> \ud83d\udcd1<\/a>","119":"FelixChao\/Sectumsempra-7B-DPO<\/a> \ud83d\udcd1<\/a>","120":"NeverSleep\/MiquMaid-v1-70B<\/a> \ud83d\udcd1<\/a>","121":"Weyaxi\/Bagel-Hermes-2x34B<\/a> \ud83d\udcd1<\/a>","122":"BarryFutureman\/WestLakeX-7B-EvoMerge-Variant2<\/a> \ud83d\udcd1<\/a>","123":"ibivibiv\/multimaster-7b-v5<\/a> \ud83d\udcd1<\/a>","124":"alnrg2arg\/test3_sft_16bit_dpo2<\/a> \ud83d\udcd1<\/a>","125":"ChaoticNeutrals\/Eris_Floramix_DPO_7B<\/a> \ud83d\udcd1<\/a>","126":"abhishekchohan\/SOLAR-10.7B-Instruct-Forest-DPO-v1<\/a> \ud83d\udcd1<\/a>","127":"abacusai\/MetaMath-Bagel-DPO-34B<\/a> \ud83d\udcd1<\/a>","128":"cognitivecomputations\/WestLake-7B-v2-laser<\/a> \ud83d\udcd1<\/a>","129":"cloudyu\/60B_MoE_Coder_v3<\/a> \ud83d\udcd1<\/a>","130":"ChaoticNeutrals\/Eris_Remix_DPO_7B<\/a> \ud83d\udcd1<\/a>","131":"Eric111\/CatunaLaserPi-DPO<\/a> \ud83d\udcd1<\/a>","132":"jondurbin\/nontoxic-bagel-34b-v0.2<\/a> \ud83d\udcd1<\/a>","133":"jondurbin\/bagel-dpo-34b-v0.2<\/a> \ud83d\udcd1<\/a>","134":"senseable\/WestLake-7B-v2<\/a> \ud83d\udcd1<\/a>","135":"moreh\/MoMo-72B-LoRA-V1.4<\/a> \ud83d\udcd1<\/a>","136":"MaziyarPanahi\/Calme-7B-Instruct-v0.4<\/a> \ud83d\udcd1<\/a>","137":"kevin009\/llamaRAGdrama<\/a> \ud83d\udcd1<\/a>","138":"vicgalle\/Mixtral-7Bx2-truthy<\/a> \ud83d\udcd1<\/a>","139":"moreh\/MoMo-72B-LoRA-V1.4<\/a> \ud83d\udcd1<\/a>","140":"jondurbin\/bagel-dpo-34b-v0.2<\/a> \ud83d\udcd1<\/a>","141":"senseable\/Westlake-7B<\/a> \ud83d\udcd1<\/a>","142":"rizla\/trrapi-16b<\/a> \ud83d\udcd1<\/a>","143":"abacusai\/MM-OV-bagel-DPO-34b-c1000-250<\/a> \ud83d\udcd1<\/a>","144":"BarryFutureman\/WestLakeX-7B-EvoMerge<\/a> \ud83d\udcd1<\/a>","145":"yunconglong\/Truthful_DPO_MOE_19B<\/a> \ud83d\udcd1<\/a>","146":"Kukedlc\/NeuralExperiment-7b-MagicCoder-v7.5<\/a> \ud83d\udcd1<\/a>","147":"ResplendentAI\/Datura_7B<\/a> \ud83d\udcd1<\/a>","148":"FelixChao\/Patronum-7B<\/a> \ud83d\udcd1<\/a>","149":"bhavinjawade\/SOLAR-10B-OrcaDPO-Jawade<\/a> \ud83d\udcd1<\/a>","150":"ResplendentAI\/Flora_DPO_7B<\/a> \ud83d\udcd1<\/a>","151":"ResplendentAI\/Flora_7B<\/a> \ud83d\udcd1<\/a>","152":"NeuralNovel\/Valor-7B-v0.1<\/a> \ud83d\udcd1<\/a>","153":"VAGOsolutions\/SauerkrautLM-SOLAR-Instruct<\/a> \ud83d\udcd1<\/a>","154":"upstage\/SOLAR-10.7B-Instruct-v1.0<\/a> \ud83d\udcd1<\/a>","155":"fblgit\/UNA-SOLAR-10.7B-Instruct-v1.0<\/a> \ud83d\udcd1<\/a>","156":"bhavinjawade\/SOLAR-10B-Nector-DPO-Jawade<\/a> \ud83d\udcd1<\/a>","157":"abacusai\/Liberated-Qwen1.5-72B<\/a> \ud83d\udcd1<\/a>","158":"dddsaty\/SOLAR-Instruct-ko-Adapter-Attach<\/a> \ud83d\udcd1<\/a>","159":"macadeliccc\/SOLAR-10.7b-Instruct-truthy-dpo<\/a> \ud83d\udcd1<\/a>","160":"abacusai\/Liberated-Qwen1.5-72B<\/a> \ud83d\udcd1<\/a>","161":"cloudyu\/19B_MATH_DPO<\/a> \ud83d\udcd1<\/a>","162":"dhanushreddy29\/BrokenKeyboard<\/a> \ud83d\udcd1<\/a>","163":"fblgit\/UNA-SOLAR-10.7B-Instruct-v1.0<\/a> \ud83d\udcd1<\/a>","164":"fblgit\/UNA-POLAR-10.7B-InstructMath-v2<\/a> \ud83d\udcd1<\/a>","165":"fblgit\/UNAversal-2x7B-v1<\/a> \ud83d\udcd1<\/a>","166":"dddsaty\/Merge_Sakura_Solar<\/a> \ud83d\udcd1<\/a>","167":"rishiraj\/meow<\/a> \ud83d\udcd1<\/a>","168":"vicgalle\/ConfigurableSOLAR-10.7B<\/a> \ud83d\udcd1<\/a>","169":"fblgit\/UNA-TheBeagle-7b-v1<\/a> \ud83d\udcd1<\/a>","170":"vicgalle\/OpenBeagle-11B<\/a> \ud83d\udcd1<\/a>","171":"InferenceIllusionist\/Excalibur-7b-DPO<\/a> \ud83d\udcd1<\/a>","172":"JaeyeonKang\/CCK_Gony_v3<\/a> \ud83d\udcd1<\/a>","173":"Sao10K\/Typhon-Mixtral-v1<\/a> \ud83d\udcd1<\/a>","174":"fblgit\/UNAversal-8x7B-v1beta<\/a> \ud83d\udcd1<\/a>","175":"sophosympatheia\/Aurora-Nights-70B-v1.0<\/a> \ud83d\udcd1<\/a>","176":"allenai\/tulu-2-dpo-70b<\/a> \ud83d\udcd1<\/a>","177":"Steelskull\/Lumosia-v2-MoE-4x10.7<\/a> \ud83d\udcd1<\/a>","178":"NousResearch\/Nous-Hermes-2-Yi-34B<\/a> \ud83d\udcd1<\/a>","179":"decruz07\/kellemar-DPO-Orca-Distilled-7B-SLERP<\/a> \ud83d\udcd1<\/a>","180":"abacusai\/MM-Orc-Vic-bagel-34b-c1000<\/a> \ud83d\udcd1<\/a>","181":"JaeyeonKang\/CCK_Asura_v2<\/a> \ud83d\udcd1<\/a>","182":"Qwen\/Qwen-72B<\/a> \ud83d\udcd1<\/a>","183":"yam-peleg\/Experiment7-7B<\/a> \ud83d\udcd1<\/a>","184":"macadeliccc\/SOLAR-10.7b-Instruct-dpo<\/a> \ud83d\udcd1<\/a>","185":"yam-peleg\/Experiment15-7B<\/a> \ud83d\udcd1<\/a>","186":"yam-peleg\/Experiment10-7B<\/a> \ud83d\udcd1<\/a>","187":"yam-peleg\/Experiment8-7B<\/a> \ud83d\udcd1<\/a>","188":"cloudyu\/Mixtral-8x7B-Instruct-v0.1-DPO<\/a> \ud83d\udcd1<\/a>","189":"Neuronovo\/neuronovo-9B-v0.4<\/a> \ud83d\udcd1<\/a>","190":"cloudyu\/Mixtral_7Bx5_MoE_30B<\/a> \ud83d\udcd1<\/a>","191":"yam-peleg\/Experiment9-7B<\/a> \ud83d\udcd1<\/a>","192":"yam-peleg\/Experiment1-7B<\/a> \ud83d\udcd1<\/a>","193":"yam-peleg\/Experiment2-7B<\/a> \ud83d\udcd1<\/a>","194":"yam-peleg\/Experiment4-7B<\/a> \ud83d\udcd1<\/a>","195":"Sao10K\/Franziska-Mixtral-v1<\/a> \ud83d\udcd1<\/a>","196":"NousResearch\/Nous-Hermes-2-Mixtral-8x7B-DPO<\/a> \ud83d\udcd1<\/a>","197":"ResplendentAI\/DaturaCookie_7B<\/a> \ud83d\udcd1<\/a>","198":"ibndias\/Nous-Hermes-2-MoE-2x34B<\/a> \ud83d\udcd1<\/a>","199":"gradientai\/v-alpha-tross<\/a> \ud83d\udcd1<\/a>","200":"carsenk\/flippa-exp26-v3-7b<\/a> \ud83d\udcd1<\/a>","201":"SUSTech\/SUS-Chat-34B<\/a> \ud83d\udcd1<\/a>","202":"Sao10K\/SOLAR-10.7B-NahIdWin<\/a> \ud83d\udcd1<\/a>","203":"yunconglong\/7Bx4_DPO<\/a> \ud83d\udcd1<\/a>","204":"gradientai\/v-alpha-tross<\/a> \ud83d\udcd1<\/a>","205":"ResplendentAI\/Luna-2x7B-MoE<\/a> \ud83d\udcd1<\/a>","206":"NousResearch\/Nous-Hermes-2-Mixtral-8x7B-DPO<\/a> \ud83d\udcd1<\/a>","207":"ibivibiv\/multimaster-7b-v3<\/a> \ud83d\udcd1<\/a>","208":"yunconglong\/7Bx4_DPO_2e<\/a> \ud83d\udcd1<\/a>","209":"argilla\/notux-8x7b-v1<\/a> \ud83d\udcd1<\/a>","210":"The-Face-Of-Goonery\/HuginnV5.5-12.6B<\/a> \ud83d\udcd1<\/a>","211":"Qwen\/Qwen1.5-72B<\/a> \ud83d\udcd1<\/a>","212":"logicker\/SkkuDS-DPO-72B-v1<\/a> \ud83d\udcd1<\/a>","213":"VAGOsolutions\/SauerkrautLM-Mixtral-8x7B-Instruct<\/a> \ud83d\udcd1<\/a>","214":"Himitsui\/Kaiju-11B<\/a> \ud83d\udcd1<\/a>","215":"PetroGPT\/Severus-7B-DPO<\/a> \ud83d\udcd1<\/a>","216":"S-miguel\/The-Trinity-Coder-7B<\/a> \ud83d\udcd1<\/a>","217":"logicker\/SkkuDS-DPO-72B-v3<\/a> \ud83d\udcd1<\/a>","218":"PSanni\/MPOMixtral-8x7B-Instruct-v0.1<\/a> \ud83d\udcd1<\/a>","219":"cloudyu\/19B_TRUTH_DPO<\/a> \ud83d\udcd1<\/a>","220":"JaeyeonKang\/CCK_Gony_v3.3<\/a> \ud83d\udcd1<\/a>","221":"VAGOsolutions\/SauerkrautLM-Mixtral-8x7B-Instruct<\/a> \ud83d\udcd1<\/a>","222":"tenyx\/TenyxChat-8x7B-v1<\/a> \ud83d\udcd1<\/a>","223":"mistralai\/Mixtral-8x7B-Instruct-v0.1<\/a> \ud83d\udcd1<\/a>","224":"ChaoticNeutrals\/RPMix-4x7B-MoE<\/a> \ud83d\udcd1<\/a>","225":"SJ-Donald\/SJ-SOLAR-10.7b-DPO<\/a> \ud83d\udcd1<\/a>","226":"senseable\/garten2-7b<\/a> \ud83d\udcd1<\/a>","227":"Azure99\/blossom-v5-34b<\/a> \ud83d\udcd1<\/a>","228":"Sao10K\/Fimbulvetr-11B-v2<\/a> \ud83d\udcd1<\/a>","229":"mistralai\/Mixtral-8x7B-Instruct-v0.1<\/a> \ud83d\udcd1<\/a>","230":"FelixChao\/Severus-7B<\/a> \ud83d\udcd1<\/a>","231":"Himitsui\/KuroMitsu-11B<\/a> \ud83d\udcd1<\/a>","232":"maywell\/PiVoT-SUS-RP<\/a> \ud83d\udcd1<\/a>","233":"jondurbin\/bagel-dpo-8x7b-v0.2<\/a> \ud83d\udcd1<\/a>","234":"ignos\/Mistral-T5-7B-v1<\/a> \ud83d\udcd1<\/a>","235":"SanjiWatsuki\/Kunoichi-DPO-v2-7B<\/a> \ud83d\udcd1<\/a>","236":"Brillibits\/Instruct_Mixtral-8x7B-v0.1_Dolly15K<\/a> \ud83d\udcd1<\/a>","237":"Sao10K\/Fimbulvetr-11B-v2<\/a> \ud83d\udcd1<\/a>","238":"SanjiWatsuki\/Kunoichi-DPO-v2-7B<\/a> \ud83d\udcd1<\/a>","239":"cognitivecomputations\/mixtral-instruct-0.1-laser<\/a> \ud83d\udcd1<\/a>","240":"cognitivecomputations\/laserxtral<\/a> \ud83d\udcd1<\/a>","241":"OpenBuddy\/openbuddy-deepseek-67b-v15.2<\/a> \ud83d\udcd1<\/a>","242":"macadeliccc\/piccolo-math-2x7b<\/a> \ud83d\udcd1<\/a>","243":"JaeyeonKang\/CCK_Gony_v0.1<\/a> \ud83d\udcd1<\/a>","244":"R136a1\/InfinityKuno-2x7B<\/a> \ud83d\udcd1<\/a>","245":"jan-ai\/Solar-10.7B-SLERP<\/a> \ud83d\udcd1<\/a>","246":"mncai\/yi-34B-v3<\/a> \ud83d\udcd1<\/a>","247":"NeverSleep\/CausalLM-RP-34B<\/a> \ud83d\udcd1<\/a>","248":"Sao10K\/Fimbulvetr-10.7B-v1<\/a> \ud83d\udcd1<\/a>","249":"SanjiWatsuki\/Kunoichi-DPO-7B<\/a> \ud83d\udcd1<\/a>","250":"jan-hq\/supermario-slerp-v3<\/a> \ud83d\udcd1<\/a>","251":"viethq188\/LeoScorpius-7B<\/a> \ud83d\udcd1<\/a>","252":"JaeyeonKang\/CCK_Gony_v3.1<\/a> \ud83d\udcd1<\/a>","253":"adonlee\/Mistral_7B_SFT_DPO_v0<\/a> \ud83d\udcd1<\/a>","254":"mncai\/yi-34B-v2<\/a> \ud83d\udcd1<\/a>","255":"NousResearch\/Nous-Hermes-2-Mixtral-8x7B-SFT<\/a> \ud83d\udcd1<\/a>","256":"CausalLM\/72B-preview-llamafied-qwen-llamafy<\/a> \ud83d\udcd1<\/a>","257":"OpenPipe\/mistral-ft-optimized-1218<\/a> \ud83d\udcd1<\/a>","258":"bn22\/Nous-Hermes-2-SOLAR-10.7B-MISALIGNED<\/a> \ud83d\udcd1<\/a>","259":"arlineka\/Brunhilde-2x7b-MOE-DPO-v.01.5<\/a> \ud83d\udcd1<\/a>","260":"OpenBuddy\/openbuddy-deepseek-67b-v18.1-4k<\/a> \ud83d\udcd1<\/a>","261":"deepseek-ai\/deepseek-llm-67b-chat<\/a> \ud83d\udcd1<\/a>","262":"mlabonne\/NeuralDarewin-7B<\/a> \ud83d\udcd1<\/a>","263":"OpenBuddy\/openbuddy-deepseek-67b-v15.1<\/a> \ud83d\udcd1<\/a>","264":"migtissera\/Tess-M-Creative-v1.0<\/a> \ud83d\udcd1<\/a>","265":"VitalContribution\/Evangelion-7B<\/a> \ud83d\udcd1<\/a>","266":"bhenrym14\/platypus-yi-34b<\/a> \ud83d\udcd1<\/a>","267":"RatanRohith\/NeuralPizza-7B-V0.3<\/a> \ud83d\udcd1<\/a>","268":"PracticeLLM\/SOLAR-tail-10.7B-Merge-v1.0<\/a> \ud83d\udcd1<\/a>","269":"Azure99\/blossom-v4-yi-34b<\/a> \ud83d\udcd1<\/a>","270":"llmixer\/BigWeave-v15-103b<\/a> \ud83d\udcd1<\/a>","271":"MaziyarPanahi\/Calme-7B-Instruct-v0.1<\/a> \ud83d\udcd1<\/a>","272":"Samee-ur\/NeuralPipe-7B-slerp-DPO<\/a> \ud83d\udcd1<\/a>","273":"VAGOsolutions\/SauerkrautLM-14b-MoE-LaserChat<\/a> \ud83d\udcd1<\/a>","274":"RatanRohith\/NeuralPizza-7B-V0.2<\/a> \ud83d\udcd1<\/a>","275":"RatanRohith\/NeuralPizza-7B-V0.1<\/a> \ud83d\udcd1<\/a>","276":"R136a1\/InfinityKumon-2x7B<\/a> \ud83d\udcd1<\/a>","277":"deepseek-ai\/deepseek-llm-67b-chat<\/a> \ud83d\udcd1<\/a>","278":"Sao10K\/14B-Glacier-Stack<\/a> \ud83d\udcd1<\/a>","279":"jan-hq\/supermario-slerp-v2<\/a> \ud83d\udcd1<\/a>","280":"dillfrescott\/amadeus-v0.1<\/a> \ud83d\udcd1<\/a>","281":"OpenBuddy\/openbuddy-deepseek-67b-v15.3-4k<\/a> \ud83d\udcd1<\/a>","282":"KnutJaegersberg\/Deita-20b<\/a> \ud83d\udcd1<\/a>","283":"LDCC\/LDCC-SOLAR-10.7B<\/a> \ud83d\udcd1<\/a>","284":"LDCC\/LDCC-SOLAR-10.7B<\/a> \ud83d\udcd1<\/a>"},"Average \u2b06\ufe0f":{"0":81.22,"1":81.0,"2":80.98,"3":80.81,"4":80.79,"5":80.72,"6":80.48,"7":79.3,"8":78.55,"9":77.91,"10":77.74,"11":77.52,"12":77.5,"13":77.44,"14":77.43,"15":77.41,"16":77.38,"17":77.3,"18":77.29,"19":77.29,"20":77.28,"21":77.22,"22":77.19,"23":77.1,"24":77.08,"25":77.07,"26":77.06,"27":77.05,"28":77.03,"29":76.95,"30":76.74,"31":76.74,"32":76.7,"33":76.67,"34":76.65,"35":76.62,"36":76.62,"37":76.61,"38":76.6,"39":76.6,"40":76.59,"41":76.59,"42":76.58,"43":76.57,"44":76.56,"45":76.55,"46":76.55,"47":76.53,"48":76.53,"49":76.5,"50":76.49,"51":76.49,"52":76.48,"53":76.47,"54":76.45,"55":76.44,"56":76.44,"57":76.43,"58":76.42,"59":76.41,"60":76.41,"61":76.4,"62":76.4,"63":76.38,"64":76.37,"65":76.36,"66":76.34,"67":76.34,"68":76.29,"69":76.23,"70":76.16,"71":76.13,"72":76.1,"73":76.09,"74":76.06,"75":76.05,"76":76.04,"77":76.04,"78":76.0,"79":76.0,"80":75.98,"81":75.96,"82":75.94,"83":75.93,"84":75.91,"85":75.9,"86":75.89,"87":75.89,"88":75.86,"89":75.81,"90":75.76,"91":75.74,"92":75.71,"93":75.66,"94":75.64,"95":75.63,"96":75.49,"97":75.48,"98":75.48,"99":75.47,"100":75.45,"101":75.44,"102":75.41,"103":75.41,"104":75.4,"105":75.4,"106":75.39,"107":75.37,"108":75.36,"109":75.36,"110":75.31,"111":75.29,"112":75.29,"113":75.29,"114":75.28,"115":75.25,"116":75.24,"117":75.22,"118":75.17,"119":75.14,"120":75.12,"121":75.1,"122":75.04,"123":75.01,"124":74.98,"125":74.87,"126":74.8,"127":74.8,"128":74.78,"129":74.75,"130":74.71,"131":74.7,"132":74.69,"133":74.69,"134":74.68,"135":74.67,"136":74.65,"137":74.65,"138":74.64,"139":74.64,"140":74.5,"141":74.48,"142":74.48,"143":74.47,"144":74.37,"145":74.3,"146":74.28,"147":74.28,"148":74.27,"149":74.27,"150":74.26,"151":74.26,"152":74.21,"153":74.21,"154":74.2,"155":74.2,"156":74.19,"157":74.13,"158":74.11,"159":74.11,"160":74.11,"161":74.1,"162":74.08,"163":74.07,"164":74.07,"165":74.05,"166":74.03,"167":73.94,"168":73.94,"169":73.87,"170":73.85,"171":73.84,"172":73.83,"173":73.81,"174":73.78,"175":73.77,"176":73.77,"177":73.75,"178":73.74,"179":73.71,"180":73.68,"181":73.62,"182":73.6,"183":73.55,"184":73.54,"185":73.48,"186":73.47,"187":73.47,"188":73.44,"189":73.42,"190":73.39,"191":73.39,"192":73.39,"193":73.38,"194":73.38,"195":73.36,"196":73.35,"197":73.35,"198":73.3,"199":73.28,"200":73.25,"201":73.22,"202":73.21,"203":73.2,"204":73.16,"205":73.13,"206":73.12,"207":73.07,"208":72.99,"209":72.97,"210":72.93,"211":72.91,"212":72.89,"213":72.89,"214":72.82,"215":72.81,"216":72.81,"217":72.8,"218":72.8,"219":72.8,"220":72.76,"221":72.73,"222":72.72,"223":72.7,"224":72.68,"225":72.67,"226":72.65,"227":72.65,"228":72.63,"229":72.62,"230":72.58,"231":72.58,"232":72.57,"233":72.49,"234":72.47,"235":72.46,"236":72.44,"237":72.4,"238":72.4,"239":72.36,"240":72.34,"241":72.33,"242":72.32,"243":72.32,"244":72.32,"245":72.31,"246":72.26,"247":72.26,"248":72.25,"249":72.24,"250":72.22,"251":72.21,"252":72.2,"253":72.17,"254":72.12,"255":72.07,"256":72.0,"257":71.94,"258":71.83,"259":71.81,"260":71.8,"261":71.79,"262":71.79,"263":71.76,"264":71.73,"265":71.71,"266":71.69,"267":71.68,"268":71.68,"269":71.67,"270":71.67,"271":71.63,"272":71.6,"273":71.6,"274":71.59,"275":71.53,"276":71.52,"277":71.52,"278":71.47,"279":71.45,"280":71.42,"281":71.42,"282":71.4,"283":71.4,"284":71.4},"ARC":{"0":79.78,"1":78.67,"2":78.58,"3":76.79,"4":76.19,"5":76.19,"6":76.02,"7":73.89,"8":70.82,"9":74.06,"10":77.47,"11":74.06,"12":73.81,"13":74.91,"14":73.89,"15":74.57,"16":73.72,"17":71.25,"18":70.14,"19":74.23,"20":72.87,"21":73.63,"22":74.49,"23":74.32,"24":74.66,"25":72.95,"26":73.63,"27":74.32,"28":72.95,"29":73.21,"30":73.38,"31":72.87,"32":72.95,"33":73.12,"34":73.29,"35":73.38,"36":73.04,"37":73.12,"38":73.21,"39":73.38,"40":72.95,"41":74.06,"42":73.55,"43":73.55,"44":73.81,"45":74.23,"46":73.21,"47":73.12,"48":73.46,"49":72.78,"50":72.95,"51":73.04,"52":71.25,"53":73.55,"54":73.12,"55":73.12,"56":71.25,"57":73.04,"58":73.21,"59":73.04,"60":72.53,"61":73.21,"62":73.04,"63":72.44,"64":73.89,"65":73.29,"66":73.12,"67":72.78,"68":74.23,"69":69.62,"70":74.06,"71":73.55,"72":72.27,"73":73.89,"74":72.35,"75":72.87,"76":72.87,"77":72.44,"78":74.06,"79":73.12,"80":72.95,"81":72.87,"82":73.04,"83":71.42,"84":73.55,"85":71.5,"86":70.48,"87":72.53,"88":73.21,"89":72.61,"90":72.44,"91":72.7,"92":73.04,"93":72.78,"94":74.91,"95":73.98,"96":74.66,"97":71.16,"98":69.71,"99":72.53,"100":74.15,"101":71.5,"102":73.55,"103":73.55,"104":71.33,"105":72.53,"106":69.71,"107":73.89,"108":72.35,"109":71.33,"110":72.35,"111":72.1,"112":71.42,"113":71.33,"114":73.55,"115":72.27,"116":69.71,"117":72.44,"118":70.73,"119":71.5,"120":71.67,"121":69.8,"122":72.53,"123":72.18,"124":73.63,"125":73.04,"126":71.93,"127":68.17,"128":73.29,"129":71.16,"130":72.44,"131":72.95,"132":72.44,"133":71.93,"134":73.04,"135":69.2,"136":70.73,"137":72.01,"138":72.18,"139":69.11,"140":72.01,"141":73.21,"142":72.1,"143":68.17,"144":71.42,"145":71.08,"146":71.33,"147":72.1,"148":71.67,"149":71.16,"150":71.76,"151":72.1,"152":72.27,"153":70.82,"154":71.08,"155":70.56,"156":71.33,"157":65.7,"158":71.08,"159":72.1,"160":65.7,"161":71.08,"162":71.25,"163":70.73,"164":70.73,"165":73.38,"166":70.73,"167":70.48,"168":70.39,"169":73.04,"170":70.48,"171":70.9,"172":71.33,"173":71.84,"174":69.8,"175":71.33,"176":72.1,"177":70.39,"178":66.89,"179":70.48,"180":67.32,"181":70.82,"182":65.19,"183":71.84,"184":71.76,"185":72.18,"186":72.18,"187":72.1,"188":69.8,"189":72.44,"190":69.97,"191":72.01,"192":72.53,"193":72.18,"194":72.18,"195":71.76,"196":71.08,"197":71.25,"198":66.64,"199":71.93,"200":68.09,"201":66.3,"202":64.51,"203":69.37,"204":71.84,"205":71.16,"206":71.42,"207":70.39,"208":68.94,"209":70.65,"210":72.01,"211":65.87,"212":65.96,"213":70.48,"214":69.97,"215":70.22,"216":69.37,"217":66.04,"218":70.99,"219":71.67,"220":70.39,"221":70.56,"222":69.71,"223":70.14,"224":71.08,"225":68.26,"226":69.37,"227":66.98,"228":70.14,"229":70.22,"230":68.43,"231":70.31,"232":66.55,"233":72.1,"234":68.6,"235":69.62,"236":69.28,"237":70.14,"238":69.37,"239":70.48,"240":69.03,"241":68.6,"242":69.11,"243":70.05,"244":69.62,"245":70.73,"246":67.06,"247":68.0,"248":68.94,"249":69.62,"250":69.28,"251":69.28,"252":69.62,"253":66.3,"254":66.13,"255":69.71,"256":65.19,"257":67.92,"258":68.26,"259":69.54,"260":67.75,"261":67.75,"262":70.14,"263":67.66,"264":66.81,"265":68.94,"266":68.43,"267":71.08,"268":66.13,"269":66.81,"270":69.71,"271":67.24,"272":69.28,"273":66.72,"274":68.77,"275":70.48,"276":69.62,"277":67.75,"278":71.67,"279":69.71,"280":68.94,"281":67.58,"282":63.91,"283":67.32,"284":67.58},"HellaSwag":{"0":91.15,"1":89.77,"2":89.74,"3":89.02,"4":89.44,"5":89.46,"6":89.27,"7":88.16,"8":85.96,"9":86.74,"10":91.88,"11":86.67,"12":89.22,"13":89.3,"14":89.07,"15":86.74,"16":86.46,"17":85.53,"18":86.03,"19":86.76,"20":86.52,"21":89.04,"22":86.76,"23":89.5,"24":89.51,"25":86.22,"26":86.59,"27":89.39,"28":88.86,"29":86.11,"30":89.15,"31":89.2,"32":89.23,"33":89.12,"34":89.11,"35":89.13,"36":89.04,"37":89.19,"38":89.13,"39":89.11,"40":89.15,"41":88.96,"42":89.19,"43":89.14,"44":89.06,"45":89.37,"46":89.19,"47":89.06,"48":89.09,"49":89.15,"50":89.26,"51":89.25,"52":85.24,"53":89.13,"54":89.09,"55":89.07,"56":85.1,"57":89.3,"58":89.03,"59":89.0,"60":88.75,"61":89.01,"62":89.11,"63":88.73,"64":89.26,"65":88.89,"66":89.09,"67":89.29,"68":88.82,"69":85.35,"70":88.9,"71":89.11,"72":89.31,"73":88.94,"74":89.77,"75":88.77,"76":88.47,"77":88.74,"78":88.97,"79":89.21,"80":88.19,"81":88.75,"82":89.23,"83":89.03,"84":88.84,"85":88.89,"86":84.63,"87":88.36,"88":89.26,"89":89.02,"90":88.41,"91":89.16,"92":88.62,"93":88.77,"94":87.79,"95":88.17,"96":87.72,"97":85.46,"98":85.28,"99":88.77,"100":85.98,"101":87.88,"102":85.93,"103":85.93,"104":88.5,"105":88.85,"106":85.18,"107":88.85,"108":88.61,"109":87.86,"110":88.77,"111":87.94,"112":88.27,"113":87.06,"114":88.87,"115":88.9,"116":85.17,"117":88.91,"118":88.01,"119":88.7,"120":87.96,"121":85.26,"122":88.52,"123":88.42,"124":89.03,"125":88.28,"126":88.44,"127":84.23,"128":88.66,"129":85.44,"130":88.03,"131":88.33,"132":85.64,"133":85.25,"134":88.65,"135":85.07,"136":87.75,"137":88.83,"138":87.88,"139":85.0,"140":85.24,"141":88.49,"142":88.88,"143":83.97,"144":88.08,"145":88.46,"146":87.94,"147":88.27,"148":88.33,"149":88.27,"150":88.28,"151":88.31,"152":86.59,"153":88.63,"154":88.16,"155":88.18,"156":88.62,"157":84.62,"158":88.2,"159":88.44,"160":84.58,"161":88.43,"162":88.34,"163":88.32,"164":88.2,"165":87.87,"166":88.51,"167":88.08,"168":88.03,"169":88.0,"170":88.76,"171":87.93,"172":88.71,"173":87.47,"174":86.9,"175":88.33,"176":88.99,"177":87.87,"178":85.49,"179":87.56,"180":83.52,"181":88.09,"182":85.94,"183":88.04,"184":88.08,"185":88.68,"186":87.96,"187":88.13,"188":87.83,"189":88.33,"190":86.82,"191":88.06,"192":88.17,"193":88.15,"194":88.09,"195":87.37,"196":87.29,"197":88.0,"198":85.73,"199":86.82,"200":86.5,"201":83.91,"202":85.67,"203":86.89,"204":86.84,"205":88.12,"206":87.21,"207":87.65,"208":86.8,"209":87.72,"210":86.7,"211":85.99,"212":86.0,"213":87.75,"214":87.72,"215":87.09,"216":86.17,"217":86.11,"218":87.95,"219":88.63,"220":87.88,"221":87.74,"222":87.76,"223":87.55,"224":87.79,"225":86.95,"226":87.54,"227":84.79,"228":87.79,"229":87.63,"230":86.89,"231":88.07,"232":84.23,"233":86.41,"234":86.3,"235":87.44,"236":87.59,"237":87.77,"238":87.42,"239":87.28,"240":86.76,"241":86.37,"242":87.27,"243":87.27,"244":87.44,"245":87.87,"246":85.11,"247":83.43,"248":87.27,"249":87.14,"250":86.71,"251":87.01,"252":87.45,"253":84.9,"254":85.0,"255":86.74,"256":83.24,"257":86.26,"258":86.11,"259":87.02,"260":84.65,"261":86.82,"262":86.4,"263":86.49,"264":85.14,"265":86.45,"266":85.21,"267":87.38,"268":86.54,"269":84.44,"270":86.41,"271":85.57,"272":86.34,"273":84.88,"274":86.11,"275":87.3,"276":87.09,"277":86.8,"278":88.35,"279":86.54,"280":86.98,"281":85.15,"282":83.11,"283":88.11,"284":88.11},"MMLU":{"0":77.95,"1":78.22,"2":78.27,"3":77.2,"4":77.07,"5":77.17,"6":77.15,"7":77.4,"8":77.13,"9":76.65,"10":68.1,"11":76.69,"12":64.92,"13":64.67,"14":75.44,"15":76.68,"16":76.72,"17":76.63,"18":77.4,"19":76.66,"20":76.96,"21":75.99,"22":76.55,"23":64.47,"24":64.53,"25":77.05,"26":76.55,"27":64.48,"28":75.41,"29":75.44,"30":64.32,"31":64.4,"32":64.42,"33":64.3,"34":64.35,"35":64.28,"36":64.44,"37":64.36,"38":64.34,"39":64.36,"40":64.44,"41":64.45,"42":64.36,"43":64.29,"44":64.34,"45":64.54,"46":64.39,"47":64.49,"48":64.4,"49":64.51,"50":64.32,"51":64.4,"52":77.28,"53":64.45,"54":64.8,"55":64.8,"56":77.36,"57":64.13,"58":64.53,"59":64.38,"60":74.96,"61":64.45,"62":64.79,"63":64.56,"64":64.94,"65":64.39,"66":64.79,"67":64.25,"68":65.01,"69":77.33,"70":65.0,"71":64.91,"72":66.55,"73":65.03,"74":66.35,"75":64.69,"76":64.29,"77":64.64,"78":64.41,"79":64.43,"80":64.6,"81":65.18,"82":64.43,"83":63.92,"84":64.68,"85":64.13,"86":76.64,"87":75.31,"88":64.47,"89":64.26,"90":64.9,"91":64.05,"92":63.23,"93":64.74,"94":70.08,"95":64.49,"96":70.06,"97":77.66,"98":77.33,"99":64.85,"100":76.52,"101":75.2,"102":76.45,"103":76.45,"104":64.7,"105":66.71,"106":77.25,"107":64.84,"108":63.08,"109":75.14,"110":64.17,"111":74.93,"112":64.79,"113":74.76,"114":64.63,"115":64.69,"116":77.24,"117":64.68,"118":64.93,"119":64.9,"120":74.9,"121":77.24,"122":64.77,"123":65.06,"124":64.63,"125":64.71,"126":65.63,"127":76.54,"128":64.72,"129":75.37,"130":65.29,"131":64.95,"132":76.41,"133":76.58,"134":64.71,"135":77.12,"136":64.4,"137":64.5,"138":65.2,"139":77.26,"140":76.58,"141":64.64,"142":64.26,"143":76.33,"144":64.84,"145":66.13,"146":64.62,"147":64.15,"148":64.84,"149":66.12,"150":64.13,"151":64.16,"152":64.09,"153":66.2,"154":66.21,"155":66.08,"156":66.22,"157":77.13,"158":66.09,"159":65.45,"160":77.08,"161":66.25,"162":66.04,"163":66.1,"164":66.03,"165":63.49,"166":66.03,"167":66.25,"168":66.44,"169":63.48,"170":66.94,"171":65.46,"172":71.07,"173":71.11,"174":70.39,"175":70.47,"176":69.84,"177":66.45,"178":76.7,"179":65.33,"180":76.09,"181":74.72,"182":77.37,"183":65.25,"184":66.06,"185":60.01,"186":65.32,"187":65.25,"188":71.05,"189":65.24,"190":64.42,"191":65.32,"192":65.28,"193":65.1,"194":65.03,"195":69.78,"196":72.17,"197":64.28,"198":76.49,"199":70.38,"200":64.42,"201":76.41,"202":64.17,"203":64.73,"204":70.44,"205":64.41,"206":72.28,"207":65.07,"208":64.5,"209":71.39,"210":64.5,"211":77.2,"212":77.33,"213":71.37,"214":66.79,"215":64.93,"216":64.9,"217":77.34,"218":70.26,"219":65.78,"220":71.43,"221":71.08,"222":71.12,"223":71.4,"224":64.36,"225":66.73,"226":65.44,"227":76.0,"228":66.83,"229":71.16,"230":65.2,"231":66.66,"232":76.23,"233":70.27,"234":64.62,"235":64.94,"236":70.96,"237":66.68,"238":64.83,"239":71.07,"240":64.68,"241":71.5,"242":63.69,"243":71.21,"244":64.49,"245":65.77,"246":75.8,"247":83.1,"248":66.59,"249":64.79,"250":65.11,"251":65.04,"252":71.2,"253":64.53,"254":75.64,"255":72.21,"256":77.04,"257":64.99,"258":66.26,"259":64.93,"260":70.58,"261":72.42,"262":64.85,"263":70.3,"264":75.54,"265":63.97,"266":78.13,"267":64.29,"268":66.52,"269":74.34,"270":71.25,"271":64.97,"272":63.7,"273":65.17,"274":64.32,"275":64.42,"276":64.97,"277":72.19,"278":66.73,"279":64.82,"280":64.69,"281":70.38,"282":67.4,"283":66.83,"284":66.63},"TruthfulQA":{"0":74.5,"1":75.18,"2":75.09,"3":79.02,"4":77.82,"5":77.78,"6":76.67,"7":72.69,"8":74.71,"9":72.24,"10":79.17,"11":71.32,"12":78.57,"13":78.02,"14":71.75,"15":70.17,"16":71.01,"17":71.99,"18":69.0,"19":70.22,"20":73.28,"21":70.19,"22":70.21,"23":78.66,"24":78.63,"25":71.31,"26":69.68,"27":78.47,"28":69.1,"29":72.78,"30":78.24,"31":77.92,"32":78.41,"33":78.04,"34":77.86,"35":77.98,"36":78.49,"37":78.0,"38":77.66,"39":77.3,"40":77.96,"41":77.67,"42":78.31,"43":78.43,"44":78.54,"45":74.26,"46":76.82,"47":78.72,"48":77.76,"49":78.8,"50":78.1,"51":78.17,"52":66.74,"53":78.7,"54":77.45,"55":77.46,"56":66.24,"57":78.71,"58":78.3,"59":77.81,"60":67.33,"61":78.49,"62":77.48,"63":78.37,"64":72.47,"65":77.47,"66":77.52,"67":78.57,"68":73.96,"69":64.64,"70":71.2,"71":74.0,"72":78.55,"73":71.24,"74":77.92,"75":73.68,"76":77.23,"77":74.9,"78":76.19,"79":77.9,"80":77.01,"81":76.44,"82":78.01,"83":79.79,"84":69.6,"85":79.47,"86":68.19,"87":66.5,"88":78.02,"89":79.29,"90":73.76,"91":78.09,"92":77.72,"93":70.89,"94":66.88,"95":79.36,"96":66.95,"97":63.84,"98":63.91,"99":70.74,"100":73.74,"101":61.96,"102":73.55,"103":73.55,"104":72.51,"105":77.13,"106":63.82,"107":69.81,"108":78.18,"109":61.95,"110":78.87,"111":63.48,"112":72.37,"113":63.8,"114":69.77,"115":73.07,"116":63.55,"117":73.03,"118":70.53,"119":72.49,"120":61.79,"121":64.82,"122":70.35,"123":70.37,"124":70.71,"125":70.94,"126":76.13,"127":65.44,"128":67.04,"129":67.01,"130":68.92,"131":70.01,"132":72.7,"133":70.05,"134":67.06,"135":62.66,"136":70.25,"137":70.24,"138":74.68,"139":62.71,"140":70.16,"141":67.36,"142":74.13,"143":63.67,"144":67.5,"145":72.29,"146":72.11,"147":71.03,"148":70.41,"149":71.57,"150":71.08,"151":71.19,"152":69.84,"153":71.95,"154":71.43,"155":72.05,"156":70.92,"157":60.64,"158":71.51,"159":76.75,"160":60.56,"161":72.11,"162":71.36,"163":72.52,"164":71.73,"165":69.93,"166":72.21,"167":70.49,"168":72.34,"169":69.85,"170":67.01,"171":70.82,"172":73.33,"173":68.81,"174":71.97,"175":62.81,"176":65.78,"177":68.48,"178":60.37,"179":64.97,"180":60.57,"181":56.97,"182":60.19,"183":70.59,"184":71.98,"185":77.05,"186":71.1,"187":70.25,"188":69.18,"189":71.07,"190":65.97,"191":70.42,"192":69.98,"193":69.97,"194":70.39,"195":70.07,"196":54.83,"197":68.48,"198":58.08,"199":65.21,"200":67.35,"201":57.04,"202":76.73,"203":65.66,"204":65.22,"205":68.66,"206":54.53,"207":59.7,"208":65.6,"209":66.21,"210":70.45,"211":59.61,"212":59.54,"213":65.71,"214":62.15,"215":64.41,"216":61.25,"217":59.73,"218":66.52,"219":72.23,"220":67.41,"221":65.72,"222":65.42,"223":64.98,"224":67.29,"225":67.74,"226":59.5,"227":62.68,"228":63.43,"229":64.58,"230":61.36,"231":61.36,"232":54.57,"233":72.83,"234":61.86,"235":66.06,"236":64.83,"237":63.42,"238":66.0,"239":65.83,"240":63.8,"241":56.2,"242":63.86,"243":63.23,"244":63.28,"245":65.72,"246":57.54,"247":54.51,"248":60.54,"249":67.31,"250":61.77,"251":63.95,"252":64.17,"253":69.72,"254":57.34,"255":51.22,"256":52.55,"257":59.48,"258":57.79,"259":65.47,"260":55.66,"261":55.85,"262":62.92,"263":54.42,"264":57.68,"265":64.01,"266":54.48,"267":67.93,"268":60.57,"269":57.89,"270":66.1,"271":59.38,"272":63.53,"273":57.64,"274":61.38,"275":67.22,"276":61.99,"277":55.83,"278":65.37,"279":63.06,"280":63.82,"281":54.88,"282":57.29,"283":68.85,"284":68.87},"Winogrande":{"0":87.85,"1":87.53,"2":87.37,"3":84.06,"4":84.93,"5":84.45,"6":85.08,"7":86.03,"8":84.06,"9":83.35,"10":87.45,"11":83.43,"12":87.37,"13":88.24,"14":86.35,"15":83.82,"16":83.35,"17":81.45,"18":84.37,"19":83.66,"20":83.19,"21":85.48,"22":83.27,"23":88.08,"24":88.08,"25":83.98,"26":83.43,"27":88.0,"28":85.08,"29":82.95,"30":84.93,"31":84.77,"32":84.93,"33":85.0,"34":84.93,"35":84.93,"36":85.4,"37":84.93,"38":84.85,"39":85.0,"40":85.0,"41":85.0,"42":85.0,"43":85.16,"44":85.16,"45":87.77,"46":85.32,"47":85.0,"48":84.85,"49":84.85,"50":85.16,"51":84.85,"52":84.29,"53":84.93,"54":84.77,"55":84.69,"56":84.77,"57":85.32,"58":84.61,"59":84.53,"60":85.87,"61":85.4,"62":84.77,"63":83.82,"64":88.0,"65":84.69,"66":84.69,"67":84.93,"68":85.24,"69":84.14,"70":87.53,"71":85.56,"72":83.82,"73":87.61,"74":84.06,"75":84.37,"76":83.11,"77":85.24,"78":84.29,"79":84.61,"80":83.9,"81":83.98,"82":84.69,"83":85.48,"84":88.16,"85":84.77,"86":82.72,"87":85.32,"88":84.45,"89":85.16,"90":83.27,"91":85.16,"92":85.0,"93":86.42,"94":81.69,"95":81.69,"96":81.61,"97":84.93,"98":84.37,"99":86.27,"100":83.27,"101":84.77,"102":82.95,"103":82.95,"104":83.5,"105":83.27,"106":84.14,"107":86.66,"108":84.53,"109":84.53,"110":85.32,"111":84.85,"112":83.27,"113":83.98,"114":84.45,"115":85.32,"116":84.14,"117":85.56,"118":83.5,"119":83.19,"120":85.08,"121":84.77,"122":85.79,"123":86.03,"124":84.37,"125":84.69,"126":82.16,"127":82.24,"128":86.74,"129":82.56,"130":84.77,"131":82.64,"132":82.48,"133":83.35,"134":86.98,"135":83.74,"136":82.08,"137":86.66,"138":80.66,"139":83.74,"140":83.03,"141":86.03,"142":86.35,"143":82.4,"144":84.77,"145":83.35,"146":83.5,"147":84.53,"148":81.85,"149":83.66,"150":84.53,"151":84.45,"152":83.35,"153":83.5,"154":83.58,"155":83.66,"156":83.43,"157":83.03,"158":83.5,"159":82.72,"160":83.11,"161":82.95,"162":83.19,"163":83.35,"164":82.95,"165":82.08,"166":82.72,"167":83.43,"168":83.03,"169":82.16,"170":83.5,"171":82.48,"172":81.22,"173":81.77,"174":82.0,"175":83.35,"176":83.27,"177":84.21,"178":82.95,"179":81.93,"180":82.32,"181":85.24,"182":82.48,"183":80.82,"184":82.32,"185":84.21,"186":80.74,"187":80.66,"188":81.37,"189":80.66,"190":80.98,"191":80.74,"192":80.82,"193":81.22,"194":81.14,"195":80.9,"196":83.11,"197":82.79,"198":83.35,"199":83.58,"200":84.77,"201":83.5,"202":80.51,"203":80.58,"204":83.11,"205":83.27,"206":82.64,"207":84.06,"208":80.74,"209":80.74,"210":81.29,"211":83.03,"212":82.64,"213":81.22,"214":83.5,"215":80.66,"216":81.77,"217":82.64,"218":82.56,"219":82.16,"220":81.22,"221":81.45,"222":81.22,"223":81.06,"224":81.93,"225":84.21,"226":84.69,"227":83.43,"228":82.95,"229":81.37,"230":80.9,"231":84.69,"232":83.35,"233":83.27,"234":80.27,"235":80.82,"236":82.56,"237":82.72,"238":80.74,"239":80.82,"240":80.03,"241":84.45,"242":79.87,"243":80.35,"244":82.72,"245":82.48,"246":83.5,"247":82.16,"248":83.5,"249":80.58,"250":80.51,"251":81.53,"252":81.14,"253":81.77,"254":83.66,"255":82.95,"256":82.4,"257":80.74,"258":83.43,"259":80.9,"260":82.95,"261":84.21,"262":79.72,"263":84.77,"264":83.11,"265":79.95,"266":84.06,"267":80.51,"268":84.77,"269":82.4,"270":80.35,"271":83.35,"272":80.51,"273":81.93,"274":80.35,"275":80.35,"276":81.93,"277":84.21,"278":84.06,"279":80.74,"280":79.95,"281":83.35,"282":84.61,"283":83.66,"284":83.66},"GSM8K":{"0":76.12,"1":76.65,"2":76.8,"3":78.77,"4":79.3,"5":79.23,"6":78.7,"7":77.63,"8":78.62,"9":74.45,"10":62.4,"11":72.93,"12":71.11,"13":69.52,"14":68.08,"15":72.48,"16":73.01,"17":76.95,"18":76.8,"19":72.18,"20":70.89,"21":68.99,"22":71.87,"23":67.55,"24":67.1,"25":70.89,"26":72.48,"27":67.63,"28":70.81,"29":71.19,"30":70.43,"31":71.27,"32":70.28,"33":70.43,"34":70.36,"35":70.05,"36":69.29,"37":70.05,"38":70.43,"39":70.43,"40":70.05,"41":69.37,"42":69.07,"43":68.84,"44":68.46,"45":69.14,"46":70.36,"47":68.76,"48":69.6,"49":68.92,"50":69.14,"51":69.22,"52":74.07,"53":68.08,"54":69.45,"55":69.52,"56":73.92,"57":68.08,"58":68.84,"59":69.67,"60":68.99,"61":67.85,"62":69.22,"63":70.36,"64":69.67,"65":69.45,"66":68.84,"67":68.23,"68":70.51,"69":76.27,"70":70.28,"71":69.67,"72":66.11,"73":69.83,"74":65.88,"75":71.95,"76":70.28,"77":70.28,"78":68.08,"79":66.72,"80":69.22,"81":68.54,"82":66.26,"83":65.96,"84":70.66,"85":66.64,"86":72.71,"87":67.32,"88":65.73,"89":64.52,"90":71.8,"91":65.28,"92":66.64,"93":70.36,"94":72.48,"95":66.11,"96":71.95,"97":69.83,"98":72.25,"99":69.67,"100":59.06,"101":71.34,"102":60.05,"103":60.05,"104":71.87,"105":63.91,"106":72.25,"107":68.16,"108":65.43,"109":71.34,"110":62.4,"111":68.46,"112":71.65,"113":70.81,"114":70.43,"115":67.25,"116":71.65,"117":66.72,"118":73.31,"119":70.05,"120":69.29,"121":68.69,"122":68.31,"123":68.01,"124":67.48,"125":67.55,"126":64.52,"127":72.18,"128":68.23,"129":66.94,"130":68.84,"131":69.29,"132":58.45,"133":60.96,"134":67.63,"135":70.2,"136":72.71,"137":65.66,"138":67.25,"139":69.98,"140":59.97,"141":67.17,"142":61.18,"143":72.25,"144":69.6,"145":64.52,"146":66.19,"147":65.58,"148":68.54,"149":64.82,"150":65.81,"151":65.35,"152":69.14,"153":64.14,"154":64.75,"155":64.67,"156":64.59,"157":73.69,"158":64.29,"159":59.21,"160":73.62,"161":63.76,"162":64.29,"163":63.38,"164":64.75,"165":67.55,"166":63.99,"167":64.9,"168":63.38,"169":66.72,"170":66.41,"171":65.43,"172":57.32,"173":61.87,"174":61.64,"175":66.34,"176":62.62,"177":65.13,"178":70.05,"179":72.02,"180":72.25,"181":65.88,"182":70.43,"183":64.75,"184":61.03,"185":58.76,"186":63.53,"187":64.44,"188":61.41,"189":62.77,"190":72.18,"191":63.76,"192":63.53,"193":63.68,"194":63.46,"195":60.27,"196":71.65,"197":65.28,"198":69.52,"199":61.79,"200":68.39,"201":72.18,"202":67.7,"203":71.95,"204":61.49,"205":63.15,"206":70.66,"207":71.57,"208":71.34,"209":61.11,"210":62.62,"211":65.73,"212":65.88,"213":60.8,"214":66.79,"215":69.52,"216":73.39,"217":64.97,"218":58.53,"219":56.33,"220":58.23,"221":59.82,"222":61.11,"223":61.11,"224":63.61,"225":62.09,"226":69.37,"227":62.02,"228":64.67,"229":60.73,"230":72.71,"231":64.37,"232":70.51,"233":50.04,"234":73.16,"235":65.88,"236":59.44,"237":63.68,"238":66.03,"239":58.68,"240":69.75,"241":66.87,"242":70.13,"243":61.79,"244":66.34,"245":61.26,"246":64.52,"247":62.32,"248":66.64,"249":63.99,"250":69.98,"251":66.41,"252":59.59,"253":65.81,"254":64.97,"255":69.6,"256":71.57,"257":72.25,"258":69.14,"259":63.0,"260":69.22,"261":63.68,"262":66.72,"263":66.94,"264":62.09,"265":66.94,"266":59.82,"267":58.91,"268":65.58,"269":64.14,"270":56.18,"271":69.29,"272":66.26,"273":73.24,"274":68.61,"275":59.44,"276":63.53,"277":62.32,"278":52.62,"279":63.84,"280":64.14,"281":67.17,"282":72.1,"283":53.6,"284":53.53},"Type":{"0":"fine-tuned on domain-specific datasets","1":"chat models (RLHF, DPO, IFT, ...)","2":"fine-tuned on domain-specific datasets","3":"fine-tuned on domain-specific datasets","4":"fine-tuned on domain-specific datasets","5":"fine-tuned on domain-specific datasets","6":"fine-tuned on domain-specific datasets","7":"fine-tuned on domain-specific datasets","8":"chat models (RLHF, DPO, IFT, ...)","9":"fine-tuned on domain-specific datasets","10":"fine-tuned on domain-specific datasets","11":"fine-tuned on domain-specific datasets","12":"fine-tuned on domain-specific datasets","13":"chat models (RLHF, DPO, IFT, ...)","14":"fine-tuned on domain-specific datasets","15":"fine-tuned on domain-specific datasets","16":"fine-tuned on domain-specific datasets","17":"fine-tuned on domain-specific datasets","18":"chat models (RLHF, DPO, IFT, ...)","19":"fine-tuned on domain-specific datasets","20":"fine-tuned on domain-specific datasets","21":"fine-tuned on domain-specific datasets","22":"fine-tuned on domain-specific datasets","23":"fine-tuned on domain-specific datasets","24":"fine-tuned on domain-specific datasets","25":"fine-tuned on domain-specific datasets","26":"fine-tuned on domain-specific datasets","27":"fine-tuned on domain-specific datasets","28":"fine-tuned on domain-specific datasets","29":"fine-tuned on domain-specific datasets","30":"fine-tuned on domain-specific datasets","31":"fine-tuned on domain-specific datasets","32":"fine-tuned on domain-specific datasets","33":"fine-tuned on domain-specific datasets","34":"fine-tuned on domain-specific datasets","35":"fine-tuned on domain-specific datasets","36":"fine-tuned on domain-specific datasets","37":"fine-tuned on domain-specific datasets","38":"fine-tuned on domain-specific datasets","39":"fine-tuned on domain-specific datasets","40":"fine-tuned on domain-specific datasets","41":"fine-tuned on domain-specific datasets","42":"fine-tuned on domain-specific datasets","43":"fine-tuned on domain-specific datasets","44":"fine-tuned on domain-specific datasets","45":"chat models (RLHF, DPO, IFT, ...)","46":"base merges and moerges","47":"fine-tuned on domain-specific datasets","48":"fine-tuned on domain-specific datasets","49":"fine-tuned on domain-specific datasets","50":"fine-tuned on domain-specific datasets","51":"chat models (RLHF, DPO, IFT, ...)","52":"fine-tuned on domain-specific datasets","53":"fine-tuned on domain-specific datasets","54":"base merges and moerges","55":"base merges and moerges","56":"fine-tuned on domain-specific datasets","57":"fine-tuned on domain-specific datasets","58":"fine-tuned on domain-specific datasets","59":"fine-tuned on domain-specific datasets","60":"fine-tuned on domain-specific datasets","61":"fine-tuned on domain-specific datasets","62":"fine-tuned on domain-specific datasets","63":"fine-tuned on domain-specific datasets","64":"chat models (RLHF, DPO, IFT, ...)","65":"fine-tuned on domain-specific datasets","66":"base merges and moerges","67":"fine-tuned on domain-specific datasets","68":"fine-tuned on domain-specific datasets","69":"chat models (RLHF, DPO, IFT, ...)","70":"fine-tuned on domain-specific datasets","71":"fine-tuned on domain-specific datasets","72":"chat models (RLHF, DPO, IFT, ...)","73":"chat models (RLHF, DPO, IFT, ...)","74":"chat models (RLHF, DPO, IFT, ...)","75":"fine-tuned on domain-specific datasets","76":"fine-tuned on domain-specific datasets","77":"chat models (RLHF, DPO, IFT, ...)","78":"chat models (RLHF, DPO, IFT, ...)","79":"chat models (RLHF, DPO, IFT, ...)","80":"chat models (RLHF, DPO, IFT, ...)","81":"chat models (RLHF, DPO, IFT, ...)","82":"chat models (RLHF, DPO, IFT, ...)","83":"fine-tuned on domain-specific datasets","84":"fine-tuned on domain-specific datasets","85":"fine-tuned on domain-specific datasets","86":"fine-tuned on domain-specific datasets","87":"fine-tuned on domain-specific datasets","88":"chat models (RLHF, DPO, IFT, ...)","89":"fine-tuned on domain-specific datasets","90":"fine-tuned on domain-specific datasets","91":"chat models (RLHF, DPO, IFT, ...)","92":"fine-tuned on domain-specific datasets","93":"fine-tuned on domain-specific datasets","94":"fine-tuned on domain-specific datasets","95":"fine-tuned on domain-specific datasets","96":"fine-tuned on domain-specific datasets","97":"fine-tuned on domain-specific datasets","98":"fine-tuned on domain-specific datasets","99":"fine-tuned on domain-specific datasets","100":"fine-tuned on domain-specific datasets","101":"fine-tuned on domain-specific datasets","102":"fine-tuned on domain-specific datasets","103":"fine-tuned on domain-specific datasets","104":"fine-tuned on domain-specific datasets","105":"chat models (RLHF, DPO, IFT, ...)","106":"fine-tuned on domain-specific datasets","107":"fine-tuned on domain-specific datasets","108":"fine-tuned on domain-specific datasets","109":"fine-tuned on domain-specific datasets","110":"fine-tuned on domain-specific datasets","111":"chat models (RLHF, DPO, IFT, ...)","112":"fine-tuned on domain-specific datasets","113":"fine-tuned on domain-specific datasets","114":"fine-tuned on domain-specific datasets","115":"fine-tuned on domain-specific datasets","116":"fine-tuned on domain-specific datasets","117":"fine-tuned on domain-specific datasets","118":"fine-tuned on domain-specific datasets","119":"fine-tuned on domain-specific datasets","120":"fine-tuned on domain-specific datasets","121":"fine-tuned on domain-specific datasets","122":"fine-tuned on domain-specific datasets","123":"fine-tuned on domain-specific datasets","124":"chat models (RLHF, DPO, IFT, ...)","125":"chat models (RLHF, DPO, IFT, ...)","126":"chat models (RLHF, DPO, IFT, ...)","127":"chat models (RLHF, DPO, IFT, ...)","128":"fine-tuned on domain-specific datasets","129":"fine-tuned on domain-specific datasets","130":"chat models (RLHF, DPO, IFT, ...)","131":"chat models (RLHF, DPO, IFT, ...)","132":"fine-tuned on domain-specific datasets","133":"chat models (RLHF, DPO, IFT, ...)","134":"fine-tuned on domain-specific datasets","135":"fine-tuned on domain-specific datasets","136":"fine-tuned on domain-specific datasets","137":"chat models (RLHF, DPO, IFT, ...)","138":"chat models (RLHF, DPO, IFT, ...)","139":"fine-tuned on domain-specific datasets","140":"fine-tuned on domain-specific datasets","141":"fine-tuned on domain-specific datasets","142":"fine-tuned on domain-specific datasets","143":"chat models (RLHF, DPO, IFT, ...)","144":"fine-tuned on domain-specific datasets","145":"chat models (RLHF, DPO, IFT, ...)","146":"fine-tuned on domain-specific datasets","147":"chat models (RLHF, DPO, IFT, ...)","148":"fine-tuned on domain-specific datasets","149":"chat models (RLHF, DPO, IFT, ...)","150":"chat models (RLHF, DPO, IFT, ...)","151":"fine-tuned on domain-specific datasets","152":"fine-tuned on domain-specific datasets","153":"fine-tuned on domain-specific datasets","154":"chat models (RLHF, DPO, IFT, ...)","155":"fine-tuned on domain-specific datasets","156":"chat models (RLHF, DPO, IFT, ...)","157":"fine-tuned on domain-specific datasets","158":"chat models (RLHF, DPO, IFT, ...)","159":"fine-tuned on domain-specific datasets","160":"fine-tuned on domain-specific datasets","161":"fine-tuned on domain-specific datasets","162":"chat models (RLHF, DPO, IFT, ...)","163":"chat models (RLHF, DPO, IFT, ...)","164":"fine-tuned on domain-specific datasets","165":"fine-tuned on domain-specific datasets","166":"base merges and moerges","167":"fine-tuned on domain-specific datasets","168":"chat models (RLHF, DPO, IFT, ...)","169":"chat models (RLHF, DPO, IFT, ...)","170":"chat models (RLHF, DPO, IFT, ...)","171":"chat models (RLHF, DPO, IFT, ...)","172":"fine-tuned on domain-specific datasets","173":"base merges and moerges","174":"fine-tuned on domain-specific datasets","175":"fine-tuned on domain-specific datasets","176":"chat models (RLHF, DPO, IFT, ...)","177":"base merges and moerges","178":"fine-tuned on domain-specific datasets","179":"chat models (RLHF, DPO, IFT, ...)","180":"fine-tuned on domain-specific datasets","181":"fine-tuned on domain-specific datasets","182":"pretrained","183":"fine-tuned on domain-specific datasets","184":"fine-tuned on domain-specific datasets","185":"fine-tuned on domain-specific datasets","186":"fine-tuned on domain-specific datasets","187":"fine-tuned on domain-specific datasets","188":"chat models (RLHF, DPO, IFT, ...)","189":"chat models (RLHF, DPO, IFT, ...)","190":"fine-tuned on domain-specific datasets","191":"fine-tuned on domain-specific datasets","192":"fine-tuned on domain-specific datasets","193":"fine-tuned on domain-specific datasets","194":"fine-tuned on domain-specific datasets","195":"base merges and moerges","196":"fine-tuned on domain-specific datasets","197":"fine-tuned on domain-specific datasets","198":"fine-tuned on domain-specific datasets","199":"chat models (RLHF, DPO, IFT, ...)","200":"fine-tuned on domain-specific datasets","201":"chat models (RLHF, DPO, IFT, ...)","202":"fine-tuned on domain-specific datasets","203":"chat models (RLHF, DPO, IFT, ...)","204":"fine-tuned on domain-specific datasets","205":"fine-tuned on domain-specific datasets","206":"chat models (RLHF, DPO, IFT, ...)","207":"fine-tuned on domain-specific datasets","208":"chat models (RLHF, DPO, IFT, ...)","209":"fine-tuned on domain-specific datasets","210":"fine-tuned on domain-specific datasets","211":"pretrained","212":"chat models (RLHF, DPO, IFT, ...)","213":"fine-tuned on domain-specific datasets","214":"fine-tuned on domain-specific datasets","215":"fine-tuned on domain-specific datasets","216":"base merges and moerges","217":"chat models (RLHF, DPO, IFT, ...)","218":"fine-tuned on domain-specific datasets","219":"fine-tuned on domain-specific datasets","220":"fine-tuned on domain-specific datasets","221":"fine-tuned on domain-specific datasets","222":"fine-tuned on domain-specific datasets","223":"fine-tuned on domain-specific datasets","224":"base merges and moerges","225":"chat models (RLHF, DPO, IFT, ...)","226":"fine-tuned on domain-specific datasets","227":"chat models (RLHF, DPO, IFT, ...)","228":"base merges and moerges","229":"chat models (RLHF, DPO, IFT, ...)","230":"fine-tuned on domain-specific datasets","231":"fine-tuned on domain-specific datasets","232":"chat models (RLHF, DPO, IFT, ...)","233":"fine-tuned on domain-specific datasets","234":"fine-tuned on domain-specific datasets","235":"fine-tuned on domain-specific datasets","236":"fine-tuned on domain-specific datasets","237":"base merges and moerges","238":"fine-tuned on domain-specific datasets","239":"chat models (RLHF, DPO, IFT, ...)","240":"fine-tuned on domain-specific datasets","241":"fine-tuned on domain-specific datasets","242":"base merges and moerges","243":"fine-tuned on domain-specific datasets","244":"base merges and moerges","245":"fine-tuned on domain-specific datasets","246":"chat models (RLHF, DPO, IFT, ...)","247":"fine-tuned on domain-specific datasets","248":"fine-tuned on domain-specific datasets","249":"fine-tuned on domain-specific datasets","250":"base merges and moerges","251":"fine-tuned on domain-specific datasets","252":"fine-tuned on domain-specific datasets","253":"chat models (RLHF, DPO, IFT, ...)","254":"chat models (RLHF, DPO, IFT, ...)","255":"chat models (RLHF, DPO, IFT, ...)","256":"fine-tuned on domain-specific datasets","257":"fine-tuned on domain-specific datasets","258":"chat models (RLHF, DPO, IFT, ...)","259":"fine-tuned on domain-specific datasets","260":"chat models (RLHF, DPO, IFT, ...)","261":"chat models (RLHF, DPO, IFT, ...)","262":"chat models (RLHF, DPO, IFT, ...)","263":"fine-tuned on domain-specific datasets","264":"fine-tuned on domain-specific datasets","265":"chat models (RLHF, DPO, IFT, ...)","266":"chat models (RLHF, DPO, IFT, ...)","267":"chat models (RLHF, DPO, IFT, ...)","268":"fine-tuned on domain-specific datasets","269":"chat models (RLHF, DPO, IFT, ...)","270":"base merges and moerges","271":"fine-tuned on domain-specific datasets","272":"chat models (RLHF, DPO, IFT, ...)","273":"fine-tuned on domain-specific datasets","274":"chat models (RLHF, DPO, IFT, ...)","275":"chat models (RLHF, DPO, IFT, ...)","276":"base merges and moerges","277":"fine-tuned on domain-specific datasets","278":"fine-tuned on domain-specific datasets","279":"base merges and moerges","280":"fine-tuned on domain-specific datasets","281":"chat models (RLHF, DPO, IFT, ...)","282":"fine-tuned on domain-specific datasets","283":"fine-tuned on domain-specific datasets","284":"fine-tuned on domain-specific datasets"},"Architecture":{"0":"LlamaForCausalLM","1":"LlamaForCausalLM","2":"LlamaForCausalLM","3":"?","4":"?","5":"?","6":"LlamaForCausalLM","7":"LlamaForCausalLM","8":"LlamaForCausalLM","9":"MixtralForCausalLM","10":"LlamaForCausalLM","11":"MixtralForCausalLM","12":"MixtralForCausalLM","13":"MixtralForCausalLM","14":"LlamaForCausalLM","15":"LlamaForCausalLM","16":"MixtralForCausalLM","17":"LlamaForCausalLM","18":"LlamaForCausalLM","19":"LlamaForCausalLM","20":"MixtralForCausalLM","21":"MixtralForCausalLM","22":"LlamaForCausalLM","23":"MixtralForCausalLM","24":"MixtralForCausalLM","25":"MixtralForCausalLM","26":"LlamaForCausalLM","27":"MixtralForCausalLM","28":"LlamaForCausalLM","29":"MixtralForCausalLM","30":"MistralForCausalLM","31":"MistralForCausalLM","32":"MistralForCausalLM","33":"MistralForCausalLM","34":"MistralForCausalLM","35":"MistralForCausalLM","36":"MistralForCausalLM","37":"MistralForCausalLM","38":"MistralForCausalLM","39":"MistralForCausalLM","40":"MistralForCausalLM","41":"MistralForCausalLM","42":"MistralForCausalLM","43":"MistralForCausalLM","44":"MistralForCausalLM","45":"MixtralForCausalLM","46":"MistralForCausalLM","47":"MistralForCausalLM","48":"MistralForCausalLM","49":"MistralForCausalLM","50":"MistralForCausalLM","51":"MistralForCausalLM","52":"MixtralForCausalLM","53":"MistralForCausalLM","54":"MistralForCausalLM","55":"MistralForCausalLM","56":"MixtralForCausalLM","57":"MistralForCausalLM","58":"MistralForCausalLM","59":"MistralForCausalLM","60":"LlamaForCausalLM","61":"MistralForCausalLM","62":"MistralForCausalLM","63":"MistralForCausalLM","64":"MixtralForCausalLM","65":"MistralForCausalLM","66":"MistralForCausalLM","67":"MistralForCausalLM","68":"MistralForCausalLM","69":"LlamaForCausalLM","70":"MixtralForCausalLM","71":"MistralForCausalLM","72":"MistralForCausalLM","73":"MixtralForCausalLM","74":"MistralForCausalLM","75":"MistralForCausalLM","76":"MistralForCausalLM","77":"MistralForCausalLM","78":"MistralForCausalLM","79":"MistralForCausalLM","80":"MistralForCausalLM","81":"MistralForCausalLM","82":"MistralForCausalLM","83":"MistralForCausalLM","84":"MixtralForCausalLM","85":"MistralForCausalLM","86":"MixtralForCausalLM","87":"MixtralForCausalLM","88":"MistralForCausalLM","89":"MistralForCausalLM","90":"MistralForCausalLM","91":"MistralForCausalLM","92":"MistralForCausalLM","93":"MixtralForCausalLM","94":"MixtralForCausalLM","95":"MixtralForCausalLM","96":"MixtralForCausalLM","97":"MixtralForCausalLM","98":"MixtralForCausalLM","99":"MixtralForCausalLM","100":"LlamaForCausalLM","101":"LlamaForCausalLM","102":"LlamaForCausalLM","103":"LlamaForCausalLM","104":"MistralForCausalLM","105":"MistralForCausalLM","106":"MixtralForCausalLM","107":"MistralForCausalLM","108":"MistralForCausalLM","109":"LlamaForCausalLM","110":"MistralForCausalLM","111":"LlamaForCausalLM","112":"MistralForCausalLM","113":"LlamaForCausalLM","114":"MistralForCausalLM","115":"MistralForCausalLM","116":"MixtralForCausalLM","117":"MistralForCausalLM","118":"MistralForCausalLM","119":"MistralForCausalLM","120":"LlamaForCausalLM","121":"MixtralForCausalLM","122":"MistralForCausalLM","123":"MixtralForCausalLM","124":"MistralForCausalLM","125":"MistralForCausalLM","126":"LlamaForCausalLM","127":"LlamaForCausalLM","128":"MistralForCausalLM","129":"MixtralForCausalLM","130":"MistralForCausalLM","131":"MistralForCausalLM","132":"LlamaForCausalLM","133":"LlamaForCausalLM","134":"MistralForCausalLM","135":"LlamaForCausalLM","136":"MistralForCausalLM","137":"MistralForCausalLM","138":"MixtralForCausalLM","139":"LlamaForCausalLM","140":"LlamaForCausalLM","141":"MistralForCausalLM","142":"MixtralForCausalLM","143":"LlamaForCausalLM","144":"MistralForCausalLM","145":"MixtralForCausalLM","146":"MistralForCausalLM","147":"MistralForCausalLM","148":"MistralForCausalLM","149":"LlamaForCausalLM","150":"MistralForCausalLM","151":"MistralForCausalLM","152":"MistralForCausalLM","153":"LlamaForCausalLM","154":"LlamaForCausalLM","155":"LlamaForCausalLM","156":"LlamaForCausalLM","157":"Qwen2ForCausalLM","158":"LlamaForCausalLM","159":"LlamaForCausalLM","160":"Qwen2ForCausalLM","161":"MixtralForCausalLM","162":"LlamaForCausalLM","163":"LlamaForCausalLM","164":"LlamaForCausalLM","165":"MixtralForCausalLM","166":"LlamaForCausalLM","167":"Unknown","168":"LlamaForCausalLM","169":"MistralForCausalLM","170":"MistralForCausalLM","171":"MistralForCausalLM","172":"MixtralForCausalLM","173":"MixtralForCausalLM","174":"MixtralForCausalLM","175":"LlamaForCausalLM","176":"LlamaForCausalLM","177":"MixtralForCausalLM","178":"LlamaForCausalLM","179":"MistralForCausalLM","180":"LlamaForCausalLM","181":"LlamaForCausalLM","182":"QWenLMHeadModel","183":"MistralForCausalLM","184":"LlamaForCausalLM","185":"MistralForCausalLM","186":"MistralForCausalLM","187":"MistralForCausalLM","188":"MixtralForCausalLM","189":"MistralForCausalLM","190":"MixtralForCausalLM","191":"MistralForCausalLM","192":"MistralForCausalLM","193":"MistralForCausalLM","194":"MistralForCausalLM","195":"MixtralForCausalLM","196":"MixtralForCausalLM","197":"MistralForCausalLM","198":"MixtralForCausalLM","199":"LlamaForCausalLM","200":"?","201":"LlamaForCausalLM","202":"LlamaForCausalLM","203":"MixtralForCausalLM","204":"LlamaForCausalLM","205":"MixtralForCausalLM","206":"MixtralForCausalLM","207":"MixtralForCausalLM","208":"MixtralForCausalLM","209":"MixtralForCausalLM","210":"MistralForCausalLM","211":"Qwen2ForCausalLM","212":"Qwen2ForCausalLM","213":"MixtralForCausalLM","214":"LlamaForCausalLM","215":"MistralForCausalLM","216":"MistralForCausalLM","217":"Qwen2ForCausalLM","218":"MixtralForCausalLM","219":"MixtralForCausalLM","220":"MixtralForCausalLM","221":"MixtralForCausalLM","222":"MixtralForCausalLM","223":"MixtralForCausalLM","224":"MixtralForCausalLM","225":"LlamaForCausalLM","226":"MistralForCausalLM","227":"LlamaForCausalLM","228":"LlamaForCausalLM","229":"MixtralForCausalLM","230":"MistralForCausalLM","231":"LlamaForCausalLM","232":"LlamaForCausalLM","233":"MixtralForCausalLM","234":"MistralForCausalLM","235":"MistralForCausalLM","236":"MixtralForCausalLM","237":"LlamaForCausalLM","238":"MistralForCausalLM","239":"MixtralForCausalLM","240":"MixtralForCausalLM","241":"LlamaForCausalLM","242":"MixtralForCausalLM","243":"MixtralForCausalLM","244":"MixtralForCausalLM","245":"LlamaForCausalLM","246":"LlamaForCausalLM","247":"LlamaForCausalLM","248":"LlamaForCausalLM","249":"MistralForCausalLM","250":"MistralForCausalLM","251":"MistralForCausalLM","252":"MixtralForCausalLM","253":"MistralForCausalLM","254":"LlamaForCausalLM","255":"MixtralForCausalLM","256":"LlamaForCausalLM","257":"MistralForCausalLM","258":"LlamaForCausalLM","259":"MixtralForCausalLM","260":"LlamaForCausalLM","261":"LlamaForCausalLM","262":"MistralForCausalLM","263":"LlamaForCausalLM","264":"LlamaForCausalLM","265":"MistralForCausalLM","266":"LlamaForCausalLM","267":"MistralForCausalLM","268":"LlamaForCausalLM","269":"LlamaForCausalLM","270":"LlamaForCausalLM","271":"MistralForCausalLM","272":"MistralForCausalLM","273":"MixtralForCausalLM","274":"MistralForCausalLM","275":"MistralForCausalLM","276":"MixtralForCausalLM","277":"LlamaForCausalLM","278":"LlamaForCausalLM","279":"MistralForCausalLM","280":"MixtralForCausalLM","281":"LlamaForCausalLM","282":"LlamaForCausalLM","283":"LlamaForCausalLM","284":"LlamaForCausalLM"},"Precision":{"0":"float16","1":"bfloat16","2":"float16","3":"bfloat16","4":"bfloat16","5":"bfloat16","6":"bfloat16","7":"float16","8":"bfloat16","9":"bfloat16","10":"bfloat16","11":"float16","12":"bfloat16","13":"bfloat16","14":"float16","15":"bfloat16","16":"bfloat16","17":"float16","18":"bfloat16","19":"bfloat16","20":"bfloat16","21":"float16","22":"float16","23":"bfloat16","24":"bfloat16","25":"bfloat16","26":"float16","27":"bfloat16","28":"float16","29":"4bit","30":"float16","31":"bfloat16","32":"float16","33":"bfloat16","34":"bfloat16","35":"float16","36":"float16","37":"float16","38":"bfloat16","39":"bfloat16","40":"float16","41":"float16","42":"bfloat16","43":"float16","44":"float16","45":"bfloat16","46":"bfloat16","47":"float16","48":"bfloat16","49":"float16","50":"float16","51":"float16","52":"bfloat16","53":"float16","54":"float16","55":"float16","56":"bfloat16","57":"float16","58":"bfloat16","59":"float16","60":"float16","61":"float16","62":"float16","63":"float16","64":"bfloat16","65":"float16","66":"float16","67":"float16","68":"bfloat16","69":"bfloat16","70":"float16","71":"float16","72":"float16","73":"float16","74":"float16","75":"float16","76":"bfloat16","77":"bfloat16","78":"float16","79":"float16","80":"float16","81":"float16","82":"float16","83":"float16","84":"bfloat16","85":"float16","86":"bfloat16","87":"bfloat16","88":"float16","89":"float16","90":"bfloat16","91":"float16","92":"float16","93":"float16","94":"bfloat16","95":"float16","96":"float16","97":"bfloat16","98":"bfloat16","99":"float16","100":"bfloat16","101":"float16","102":"bfloat16","103":"bfloat16","104":"float16","105":"float16","106":"bfloat16","107":"float16","108":"float16","109":"bfloat16","110":"float16","111":"bfloat16","112":"float16","113":"float16","114":"bfloat16","115":"bfloat16","116":"bfloat16","117":"float16","118":"float16","119":"float16","120":"float16","121":"bfloat16","122":"float16","123":"float16","124":"bfloat16","125":"float16","126":"bfloat16","127":"bfloat16","128":"float16","129":"bfloat16","130":"float16","131":"float16","132":"bfloat16","133":"float16","134":"float16","135":"bfloat16","136":"float16","137":"float16","138":"float16","139":"float16","140":"bfloat16","141":"float16","142":"bfloat16","143":"bfloat16","144":"float16","145":"bfloat16","146":"float16","147":"float16","148":"float16","149":"float16","150":"float16","151":"float16","152":"bfloat16","153":"bfloat16","154":"float16","155":"bfloat16","156":"float16","157":"bfloat16","158":"float16","159":"float16","160":"float16","161":"bfloat16","162":"float16","163":"float16","164":"bfloat16","165":"bfloat16","166":"float16","167":"bfloat16","168":"float16","169":"bfloat16","170":"float16","171":"bfloat16","172":"float16","173":"bfloat16","174":"bfloat16","175":"float16","176":"bfloat16","177":"bfloat16","178":"bfloat16","179":"float16","180":"bfloat16","181":"float16","182":"bfloat16","183":"float16","184":"bfloat16","185":"float16","186":"float16","187":"float16","188":"bfloat16","189":"bfloat16","190":"bfloat16","191":"float16","192":"float16","193":"float16","194":"float16","195":"bfloat16","196":"float16","197":"bfloat16","198":"bfloat16","199":"bfloat16","200":"float16","201":"bfloat16","202":"float16","203":"bfloat16","204":"float16","205":"bfloat16","206":"bfloat16","207":"float16","208":"bfloat16","209":"float16","210":"float16","211":"bfloat16","212":"float16","213":"float16","214":"float16","215":"float16","216":"float16","217":"float16","218":"bfloat16","219":"bfloat16","220":"float16","221":"bfloat16","222":"bfloat16","223":"float16","224":"bfloat16","225":"float16","226":"float16","227":"bfloat16","228":"float16","229":"bfloat16","230":"float16","231":"float16","232":"bfloat16","233":"bfloat16","234":"float16","235":"float16","236":"bfloat16","237":"bfloat16","238":"bfloat16","239":"float16","240":"bfloat16","241":"bfloat16","242":"bfloat16","243":"float16","244":"bfloat16","245":"bfloat16","246":"float16","247":"bfloat16","248":"float16","249":"bfloat16","250":"bfloat16","251":"float16","252":"float16","253":"float16","254":"float16","255":"bfloat16","256":"bfloat16","257":"bfloat16","258":"float16","259":"float16","260":"bfloat16","261":"float16","262":"float16","263":"bfloat16","264":"float16","265":"float16","266":"float16","267":"float16","268":"float16","269":"float16","270":"float16","271":"float16","272":"float16","273":"bfloat16","274":"float16","275":"float16","276":"float16","277":"bfloat16","278":"float16","279":"bfloat16","280":"float16","281":"bfloat16","282":"float16","283":"float16","284":"bfloat16"},"Merged":{"0":false,"1":false,"2":false,"3":false,"4":false,"5":false,"6":false,"7":false,"8":false,"9":false,"10":false,"11":false,"12":false,"13":false,"14":false,"15":false,"16":false,"17":false,"18":false,"19":false,"20":false,"21":false,"22":false,"23":false,"24":false,"25":false,"26":false,"27":false,"28":false,"29":false,"30":false,"31":false,"32":false,"33":false,"34":false,"35":false,"36":false,"37":false,"38":false,"39":false,"40":false,"41":false,"42":false,"43":false,"44":false,"45":false,"46":false,"47":false,"48":false,"49":false,"50":false,"51":false,"52":false,"53":false,"54":false,"55":false,"56":false,"57":false,"58":false,"59":false,"60":false,"61":false,"62":false,"63":false,"64":false,"65":false,"66":false,"67":false,"68":false,"69":false,"70":false,"71":false,"72":false,"73":false,"74":false,"75":false,"76":false,"77":false,"78":false,"79":false,"80":false,"81":false,"82":false,"83":false,"84":false,"85":false,"86":false,"87":false,"88":false,"89":false,"90":false,"91":false,"92":false,"93":false,"94":false,"95":false,"96":false,"97":false,"98":false,"99":false,"100":false,"101":false,"102":false,"103":false,"104":false,"105":false,"106":false,"107":false,"108":false,"109":false,"110":false,"111":false,"112":false,"113":false,"114":false,"115":false,"116":false,"117":false,"118":false,"119":false,"120":false,"121":false,"122":false,"123":false,"124":false,"125":false,"126":false,"127":false,"128":false,"129":false,"130":false,"131":false,"132":false,"133":false,"134":false,"135":false,"136":false,"137":false,"138":false,"139":false,"140":false,"141":false,"142":false,"143":false,"144":false,"145":false,"146":false,"147":false,"148":false,"149":false,"150":false,"151":false,"152":false,"153":false,"154":false,"155":false,"156":false,"157":false,"158":false,"159":false,"160":false,"161":false,"162":false,"163":false,"164":false,"165":false,"166":false,"167":false,"168":false,"169":false,"170":false,"171":false,"172":false,"173":false,"174":false,"175":false,"176":false,"177":false,"178":false,"179":false,"180":false,"181":false,"182":false,"183":false,"184":false,"185":false,"186":false,"187":false,"188":false,"189":false,"190":false,"191":false,"192":false,"193":false,"194":false,"195":false,"196":false,"197":false,"198":false,"199":false,"200":false,"201":false,"202":false,"203":false,"204":false,"205":false,"206":false,"207":false,"208":false,"209":false,"210":false,"211":false,"212":false,"213":false,"214":false,"215":false,"216":false,"217":false,"218":false,"219":false,"220":false,"221":false,"222":false,"223":false,"224":false,"225":false,"226":false,"227":false,"228":false,"229":false,"230":false,"231":false,"232":false,"233":false,"234":false,"235":false,"236":false,"237":false,"238":false,"239":false,"240":false,"241":false,"242":false,"243":false,"244":false,"245":false,"246":false,"247":false,"248":false,"249":false,"250":false,"251":false,"252":false,"253":false,"254":false,"255":false,"256":false,"257":false,"258":false,"259":false,"260":false,"261":false,"262":false,"263":false,"264":false,"265":false,"266":false,"267":false,"268":false,"269":false,"270":false,"271":false,"272":false,"273":false,"274":false,"275":false,"276":false,"277":false,"278":false,"279":false,"280":false,"281":false,"282":false,"283":false,"284":false},"Hub License":{"0":"apache-2.0","1":"other","2":"other","3":"apache-2.0","4":"apache-2.0","5":"apache-2.0","6":"other","7":"other","8":"mit","9":"other","10":"apache-2.0","11":"other","12":"apache-2.0","13":"mit","14":"cc-by-nc-4.0","15":"apache-2.0","16":"mit","17":"other","18":"mit","19":"other","20":"mit","21":"apache-2.0","22":"other","23":["other"],"24":"other","25":"mit","26":"other","27":"other","28":"cc-by-nc-4.0","29":"other","30":"apache-2.0","31":"apache-2.0","32":"apache-2.0","33":"apache-2.0","34":"mit","35":"apache-2.0","36":"apache-2.0","37":"apache-2.0","38":"mit","39":"mit","40":"apache-2.0","41":"openrail","42":"apache-2.0","43":"apache-2.0","44":"apache-2.0","45":"apache-2.0","46":"apache-2.0","47":"apache-2.0","48":"apache-2.0","49":"apache-2.0","50":"apache-2.0","51":"apache-2.0","52":"mit","53":"apache-2.0","54":"cc-by-nc-4.0","55":"cc-by-nc-4.0","56":"other","57":"apache-2.0","58":"cc-by-nc-4.0","59":"cc-by-4.0","60":"cc-by-nc-4.0","61":"apache-2.0","62":"cc-by-nc-4.0","63":"apache-2.0","64":"apache-2.0","65":"cc-by-4.0","66":"cc-by-nc-4.0","67":"apache-2.0","68":"apache-2.0","69":"mit","70":"mit","71":"cc","72":"apache-2.0","73":"mit","74":"apache-2.0","75":"apache-2.0","76":"apache-2.0","77":"mit","78":"apache-2.0","79":"cc-by-nc-4.0","80":"apache-2.0","81":"apache-2.0","82":"cc-by-nc-4.0","83":"apache-2.0","84":"mit","85":"apache-2.0","86":"other","87":"cc-by-nc-4.0","88":"cc-by-nc-4.0","89":"cc-by-nc-4.0","90":"apache-2.0","91":"cc-by-nc-4.0","92":"apache-2.0","93":"apache-2.0","94":"apache-2.0","95":"apache-2.0","96":"apache-2.0","97":"other","98":"other","99":"apache-2.0","100":"apache-2.0","101":"cc0-1.0","102":"other","103":"other","104":"apache-2.0","105":"apache-2.0","106":"other","107":"apache-2.0","108":"apache-2.0","109":"cc0-1.0","110":"apache-2.0","111":"cc-by-sa-4.0","112":"apache-2.0","113":"llama2","114":"cc-by-nc-4.0","115":"apache-2.0","116":"other","117":"apache-2.0","118":"apache-2.0","119":"apache-2.0","120":"cc-by-nc-4.0","121":"other","122":"apache-2.0","123":"apache-2.0","124":"cc-by-nc-4.0","125":"other","126":"apache-2.0","127":"apache-2.0","128":"apache-2.0","129":"other","130":"other","131":"cc-by-nc-4.0","132":"other","133":"other","134":"apache-2.0","135":"mit","136":"apache-2.0","137":"apache-2.0","138":"apache-2.0","139":"mit","140":"other","141":"apache-2.0","142":"cc-by-nc-nd-4.0","143":"apache-2.0","144":"apache-2.0","145":"other","146":"apache-2.0","147":"cc-by-sa-4.0","148":"apache-2.0","149":"mit","150":"cc-by-sa-4.0","151":"cc-by-sa-4.0","152":"apache-2.0","153":"cc-by-nc-4.0","154":"cc-by-nc-4.0","155":"cc-by-nc-nd-4.0","156":"mit","157":"other","158":"cc-by-nc-4.0","159":"cc","160":"other","161":"other","162":"cc-by-nc-4.0","163":"cc-by-nc-nd-4.0","164":"cc-by-nc-nd-4.0","165":"apache-2.0","166":"cc-by-nc-sa-4.0","167":"cc-by-nc-4.0","168":"apache-2.0","169":"cc-by-nc-nd-4.0","170":"apache-2.0","171":"apache-2.0","172":"cc-by-nc-4.0","173":"cc-by-nc-4.0","174":"cc-by-nc-sa-4.0","175":"llama2","176":"other","177":"apache-2.0","178":"apache-2.0","179":"cc-by-nc-4.0","180":"apache-2.0","181":"cc-by-nc-4.0","182":"other","183":"apache-2.0","184":"cc-by-nc-4.0","185":"apache-2.0","186":"apache-2.0","187":"apache-2.0","188":"apache-2.0","189":"apache-2.0","190":"cc-by-nc-4.0","191":"apache-2.0","192":"apache-2.0","193":"apache-2.0","194":"apache-2.0","195":"cc-by-nc-4.0","196":"apache-2.0","197":"apache-2.0","198":"apache-2.0","199":"llama2","200":"apache-2.0","201":"other","202":"cc-by-nc-4.0","203":"mit","204":"llama2","205":"apache-2.0","206":"apache-2.0","207":"apache-2.0","208":"mit","209":"apache-2.0","210":"cc-by-4.0","211":"other","212":"other","213":"apache-2.0","214":"cc-by-nc-4.0","215":"apache-2.0","216":"apache-2.0","217":"other","218":"apache-2.0","219":"cc-by-nc-4.0","220":"cc-by-nc-4.0","221":"apache-2.0","222":"apache-2.0","223":"apache-2.0","224":"apache-2.0","225":"cc-by-nc-4.0","226":"apache-2.0","227":"apache-2.0","228":"cc-by-nc-4.0","229":"apache-2.0","230":"apache-2.0","231":"cc-by-nc-4.0","232":"apache-2.0","233":"apache-2.0","234":"apache-2.0","235":"cc-by-nc-4.0","236":"apache-2.0","237":"cc-by-nc-4.0","238":"cc-by-nc-4.0","239":"apache-2.0","240":"cc-by-nc-2.0","241":"other","242":"mit","243":"cc-by-nc-4.0","244":"apache-2.0","245":"apache-2.0","246":"other","247":"cc-by-nc-4.0","248":"cc-by-nc-4.0","249":"cc-by-nc-4.0","250":"apache-2.0","251":"apache-2.0","252":"cc-by-nc-4.0","253":"apache-2.0","254":"other","255":"apache-2.0","256":"gpl-3.0","257":"cc-by-nc-4.0","258":"apache-2.0","259":"cc-by-nc-4.0","260":"other","261":"other","262":"apache-2.0","263":"other","264":"other","265":"apache-2.0","266":"other","267":"apache-2.0","268":"cc-by-nc-sa-4.0","269":"apache-2.0","270":"unknown","271":"apache-2.0","272":"apache-2.0","273":"apache-2.0","274":"apache-2.0","275":"apache-2.0","276":"apache-2.0","277":"other","278":"cc-by-nc-4.0","279":"apache-2.0","280":"wtfpl","281":"other","282":"other","283":"cc-by-nc-4.0","284":"cc-by-nc-4.0"},"#Params (B)":{"0":72.29,"1":72.29,"2":72.29,"3":72.0,"4":72.0,"5":72.0,"6":72.29,"7":72.29,"8":72.29,"9":60.81,"10":21.42,"11":60.81,"12":12.88,"13":12.88,"14":68.98,"15":34.39,"16":60.81,"17":72.29,"18":72.29,"19":34.39,"20":60.81,"21":125.35,"22":34.39,"23":12.88,"24":12.88,"25":60.81,"26":34.39,"27":12.88,"28":68.98,"29":31.8,"30":7.24,"31":7.24,"32":7.24,"33":7.24,"34":7.24,"35":7.24,"36":7.24,"37":7.24,"38":7.24,"39":7.24,"40":7.24,"41":7.24,"42":7.24,"43":7.24,"44":7.24,"45":12.88,"46":7.24,"47":7.24,"48":7.24,"49":7.24,"50":7.24,"51":7.24,"52":60.81,"53":7.24,"54":7.24,"55":7.24,"56":60.81,"57":7.24,"58":7.24,"59":7.24,"60":68.98,"61":7.24,"62":7.24,"63":7.24,"64":12.88,"65":7.24,"66":7.24,"67":7.24,"68":7.24,"69":72.29,"70":12.88,"71":7.24,"72":10.73,"73":12.88,"74":10.73,"75":7.24,"76":7.24,"77":7.24,"78":7.24,"79":7.24,"80":7.24,"81":7.24,"82":7.24,"83":7.24,"84":12.88,"85":7.24,"86":60.81,"87":125.35,"88":7.24,"89":7.24,"90":7.24,"91":7.24,"92":7.24,"93":35.43,"94":46.7,"95":12.88,"96":46.7,"97":60.81,"98":113.66,"99":35.43,"100":34.39,"101":68.98,"102":34.39,"103":34.39,"104":7.24,"105":10.73,"106":87.24,"107":7.24,"108":7.24,"109":68.98,"110":7.24,"111":68.98,"112":7.24,"113":68.98,"114":7.24,"115":7.24,"116":113.66,"117":7.24,"118":7.24,"119":7.24,"120":70.0,"121":60.81,"122":7.24,"123":35.43,"124":7.24,"125":7.24,"126":10.7,"127":34.39,"128":7.24,"129":60.81,"130":7.24,"131":7.24,"132":34.39,"133":34.39,"134":7.24,"135":72.29,"136":7.24,"137":7.24,"138":12.88,"139":72.29,"140":34.39,"141":7.24,"142":18.79,"143":34.39,"144":7.24,"145":19.19,"146":7.24,"147":7.24,"148":7.24,"149":10.73,"150":7.24,"151":7.24,"152":7.24,"153":10.73,"154":10.73,"155":10.73,"156":10.73,"157":72.0,"158":10.73,"159":10.73,"160":72.0,"161":19.19,"162":10.73,"163":10.73,"164":10.73,"165":12.88,"166":10.73,"167":10.73,"168":10.73,"169":7.24,"170":10.73,"171":7.24,"172":46.7,"173":46.7,"174":46.7,"175":68.98,"176":68.98,"177":36.1,"178":34.39,"179":7.24,"180":34.39,"181":68.98,"182":72.29,"183":8.99,"184":10.73,"185":7.24,"186":8.99,"187":8.99,"188":46.7,"189":8.99,"190":29.79,"191":8.99,"192":8.99,"193":8.99,"194":8.99,"195":46.7,"196":46.7,"197":7.24,"198":60.81,"199":68.98,"200":7.0,"201":34.0,"202":10.73,"203":24.15,"204":68.98,"205":12.88,"206":46.7,"207":35.43,"208":24.15,"209":46.7,"210":12.91,"211":72.29,"212":72.29,"213":46.7,"214":10.73,"215":7.24,"216":7.24,"217":72.29,"218":46.7,"219":19.19,"220":46.7,"221":46.7,"222":46.7,"223":46.7,"224":24.15,"225":10.86,"226":7.24,"227":34.39,"228":10.73,"229":46.7,"230":7.24,"231":11.0,"232":34.39,"233":46.7,"234":7.0,"235":7.24,"236":46.7,"237":10.73,"238":7.24,"239":46.7,"240":24.15,"241":67.42,"242":12.88,"243":46.7,"244":12.88,"245":10.73,"246":34.39,"247":34.0,"248":10.73,"249":7.24,"250":7.24,"251":7.24,"252":46.7,"253":7.24,"254":34.39,"255":46.7,"256":72.0,"257":7.24,"258":10.73,"259":12.88,"260":67.42,"261":67.0,"262":7.24,"263":67.42,"264":34.39,"265":7.24,"266":34.39,"267":7.24,"268":10.73,"269":34.0,"270":103.2,"271":7.24,"272":7.24,"273":12.88,"274":7.24,"275":7.24,"276":12.88,"277":67.0,"278":14.22,"279":7.24,"280":24.15,"281":67.42,"282":19.86,"283":10.86,"284":10.86},"Hub \u2764\ufe0f":{"0":29,"1":2,"2":2,"3":10,"4":4,"5":3,"6":430,"7":19,"8":67,"9":9,"10":19,"11":2,"12":27,"13":46,"14":2,"15":14,"16":7,"17":12,"18":32,"19":50,"20":4,"21":3,"22":11,"23":9,"24":0,"25":8,"26":7,"27":4,"28":0,"29":1,"30":71,"31":19,"32":2,"33":71,"34":0,"35":0,"36":0,"37":7,"38":0,"39":0,"40":2,"41":1,"42":1,"43":1,"44":0,"45":16,"46":2,"47":1,"48":0,"49":0,"50":9,"51":4,"52":0,"53":0,"54":4,"55":0,"56":2,"57":0,"58":0,"59":26,"60":0,"61":0,"62":0,"63":1,"64":3,"65":9,"66":0,"67":0,"68":0,"69":11,"70":6,"71":4,"72":8,"73":0,"74":6,"75":7,"76":0,"77":0,"78":1,"79":8,"80":8,"81":0,"82":9,"83":1,"84":35,"85":0,"86":1,"87":14,"88":3,"89":0,"90":0,"91":2,"92":0,"93":1,"94":11,"95":1,"96":11,"97":1,"98":11,"99":0,"100":5,"101":134,"102":1,"103":9,"104":0,"105":2,"106":9,"107":21,"108":0,"109":134,"110":1,"111":22,"112":3,"113":19,"114":1,"115":1,"116":3,"117":1,"118":4,"119":2,"120":16,"121":15,"122":0,"123":0,"124":0,"125":5,"126":1,"127":12,"128":33,"129":2,"130":2,"131":0,"132":35,"133":87,"134":85,"135":86,"136":1,"137":2,"138":1,"139":86,"140":87,"141":6,"142":2,"143":1,"144":0,"145":1,"146":0,"147":6,"148":0,"149":4,"150":2,"151":6,"152":8,"153":44,"154":554,"155":16,"156":1,"157":69,"158":0,"159":3,"160":69,"161":0,"162":2,"163":16,"164":5,"165":3,"166":1,"167":14,"168":1,"169":31,"170":2,"171":2,"172":1,"173":4,"174":8,"175":16,"176":142,"177":5,"178":215,"179":6,"180":1,"181":0,"182":314,"183":0,"184":1,"185":0,"186":1,"187":0,"188":1,"189":5,"190":1,"191":0,"192":1,"193":1,"194":0,"195":0,"196":320,"197":3,"198":0,"199":10,"200":0,"201":111,"202":7,"203":2,"204":10,"205":7,"206":320,"207":0,"208":1,"209":161,"210":7,"211":44,"212":0,"213":19,"214":10,"215":0,"216":1,"217":0,"218":0,"219":0,"220":0,"221":19,"222":12,"223":3536,"224":3,"225":0,"226":2,"227":1,"228":73,"229":3536,"230":1,"231":9,"232":5,"233":20,"234":6,"235":54,"236":0,"237":73,"238":54,"239":6,"240":76,"241":10,"242":1,"243":0,"244":0,"245":2,"246":5,"247":5,"248":32,"249":5,"250":0,"251":4,"252":0,"253":1,"254":3,"255":55,"256":73,"257":150,"258":0,"259":0,"260":1,"261":157,"262":3,"263":1,"264":32,"265":2,"266":10,"267":0,"268":0,"269":5,"270":2,"271":2,"272":0,"273":6,"274":1,"275":3,"276":0,"277":157,"278":2,"279":1,"280":9,"281":1,"282":0,"283":14,"284":14},"Model sha":{"0":"fda5cf998a0f2d89b53b5fa490793e3e50bb8239","1":"ea2b4ff8e5acd7a48993f56b2d7b99e049eb6939","2":"ea2b4ff8e5acd7a48993f56b2d7b99e049eb6939","3":"40d451f32b1a6c9ad694b32ba8ed4822c27f3022","4":"1f302e0e15f3d3711778cd61686eb9b28b0c72ae","5":"84d38e29fec0dc9c274237968fdafe9396702f9b","6":"54a8c35600ec5cb30ca2129247854ece23e57f57","7":"4df251a558c53b6b6a4c459045b161951cfc3c4e","8":"c64edea08b27be1e7e2ae6a95bcdd74849cb887e","9":"cd29cfa124072c96ba8601230bead65d76e04dcb","10":"ba3403eaafc6d1f6e3a73245314ee96025c08d96","11":"e8e558b5fd4ac9da839577b1295d10ca75fc2663","12":"2d8cff968dbfb31e0c1ccc42053ccc4d2698a390","13":"915651208ea9f40c65a60d1f971a09f9461ee691","14":"7dd3ddea090bd63f3143e70d7d6237cc40c046e4","15":"e1cdc5b02c662c5f29a50d0b22c64a8902ca856b","16":"6c7ec6d2ca1c0d126a26963fedc9bbdf5210b0d1","17":"dc092ecc5d5a424678eac445a9f4443069776691","18":"76389d5d825c3743cc70bc75b902bbfdad11beba","19":"7b74a95019f01b59630cbd6469814c752d0e59e5","20":"097b951c2524e6113252fcd98ba5830c85dc450f","21":"95b3b4e432d98b804d64cfe42dd9fa6b67198e5b","22":"3880710724abcaffbdf8fa4031e1d02066fbfe9d","23":"74c6e4fbd272c9d897e8c93ee7de8a234f61900f","24":"96c62ad90f2b82016a1cdbfe96cfa5c4bb278e21","25":"c5575550053c84a401baf56174cb2e5d5bd9e79a","26":"d3efc551679d7ec00da14722d44151c948a48d25","27":"d8d6a47f877fee3e638a158c2bd637c0013ed4e4","28":"06fd0e293aeb3b2722e3910daefcd185fad4558c","29":"331bb6bdba4140bbf0031bd37076f2c8a76d7dbb","30":"bbaef291e93a7f6c9f8cb76a4dbd8c3c054d3f3c","31":"a4ca706d1bbc263b95e223a80ad68b0f125840b3","32":"f136ec75c9fb7c86c071291ddf418089c8f43da0","33":"bbaef291e93a7f6c9f8cb76a4dbd8c3c054d3f3c","34":"cd8bfad664fb7f9b017388d974dd3265f8c40396","35":"ff261dadc107d0ce67b836a052d7131f9d9e4260","36":"5efde29924cf7158e4cbd642311a92a14e85597c","37":"a283f4e8169009d683b329ae1a96c9a77ce5936a","38":"cb04e33c4ff559b31767765100cd50c24ec2531c","39":"1dc4edde961960f7263dc3bdd37ca9e9f7e451ea","40":"03405145ca06170f1b2e0acc838f573f0e090df8","41":"0da1865ae1ce682d4002dd9935d20520e79ed520","42":"a27e0dfaf79af8da32fc4ff6c5eb8be46c9f5a13","43":"a27e0dfaf79af8da32fc4ff6c5eb8be46c9f5a13","44":"b7f5aa8d4c899c175a1dad40a03b4071df90bd8e","45":"69b9280ee4d2a20ef5645798621e62dd9777c139","46":"4774173a54be9a648e1cf03248af3ae3d51a0434","47":"11a51df04f85047e166d63eb64cedc1ec02732a1","48":"ff261dadc107d0ce67b836a052d7131f9d9e4260","49":"1e1cd6e84d02a9c1d70c2a2037f485bc2b646391","50":"22a9da7289d20a1d5452f77aa5bc49e97344af52","51":"cd343f0846ceb4180297920b2da50d6b28dcb242","52":"6ba7b5acb65dd62c28585cba298e0d3671c14f3a","53":"2b81b03b242a548e54e9e10af6a4c24f24a4c5fc","54":"f92057866ff68bf215487d34ca1080707bb4e98c","55":"c00b0fa78ab41aec778209fdf7640ebbe6d83065","56":"3d0181b920304bca0bdfd41aff55188a574c85e3","57":"b1ec306bf85762b28ce29ac71924bb9a8fa01e5a","58":"af1576f357ce8c5c3ee2e8bda45f8ffd7e0535f0","59":"25c0f5c1edad0ed1ab02347adf02fe03e0a3b62a","60":"546cdd443abc56b48aaadb4ebb5fb9249015f0bb","61":"aa6e42036cea01cb99426a9333481b353fd36e61","62":"dd1a314a04b8b4faf33e7d5037a71246d3e65bad","63":"ec27a21a66dc4411f24f36d585787853ba2e6354","64":"d718acb1b95c85009db8dd34af1318bcaf23ebcd","65":"aa3528c04c38fa49b5b65e1d064c46db3e9774f1","66":"ff89febead2585b2a1efae12b53887b18c283a8a","67":"87382cefca257137b983fd01d0e6a8839704d75e","68":"d696e99a2a4eeb13994c277f2fb113e9ddd1e632","69":"a2c3a87dd53a87dc9fc622ce4ddbb05d3e9cf6a9","70":"a0d648c1bcc3f1615bb2f0a94c6d32e7abde355d","71":"099b9c3e105fbb579d561fe93174ae3bd75dac8d","72":"5c649b6bbb8aa16d52dda26c5ce8574d1c7a3274","73":"0c9f2823a900408cf3c70c532288f89e452067f7","74":"7637cbf40c746030910154e0b344c5358f35a878","75":"7e26287665e6214be131f4e7ee20a312a07a4c1c","76":"bfba5a5114005c849a49662b4c7e53debac98105","77":"b7174ccf5c91095737cdb29f50853512017a1ac4","78":"c9beb3cba8030cb4fe7d96dd513c9e7ab40da126","79":"bff2cd7ba1f8a742cd22cd9df22485636c3b6410","80":"783a2f1231542b9fe8bc728dc676745c62f35b9f","81":"67f6f5ea337547b3f5e287e0ed1392ef0462e65a","82":"5b806671b663295f5212704dfb7373ddfefe804f","83":"36a9851b8c9213c4e1bcfd2c46b3f799c36caa69","84":"a619fd0fcbdfcc897054491c2f285677bee38a11","85":"46afad714b0528863bcf67b2bf5fcd4318235ccf","86":"a5965f77bbb0fe23f16a5137918af27c753800af","87":"30e44c452e38ff3d879d7ba92a130fa2cc072754","88":"34e3f31067be2bcbf86c8af9d137db227b2ece20","89":"c241960b69943b3d32b8af110bbed20508265334","90":"ff7d668721b961a73a95098cf7436db0170b1db6","91":"1a8a1ce0ceea0e298d9c8d5cce0b869a4a8c0514","92":"2491f12e51d7b74fb47ef5480d4b5f547d4d19ea","93":"cc18e2b0b9764f255341d3e530d018545987544b","94":"2f83b45077479bc3f663da50c4c40372894bf92e","95":"72f6e05eddabe6f3fa8891c99c4ba02aa60158c1","96":"98fdc8315906b0a8b9e7f24bad89914869fcfc20","97":"bd9ac169a0d6acb8fb66d55a6471ef162271b248","98":"583254a5a134243d7793b311c465da12b10a3ff2","99":"a89b5a4ce482c531b1cb3b8703e8eb2b9321994c","100":"ead4b4aedf94b98916f30388b85620a3583375e8","101":"cf06159aaaadda2ca50b19ce547a52424f7d47c3","102":"e02a631564990af3d9c8b0232f979af11cd8b6f6","103":"d6024b97f624e9169a63f5faccb8c5ab121eb13a","104":"ea5855b529987fde6eca87492bccbd28eef8d052","105":"fbf1c9958c47062e2db30276c723867c0d019652","106":"644f20245c08dbbc6baad20100fcf0c8bd3181a0","107":"e01fb197b4303ba63ba2f4d68a897006ec7ec4fd","108":"ebc7cba80494385e29bce8b1b86a75d14666c19e","109":"cf06159aaaadda2ca50b19ce547a52424f7d47c3","110":"1442ca4e728892f18ef101c4987bdf11ef5bbae5","111":"c8ad8ee000e4e042d80e4cf53fb6d0815d7743dd","112":"50dd207ce4319397d862a91f8295d902549dbdf7","113":"031e9404b7a1467fdcc96bc109e05b640d573209","114":"a892e9a26785d59d8bf4ccef48606664c6cbc48b","115":"2d197f7a290d191183b86f35c3857dd15a16d9b6","116":"513311818a707ccc0c7d007ddabfab19e1a2e470","117":"2d197f7a290d191183b86f35c3857dd15a16d9b6","118":"e13a48ef1524ba35615d7f63834e7c9192fa1836","119":"5ecc835f4137adac99198831c61c2afff4f340cf","120":"0dc1f9340fac9aadf883f52e6409e49e8d286af6","121":"d187b7bd6757d78bf89aaad8b0b5834ddbf29392","122":"2e36c528223443d6b8b5203b6a013e79f6d78d09","123":"95d031f0cad065bc18387f09ce37b256756f762f","124":"8a741a32e8d1d426c408c3eeb208eccc172c655e","125":"16e9d47cb25c33d57328638e5c56e257c6021ce1","126":"543b52f9b6c96a4922dc8ed1251625b1bd919e19","127":"0c670c988b61240e5f89ae9df0820db7dc572576","128":"c3227c2b48ac6b136c074871b72088677f2adca9","129":"ccd128942c5a6bb1672ceed21730d0e172655d77","130":"4c09542d0154eb09bf7be874e2c68189407114ed","131":"207cddf327154c23b484f1cbd972b3c7989b7554","132":"08903c93d929829aabbde2681c7ad2465d7d4189","133":"fcc6ada5ea6dbf2f644d26b545ac402d2202cc74","134":"6df7bb2069432bcab0971ab105284a66b3ec1ce0","135":"66bf25995056155b5d0796f7c0981e243bdd48f3","136":"448255ff2397e04c62ecba4c4d982531eb42d241","137":"f1d27aab09086a6e691db6892d50ba809cbe0607","138":"4bfad083e96a4ab129cc202fc941994be2e3adc4","139":"e5dd511955f4ac65bb1884f07426157740ad8574","140":"fcc6ada5ea6dbf2f644d26b545ac402d2202cc74","141":"645fa936256811f53f0c33f1e5298f6ad1095dce","142":"96b27c4205881920289b29ac3d83ba5edf5cf672","143":"1d697d32ba4f6ed471cd2857669029f425b827bb","144":"6bf661cdade79d96c4def4f09c27ad5ca1bae11a","145":"de574b57d45cfea00748c464af17f1c1ca53e548","146":"43ea8d27d652dc15e4d27f665c5d636a5937780b","147":"750463cf0946dd46c4504b302757f2bb6e2b4521","148":"78da696445e50002d29bf5610af059fd3f00f51b","149":"02a497125bbf85fe0355eb22424315c920d1aec4","150":"0d04b46ec6ce4c707bcdebb94b98e30fe8f4ae1d","151":"9c95dafc63de0e98627458369e87347df87fa17d","152":"875319a815400bdb73c309601c175d72997a4fa0","153":"8b9615124a0bcadd7fa984eaadd066da0fb4fbae","154":"d3167df97a44b8632538b32ee8cd887893ea1435","155":"08d3f07da7160e9657630ba98531850905619def","156":"669f8f726fac4a588ced06a4da3959eb8ca20f9f","157":"8761e9acb20bc475c095455fd754bf632e0f88f0","158":"c2519bf48d73f5751cfecfe2c4c796fbcb73c390","159":"31bbd3c348400c942a33c1f952dca8e7125996b7","160":"8761e9acb20bc475c095455fd754bf632e0f88f0","161":"0a25a243957b41c7ac8d59af50294547151ae621","162":"c03dfcda5d45ea4c518bd14641d9604726e00477","163":"c63d06344214886094d7ab6c7fd5692cc59fdf0d","164":"b47d17b0df02e38e97f565784bb3cf948b29a6ec","165":"514783cefac2b142adb50ee5f61dd724d62910cf","166":"0cce8842b179e19e6faac936a8c44ea1ba05b6b9","167":"d933dcd7cbb19916f4732ae7e3892a656a8c3d27","168":"df83494a4366e081563659e1142464029a0dec82","169":"72084679bda2e7679259e9c0fa2fdcd48ecb158c","170":"b59ed47a8f30e7488f1faef65ff0a75597af0a44","171":"f2a7ecb1f539bb41a61c254150e404820851005f","172":"73b5302f1efc7ba87e123cfed0c9c998e098c16a","173":"8483318133a7763eb2dedc59294559febbf657c9","174":"db160d4bc5bd9f2e66a764aeb44dcd18fb8afa6d","175":"e4b4ee3d952b1e8360a82d2b3506fd5b4ab68df9","176":"0ab5c875f0070d5aee8d36bc55f41de440a13f02","177":"479d3907a5bce4f3edb476d3ae05fe4b38a0a6e4","178":"deb99d98742ec9691ef593418bea71a4437745a1","179":"efb6caff9804383600563a658ba18720ec3b2d11","180":"dc7dfbece1b31665b0456476f67ef97a17bd2323","181":"515d7d948b4274c7451fdef61eae9e76eac93a38","182":"f62c59844a8de3c27cf22735218d77e9fa9f6b17","183":"fff356f1e506e6801c5a60c165636e84a4bd302c","184":"0219ef0ce5c8aaa6abe5e6c30f287edb777c7e8c","185":"45b93bfc4297b0bc1ef0b7316cbae11d2bb527d1","186":"b79854d1c29b5caae403c29d484f969b31734a5e","187":"e17d301fb143b20ac943c99f34aa8b118f14e1e0","188":"9311a4300f61f4cba381ba8347b73f0f2977a8f9","189":"f4bfa8b298cbd0acc236117231d5b00de5f43240","190":"323fba03ac21b03df8d04ab575741429cc509d7b","191":"a3798e202aaa326b1027c0ee0a61ac78dc175e63","192":"aedfd66841e39a8db181d8549a42f4d2ee248b0a","193":"689dbca3e4bd977fa08b7a933e4e709277cd1394","194":"fa406117c67fc86cc8171f57b12184eecb8069be","195":"0121c0f6d769e8c0ecafeae0e85092855a4e95c9","196":"566cdea53950f86eb51dae62812c29e79405cffe","197":"755d702c80e5acee8c07676b4a4dee37de56e2a8","198":"af9757f0420e27e2a332cc16cbe1eeefe99cb5f1","199":"6188e34517a82298b0216c141ec728a5d9861658","200":"2dda3515c5bbf02824addbe2e8f924a48ce21156","201":"01f1a7861667c4869bb03251dfd10526bf846e9c","202":"94bad5a6b469d84f556d6cc52c44fd88c07476f3","203":"d26e345f256b8a8210637258a5973fd36227d8ec","204":"2d5b9af81408ebc5e45c944cc24c9bab85b7ae1f","205":"9f9e4ae1c294ea4301eeefd3cf6222d156916144","206":"6ba531f1aec62375bf94ad9c7bb064953c4e9868","207":"6ee0b7c59743c3047f307643c7c1f13ada56fdd1","208":"b2535b271d83f892de2fb3a790b298618565dcff","209":"1f8562051647d5537dc950315e74534b363a0812","210":"9cb2d09228ac87d761d23a1284c79b55f9f285d9","211":"cc2f19f5bc9ad693d4447e42e9844d9931ab8e81","212":"5e194e1e44c6c2ebe294f854733f5c5532de5688","213":"330eb185920d6a470b265a4b31217c60e810fb3e","214":"cb9c9b0fb1d49b085069617bd8dc9cdddfdba7fb","215":"9893dc24b32bc83ca63e7d06cfa296d66be3fb3d","216":"cca19971f846c6d45e089dd1425f86fa4cb48f0a","217":"5cf11f6e983a7c11b17c1b7c4aee6ff99e30ba82","218":"a4400d021e29279c8676d5c46cf76c4b36d748f6","219":"a388bd7af444f632e5e9370bedaeb69572f861af","220":"cbf9c2350f24d9d10ebb1961965e7fbb4361cafb","221":"330eb185920d6a470b265a4b31217c60e810fb3e","222":"86fd0b7d132126be49c02e061ebec02e1d3a4e38","223":"125c431e2ff41a156b9f9076f744d2f35dd6e67a","224":"c9fb38c846ba1f1ce9a7a3560e491ea9d4a8d875","225":"618e5aedf02d58358d6fda7d9fa67c169b7156d0","226":"96e7c78544d7eca96e3ae60ff80c728f3109e8ba","227":"05a9ef37686d678f267a15664b5ce66612b7996a","228":"afc90bd0690d0cbedd01f22d1d6ef0e44f30b5f4","229":"3de0408ae8b591d9ac516a2384925dd98ebc66f4","230":"adf7c513e9cadbe25cc2be61c43f3f36f1b488e9","231":"7bd8487fc3a5c3bac022bfe8c34d2f630c123d40","232":"1b3a5c98381f37a2ec97ce80d1d88d472a7d1802","233":"61822ea65b8a4c56d2b5622e2adf69e430fac29a","234":"e91dcc46d28fc0aa5553fb73c4eac5e28abfd3ec","235":"f55aef05f6632a1407fcddcbc6729613b07e87e2","236":"e593de223b662cfda40aa96163c6a42d6b32de5e","237":"afc90bd0690d0cbedd01f22d1d6ef0e44f30b5f4","238":"d7d33a1517c57b596162a71a48bc29c87d29d9aa","239":"5dbc14842c16f1fa315e682e7e5bdb0248a2b05e","240":"91e0a33fd2cb0a77401831e96536b91c5b7817e4","241":"c3caef28f8402d52d6a646a7e1e00a971db1c507","242":"b7cd9398c8797b4e90cdd90ec9f64300e6334e6a","243":"5aeed89b3b0eba74cea863b59a43c63c81be5989","244":"382698efc4e5ff54a4155e1f2c40547ac3b2aa64","245":"786e6492919d0d1eb07b5988f67e0ee61aa05c21","246":"f7605af56f29b42e72f9c2cbbd4ad8e443a8dae0","247":"e2a033646231bd947a3948d3aac198d34d04ea38","248":"bff7146aafe1a5b84631bd279112c8c5b95d2802","249":"4e21eea3c32d00b2fcfc5bcfd16d8dc9d0d8874d","250":"9bfa05ff62ddd960cb9fb3e9dff70d800ea1c0a1","251":"02e11fa83d18975f95c5d5047d0439897308c73b","252":"dfc889db0d02cebaadacc6726a8622a40f45eb5e","253":"03955e2748064dcfac121e35e4e060cf6f48e259","254":"bf7696c10077e73d06752c564ea35cc7e5e336ca","255":"6011e2ef7791738f3b78fa9e122360029df7c9ed","256":"f16df07e24654858a6b04c3ecb0670dcfc42337d","257":"f4f3f6144dd143d6ec43ece9ab0fdd740ed610f1","258":"e402c5ea1ba23d776062f18306690296a708d469","259":"d9bb402315f47764bf0f6002e513cd7e89c7c804","260":"e4ba7abdb25b00308f67589458cb9380a2ccd5e6","261":"79648bef7658bb824e4630740f6e1484c1b0620b","262":"72267d131001da8cdf253105c367fd913db79523","263":"3120e204e1b4928fd784ae78fa754bc937352c98","264":"26923a2648b9864e2ec6f0cc66b8b6fcfbbdd491","265":"7e3fdb60969ef0f7219cbcb9b05f7d1537af1c8d","266":"66abec7cba89b35c7b6cab2140c3532049de0157","267":"04cd413008c353ca558ab901c0d88132c25772c2","268":"957474e32057f19ef863c1c8ba3d16389cf58eed","269":"d1bdf5a5ea942b8236e48c17c3c07e3bd49ae5c8","270":"59004f5610548e626ad27cd4a7b92daa3ccfc9c8","271":"5f18e24665f62b8e9a3492af247978073fea54f9","272":"17af425d904f21f8500bf965b16d07603e01d125","273":"e3d7c73110dd6edd9e96b1f3d9b0dea91d83ce2d","274":"e2164a4cce391e1f4228e2e89689793ec037135e","275":"fb53c42ba7d5719e730f67c5356766d84e5f3619","276":"1cf1b3a313de0b3b22a61dd3741c1bd5a3d14c66","277":"79648bef7658bb824e4630740f6e1484c1b0620b","278":"cf6f466d227c041df3b892dff394df43ecf99b8b","279":"dd27a200fd3dd5500a0b5bbfc0e4a9289af486e5","280":"2d6dcf8bf9f1a758f135929de4a6fd81e26a38da","281":"7152f2dc8e0aceb0412e802653271cd9e59bf23e","282":"855035b23011e2a09182025a63a9252e19033163","283":"1055563879363d9ee2fba1d9fd1628eca6bcbb4e","284":"c8741ec6f4f24324a96041efaf2f627a99d946e6"},"model_name_for_query":{"0":"davidkim205\/Rhea-72b-v0.5","1":"MTSAIR\/MultiVerse_70B","2":"MTSAIR\/MultiVerse_70B","3":"SF-Foundation\/Ein-72B-v0.11","4":"SF-Foundation\/Ein-72B-v0.13","5":"SF-Foundation\/Ein-72B-v0.12","6":"abacusai\/Smaug-72B-v0.1","7":"ibivibiv\/alpaca-dragon-72b-v1","8":"moreh\/MoMo-72B-lora-1.8.7-DPO","9":"cloudyu\/TomGrc_FusionNet_34Bx2_MoE_v0.1_DPO_f16","10":"saltlux\/luxia-21.4b-alignment-v1.0","11":"cloudyu\/TomGrc_FusionNet_34Bx2_MoE_v0.1_full_linear_DPO","12":"zhengr\/MixTAO-7Bx2-MoE-v8.1","13":"yunconglong\/Truthful_DPO_TomGrc_FusionNet_7Bx2_MoE_13B","14":"JaeyeonKang\/CCK_Asura_v1","15":"fblgit\/UNA-SimpleSmaug-34b-v1beta","16":"TomGrc\/FusionNet_34Bx2_MoE_v0.1","17":"migtissera\/Tess-72B-v1.5b","18":"moreh\/MoMo-72B-lora-1.8.6-DPO","19":"abacusai\/Smaug-34B-v0.1","20":"cloudyu\/Truthful_DPO_TomGrc_FusionNet_34Bx2_MoE","21":"ibivibiv\/orthorus-125b-v2","22":"ConvexAI\/Luminex-34B-v0.2","23":"yunconglong\/DARE_TIES_13B","24":"yunconglong\/13B_MATH_DPO","25":"TomGrc\/FusionNet_34Bx2_MoE","26":"ConvexAI\/Luminex-34B-v0.1","27":"yunconglong\/MoE_13B_DPO","28":"JaeyeonKang\/CCK_Asura_v3.0","29":"cloudyu\/4bit_quant_TomGrc_FusionNet_34Bx2_MoE_v0.1_DPO","30":"yam-peleg\/Experiment26-7B","31":"MTSAIR\/multi_verse_model","32":"chihoonlee10\/T3Q-Mistral-Orca-Math-DPO","33":"yam-peleg\/Experiment26-7B","34":"rwitz\/experiment26-truthy-iter-0","35":"yam-peleg\/Experiment30-7B","36":"yam-peleg\/Experiment28-7B","37":"MaziyarPanahi\/Calme-7B-Instruct-v0.2","38":"rwitz\/experiment26-truthy-iter-1","39":"rwitz\/experiment26-truthy-iter-2","40":"chlee10\/T3Q-Merge-Mistral7B","41":"LeroyDyer\/Mixtral_AI_Cyber_3.m1","42":"yam-peleg\/Experiment31-7B","43":"yam-peleg\/Experiment31-7B","44":"yam-peleg\/Experiment24-7B","45":"zhengr\/MixTAO-7Bx2-MoE-Instruct-v7.0","46":"bobofrut\/ladybird-base-7B-v8","47":"yam-peleg\/Experiment29-7B","48":"yam-peleg\/Experiment30-7B","49":"CorticalStack\/pastiche-crown-clown-7b-dare-dpo","50":"MaziyarPanahi\/Calme-7B-Instruct-v0.1.1","51":"mlabonne\/UltraMerge-7B","52":"cloudyu\/Truthful_DPO_cloudyu_Mixtral_34Bx2_MoE_60B","53":"yam-peleg\/Experiment27-7B","54":"eren23\/ogno-monarch-jaskier-merge-7b-OH-PREF-DPO","55":"eren23\/ogno-monarch-jaskier-merge-7b-OH-PREF-DPO-v2","56":"cloudyu\/Yi-34Bx2-MoE-60B-DPO","57":"chihoonlee10\/T3Q-EN-DPO-Mistral-7B","58":"jefferylovely\/AiMaven-Merkaba-7b","59":"bardsai\/jaskier-7b-dpo-v5.6","60":"JaeyeonKang\/CCK_Asura_v2.1","61":"yam-peleg\/Experiment25-7B","62":"eren23\/ogno-monarch-jaskier-merge-7b-OH-PREF-DPO-v3","63":"CorticalStack\/neurotic-crown-clown-7b-tak-stack-dpo","64":"jan-hq\/stealth-v2","65":"bardsai\/jaskier-7b-dpo-v6.1","66":"eren23\/ogno-monarch-jaskier-merge-7b-OH-PREF-DPO-v4-test","67":"chihoonlee10\/T3Q-DPO-Mistral-7B","68":"Kukedlc\/Jupiter-k-7B-slerp","69":"moreh\/MoMo-72B-lora-1.8.4-DPO","70":"TomGrc\/FusionNet_7Bx2_MoE_v0.1","71":"macadeliccc\/MBX-7B-v3-DPO","72":"vicgalle\/CarbonBeagle-11B-truthy","73":"dddsaty\/FusionNet_7Bx2_MoE_Ko_DPO_Adapter_Attach","74":"vicgalle\/RoleBeagle-11B","75":"MaziyarPanahi\/Calme-7B-Instruct-v0.5","76":"FelixChao\/Capricorn-7B-DPO","77":"rwitz\/experiment26-SPIN-iter-0","78":"Kukedlc\/NeuralKrishna-7B-V2-DPO","79":"abideen\/AlphaMonarch-laser","80":"touqir\/Cyrax-7B","81":"Eric111\/UltraCatunaMayo-DPO","82":"abideen\/AlphaMonarch-daser","83":"yam-peleg\/Experiment21-7B","84":"TomGrc\/FusionNet_7Bx2_MoE_14B","85":"yam-peleg\/Experiment22-7B","86":"cloudyu\/Yi-34Bx2-MOE-200K","87":"NeverSleep\/MiquMaid-v2-2x70B-DPO","88":"abideen\/AlphaMonarch-dora","89":"AiMavenAi\/Prometheus-1.3","90":"FelixChao\/Capricorn-7B","91":"yleo\/EmertonMonarch-7B","92":"yam-peleg\/Experiment20-7B","93":"ibivibiv\/multimaster-7b-v6","94":"abacusai\/Smaug-Mixtral-v0.1","95":"daxiongshu\/Pluto_24B_DPO_63","96":"abacusai\/Smaug-Mixtral-v0.1","97":"cloudyu\/Phoenix_DPO_60B","98":"Weyaxi\/Helion-4x34B","99":"ibivibiv\/multimaster-7b-v4","100":"fblgit\/UNA-34BeagleSimpleMath-32K-v1","101":"ShinojiResearch\/Senku-70B-Full","102":"fblgit\/UNA-34Beagles-32K-v1","103":"one-man-army\/UNA-34Beagles-32K-bf16-v1","104":"FelixChao\/Scorpio-7B","105":"vicgalle\/ConfigurableBeagle-11B","106":"Weyaxi\/Cosmosis-3x34B","107":"macadeliccc\/WestLake-7B-v2-laser-truthy-dpo","108":"yam-peleg\/Experiment19-7B","109":"ShinojiResearch\/Senku-70B-Full","110":"yam-peleg\/Experiment23-7B","111":"maywell\/kiqu-70b","112":"FelixChao\/WestSeverus-7B-DPO-v2","113":"migtissera\/Tess-70B-v1.6","114":"alnrg2arg\/test3_sft_16bit","115":"FelixChao\/Faraday-7B","116":"Weyaxi\/Astralis-4x34B","117":"FelixChao\/Faraday-7B","118":"PetroGPT\/WestSeverus-7B-DPO","119":"FelixChao\/Sectumsempra-7B-DPO","120":"NeverSleep\/MiquMaid-v1-70B","121":"Weyaxi\/Bagel-Hermes-2x34B","122":"BarryFutureman\/WestLakeX-7B-EvoMerge-Variant2","123":"ibivibiv\/multimaster-7b-v5","124":"alnrg2arg\/test3_sft_16bit_dpo2","125":"ChaoticNeutrals\/Eris_Floramix_DPO_7B","126":"abhishekchohan\/SOLAR-10.7B-Instruct-Forest-DPO-v1","127":"abacusai\/MetaMath-Bagel-DPO-34B","128":"cognitivecomputations\/WestLake-7B-v2-laser","129":"cloudyu\/60B_MoE_Coder_v3","130":"ChaoticNeutrals\/Eris_Remix_DPO_7B","131":"Eric111\/CatunaLaserPi-DPO","132":"jondurbin\/nontoxic-bagel-34b-v0.2","133":"jondurbin\/bagel-dpo-34b-v0.2","134":"senseable\/WestLake-7B-v2","135":"moreh\/MoMo-72B-LoRA-V1.4","136":"MaziyarPanahi\/Calme-7B-Instruct-v0.4","137":"kevin009\/llamaRAGdrama","138":"vicgalle\/Mixtral-7Bx2-truthy","139":"moreh\/MoMo-72B-LoRA-V1.4","140":"jondurbin\/bagel-dpo-34b-v0.2","141":"senseable\/Westlake-7B","142":"rizla\/trrapi-16b","143":"abacusai\/MM-OV-bagel-DPO-34b-c1000-250","144":"BarryFutureman\/WestLakeX-7B-EvoMerge","145":"yunconglong\/Truthful_DPO_MOE_19B","146":"Kukedlc\/NeuralExperiment-7b-MagicCoder-v7.5","147":"ResplendentAI\/Datura_7B","148":"FelixChao\/Patronum-7B","149":"bhavinjawade\/SOLAR-10B-OrcaDPO-Jawade","150":"ResplendentAI\/Flora_DPO_7B","151":"ResplendentAI\/Flora_7B","152":"NeuralNovel\/Valor-7B-v0.1","153":"VAGOsolutions\/SauerkrautLM-SOLAR-Instruct","154":"upstage\/SOLAR-10.7B-Instruct-v1.0","155":"fblgit\/UNA-SOLAR-10.7B-Instruct-v1.0","156":"bhavinjawade\/SOLAR-10B-Nector-DPO-Jawade","157":"abacusai\/Liberated-Qwen1.5-72B","158":"dddsaty\/SOLAR-Instruct-ko-Adapter-Attach","159":"macadeliccc\/SOLAR-10.7b-Instruct-truthy-dpo","160":"abacusai\/Liberated-Qwen1.5-72B","161":"cloudyu\/19B_MATH_DPO","162":"dhanushreddy29\/BrokenKeyboard","163":"fblgit\/UNA-SOLAR-10.7B-Instruct-v1.0","164":"fblgit\/UNA-POLAR-10.7B-InstructMath-v2","165":"fblgit\/UNAversal-2x7B-v1","166":"dddsaty\/Merge_Sakura_Solar","167":"rishiraj\/meow","168":"vicgalle\/ConfigurableSOLAR-10.7B","169":"fblgit\/UNA-TheBeagle-7b-v1","170":"vicgalle\/OpenBeagle-11B","171":"InferenceIllusionist\/Excalibur-7b-DPO","172":"JaeyeonKang\/CCK_Gony_v3","173":"Sao10K\/Typhon-Mixtral-v1","174":"fblgit\/UNAversal-8x7B-v1beta","175":"sophosympatheia\/Aurora-Nights-70B-v1.0","176":"allenai\/tulu-2-dpo-70b","177":"Steelskull\/Lumosia-v2-MoE-4x10.7","178":"NousResearch\/Nous-Hermes-2-Yi-34B","179":"decruz07\/kellemar-DPO-Orca-Distilled-7B-SLERP","180":"abacusai\/MM-Orc-Vic-bagel-34b-c1000","181":"JaeyeonKang\/CCK_Asura_v2","182":"Qwen\/Qwen-72B","183":"yam-peleg\/Experiment7-7B","184":"macadeliccc\/SOLAR-10.7b-Instruct-dpo","185":"yam-peleg\/Experiment15-7B","186":"yam-peleg\/Experiment10-7B","187":"yam-peleg\/Experiment8-7B","188":"cloudyu\/Mixtral-8x7B-Instruct-v0.1-DPO","189":"Neuronovo\/neuronovo-9B-v0.4","190":"cloudyu\/Mixtral_7Bx5_MoE_30B","191":"yam-peleg\/Experiment9-7B","192":"yam-peleg\/Experiment1-7B","193":"yam-peleg\/Experiment2-7B","194":"yam-peleg\/Experiment4-7B","195":"Sao10K\/Franziska-Mixtral-v1","196":"NousResearch\/Nous-Hermes-2-Mixtral-8x7B-DPO","197":"ResplendentAI\/DaturaCookie_7B","198":"ibndias\/Nous-Hermes-2-MoE-2x34B","199":"gradientai\/v-alpha-tross","200":"carsenk\/flippa-exp26-v3-7b","201":"SUSTech\/SUS-Chat-34B","202":"Sao10K\/SOLAR-10.7B-NahIdWin","203":"yunconglong\/7Bx4_DPO","204":"gradientai\/v-alpha-tross","205":"ResplendentAI\/Luna-2x7B-MoE","206":"NousResearch\/Nous-Hermes-2-Mixtral-8x7B-DPO","207":"ibivibiv\/multimaster-7b-v3","208":"yunconglong\/7Bx4_DPO_2e","209":"argilla\/notux-8x7b-v1","210":"The-Face-Of-Goonery\/HuginnV5.5-12.6B","211":"Qwen\/Qwen1.5-72B","212":"logicker\/SkkuDS-DPO-72B-v1","213":"VAGOsolutions\/SauerkrautLM-Mixtral-8x7B-Instruct","214":"Himitsui\/Kaiju-11B","215":"PetroGPT\/Severus-7B-DPO","216":"S-miguel\/The-Trinity-Coder-7B","217":"logicker\/SkkuDS-DPO-72B-v3","218":"PSanni\/MPOMixtral-8x7B-Instruct-v0.1","219":"cloudyu\/19B_TRUTH_DPO","220":"JaeyeonKang\/CCK_Gony_v3.3","221":"VAGOsolutions\/SauerkrautLM-Mixtral-8x7B-Instruct","222":"tenyx\/TenyxChat-8x7B-v1","223":"mistralai\/Mixtral-8x7B-Instruct-v0.1","224":"ChaoticNeutrals\/RPMix-4x7B-MoE","225":"SJ-Donald\/SJ-SOLAR-10.7b-DPO","226":"senseable\/garten2-7b","227":"Azure99\/blossom-v5-34b","228":"Sao10K\/Fimbulvetr-11B-v2","229":"mistralai\/Mixtral-8x7B-Instruct-v0.1","230":"FelixChao\/Severus-7B","231":"Himitsui\/KuroMitsu-11B","232":"maywell\/PiVoT-SUS-RP","233":"jondurbin\/bagel-dpo-8x7b-v0.2","234":"ignos\/Mistral-T5-7B-v1","235":"SanjiWatsuki\/Kunoichi-DPO-v2-7B","236":"Brillibits\/Instruct_Mixtral-8x7B-v0.1_Dolly15K","237":"Sao10K\/Fimbulvetr-11B-v2","238":"SanjiWatsuki\/Kunoichi-DPO-v2-7B","239":"cognitivecomputations\/mixtral-instruct-0.1-laser","240":"cognitivecomputations\/laserxtral","241":"OpenBuddy\/openbuddy-deepseek-67b-v15.2","242":"macadeliccc\/piccolo-math-2x7b","243":"JaeyeonKang\/CCK_Gony_v0.1","244":"R136a1\/InfinityKuno-2x7B","245":"jan-ai\/Solar-10.7B-SLERP","246":"mncai\/yi-34B-v3","247":"NeverSleep\/CausalLM-RP-34B","248":"Sao10K\/Fimbulvetr-10.7B-v1","249":"SanjiWatsuki\/Kunoichi-DPO-7B","250":"jan-hq\/supermario-slerp-v3","251":"viethq188\/LeoScorpius-7B","252":"JaeyeonKang\/CCK_Gony_v3.1","253":"adonlee\/Mistral_7B_SFT_DPO_v0","254":"mncai\/yi-34B-v2","255":"NousResearch\/Nous-Hermes-2-Mixtral-8x7B-SFT","256":"CausalLM\/72B-preview-llamafied-qwen-llamafy","257":"OpenPipe\/mistral-ft-optimized-1218","258":"bn22\/Nous-Hermes-2-SOLAR-10.7B-MISALIGNED","259":"arlineka\/Brunhilde-2x7b-MOE-DPO-v.01.5","260":"OpenBuddy\/openbuddy-deepseek-67b-v18.1-4k","261":"deepseek-ai\/deepseek-llm-67b-chat","262":"mlabonne\/NeuralDarewin-7B","263":"OpenBuddy\/openbuddy-deepseek-67b-v15.1","264":"migtissera\/Tess-M-Creative-v1.0","265":"VitalContribution\/Evangelion-7B","266":"bhenrym14\/platypus-yi-34b","267":"RatanRohith\/NeuralPizza-7B-V0.3","268":"PracticeLLM\/SOLAR-tail-10.7B-Merge-v1.0","269":"Azure99\/blossom-v4-yi-34b","270":"llmixer\/BigWeave-v15-103b","271":"MaziyarPanahi\/Calme-7B-Instruct-v0.1","272":"Samee-ur\/NeuralPipe-7B-slerp-DPO","273":"VAGOsolutions\/SauerkrautLM-14b-MoE-LaserChat","274":"RatanRohith\/NeuralPizza-7B-V0.2","275":"RatanRohith\/NeuralPizza-7B-V0.1","276":"R136a1\/InfinityKumon-2x7B","277":"deepseek-ai\/deepseek-llm-67b-chat","278":"Sao10K\/14B-Glacier-Stack","279":"jan-hq\/supermario-slerp-v2","280":"dillfrescott\/amadeus-v0.1","281":"OpenBuddy\/openbuddy-deepseek-67b-v15.3-4k","282":"KnutJaegersberg\/Deita-20b","283":"LDCC\/LDCC-SOLAR-10.7B","284":"LDCC\/LDCC-SOLAR-10.7B"}} \ No newline at end of file diff --git a/demo/mini_leaderboard/requirements.txt b/demo/mini_leaderboard/requirements.txt new file mode 100644 index 0000000..fb6c7ed --- /dev/null +++ b/demo/mini_leaderboard/requirements.txt @@ -0,0 +1 @@ +pandas diff --git a/demo/mini_leaderboard/run.py b/demo/mini_leaderboard/run.py new file mode 100644 index 0000000..c625bbd --- /dev/null +++ b/demo/mini_leaderboard/run.py @@ -0,0 +1,237 @@ +# type: ignore +import gradio as gr +import pandas as pd +from pathlib import Path + +abs_path = Path(__file__).parent.absolute() + +df = pd.read_json(str(abs_path / "assets/leaderboard_data.json")) +invisible_df = df.copy() + +COLS = [ + "T", + "Model", + "Average ⬆️", + "ARC", + "HellaSwag", + "MMLU", + "TruthfulQA", + "Winogrande", + "GSM8K", + "Type", + "Architecture", + "Precision", + "Merged", + "Hub License", + "#Params (B)", + "Hub ❤️", + "Model sha", + "model_name_for_query", +] +ON_LOAD_COLS = [ + "T", + "Model", + "Average ⬆️", + "ARC", + "HellaSwag", + "MMLU", + "TruthfulQA", + "Winogrande", + "GSM8K", + "model_name_for_query", +] +TYPES = [ + "str", + "markdown", + "number", + "number", + "number", + "number", + "number", + "number", + "number", + "str", + "str", + "str", + "str", + "bool", + "str", + "number", + "number", + "bool", + "str", + "bool", + "bool", + "str", +] +NUMERIC_INTERVALS = { + "?": pd.Interval(-1, 0, closed="right"), + "~1.5": pd.Interval(0, 2, closed="right"), + "~3": pd.Interval(2, 4, closed="right"), + "~7": pd.Interval(4, 9, closed="right"), + "~13": pd.Interval(9, 20, closed="right"), + "~35": pd.Interval(20, 45, closed="right"), + "~60": pd.Interval(45, 70, closed="right"), + "70+": pd.Interval(70, 10000, closed="right"), +} +MODEL_TYPE = [str(s) for s in df["T"].unique()] +Precision = [str(s) for s in df["Precision"].unique()] + +# Searching and filtering +def update_table( + hidden_df: pd.DataFrame, + columns: list, + type_query: list, + precision_query: str, + size_query: list, + query: str, +): + filtered_df = filter_models(hidden_df, type_query, size_query, precision_query) # type: ignore + filtered_df = filter_queries(query, filtered_df) + df = select_columns(filtered_df, columns) + return df + +def search_table(df: pd.DataFrame, query: str) -> pd.DataFrame: + return df[(df["model_name_for_query"].str.contains(query, case=False))] # type: ignore + +def select_columns(df: pd.DataFrame, columns: list) -> pd.DataFrame: + # We use COLS to maintain sorting + filtered_df = df[[c for c in COLS if c in df.columns and c in columns]] + return filtered_df # type: ignore + +def filter_queries(query: str, filtered_df: pd.DataFrame) -> pd.DataFrame: + final_df = [] + if query != "": + queries = [q.strip() for q in query.split(";")] + for _q in queries: + _q = _q.strip() + if _q != "": + temp_filtered_df = search_table(filtered_df, _q) + if len(temp_filtered_df) > 0: + final_df.append(temp_filtered_df) + if len(final_df) > 0: + filtered_df = pd.concat(final_df) + filtered_df = filtered_df.drop_duplicates( # type: ignore + subset=["Model", "Precision", "Model sha"] + ) + + return filtered_df + +def filter_models( + df: pd.DataFrame, + type_query: list, + size_query: list, + precision_query: list, +) -> pd.DataFrame: + # Show all models + filtered_df = df + + type_emoji = [t[0] for t in type_query] + filtered_df = filtered_df.loc[df["T"].isin(type_emoji)] + filtered_df = filtered_df.loc[df["Precision"].isin(precision_query + ["None"])] + + numeric_interval = pd.IntervalIndex( + sorted([NUMERIC_INTERVALS[s] for s in size_query]) # type: ignore + ) + params_column = pd.to_numeric(df["#Params (B)"], errors="coerce") + mask = params_column.apply(lambda x: any(numeric_interval.contains(x))) # type: ignore + filtered_df = filtered_df.loc[mask] + + return filtered_df + +demo = gr.Blocks() +with demo: + gr.Markdown("""Test Space of the LLM Leaderboard""", elem_classes="markdown-text") + + with gr.Tabs(elem_classes="tab-buttons") as tabs: + with gr.TabItem("🏅 LLM Benchmark", elem_id="llm-benchmark-tab-table", id=0): + with gr.Row(): + with gr.Column(): + with gr.Row(): + search_bar = gr.Textbox( + placeholder=" 🔍 Search for your model (separate multiple queries with `;`) and press ENTER...", + show_label=False, + elem_id="search-bar", + ) + with gr.Row(): + shown_columns = gr.CheckboxGroup( + choices=COLS, + value=ON_LOAD_COLS, + label="Select columns to show", + elem_id="column-select", + interactive=True, + ) + with gr.Column(min_width=320): + filter_columns_type = gr.CheckboxGroup( + label="Model types", + choices=MODEL_TYPE, + value=MODEL_TYPE, + interactive=True, + elem_id="filter-columns-type", + ) + filter_columns_precision = gr.CheckboxGroup( + label="Precision", + choices=Precision, + value=Precision, + interactive=True, + elem_id="filter-columns-precision", + ) + filter_columns_size = gr.CheckboxGroup( + label="Model sizes (in billions of parameters)", + choices=list(NUMERIC_INTERVALS.keys()), + value=list(NUMERIC_INTERVALS.keys()), + interactive=True, + elem_id="filter-columns-size", + ) + + leaderboard_table = gr.components.Dataframe( + value=df[ON_LOAD_COLS], # type: ignore + headers=ON_LOAD_COLS, + datatype=TYPES, + elem_id="leaderboard-table", + interactive=False, + visible=True, + column_widths=["2%", "33%"], + ) + + # Dummy leaderboard for handling the case when the user uses backspace key + hidden_leaderboard_table_for_search = gr.components.Dataframe( + value=invisible_df[COLS], # type: ignore + headers=COLS, + datatype=TYPES, + visible=False, + ) + search_bar.submit( + update_table, + [ + hidden_leaderboard_table_for_search, + shown_columns, + filter_columns_type, + filter_columns_precision, + filter_columns_size, + search_bar, + ], + leaderboard_table, + ) + for selector in [ + shown_columns, + filter_columns_type, + filter_columns_precision, + filter_columns_size, + ]: + selector.change( + update_table, + [ + hidden_leaderboard_table_for_search, + shown_columns, + filter_columns_type, + filter_columns_precision, + filter_columns_size, + search_bar, + ], + leaderboard_table, + queue=True, + ) + +if __name__ == "__main__": + demo.queue(default_concurrency_limit=40).launch(css=str(abs_path / "assets/custom_css.css")) diff --git a/demo/model3D/run.py b/demo/model3D/run.py new file mode 100644 index 0000000..dbe261e --- /dev/null +++ b/demo/model3D/run.py @@ -0,0 +1,34 @@ +import gradio as gr +# get_model3d() returns the file path to sample 3D models included with Gradio +from gradio.media import get_model3d, MEDIA_ROOT + + +def load_mesh(mesh_file_name): + return mesh_file_name + + +demo = gr.Interface( + fn=load_mesh, + inputs=gr.Model3D(label="Other name", display_mode="wireframe"), + outputs=gr.Model3D( + clear_color=(0.0, 0.0, 0.0, 0.0), label="3D Model", display_mode="wireframe" + ), + examples=[ + [get_model3d("Bunny.obj")], + [get_model3d("Duck.glb")], + [get_model3d("Fox.gltf")], + [get_model3d("face.obj")], + [get_model3d("sofia.stl")], + [ + "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k-mini.splat" + ], + [ + "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/luigi/luigi.ply" + ], + ], + cache_examples=True, + api_name="predict", +) + +if __name__ == "__main__": + demo.launch(allowed_paths=[str(MEDIA_ROOT)]) diff --git a/demo/model3D_demo.zip b/demo/model3D_demo.zip new file mode 100644 index 0000000..7b18470 Binary files /dev/null and b/demo/model3D_demo.zip differ diff --git a/demo/model3d_component/run.py b/demo/model3d_component/run.py new file mode 100644 index 0000000..ceaa0aa --- /dev/null +++ b/demo/model3d_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Model3D() + +demo.launch() diff --git a/demo/model3d_component_events/run.py b/demo/model3d_component_events/run.py new file mode 100644 index 0000000..f49ed86 --- /dev/null +++ b/demo/model3d_component_events/run.py @@ -0,0 +1,19 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + input_3d = gr.Model3D(label="Input Model3D") + with gr.Column(): + output_3d = gr.Model3D(label="Output Model3D") + with gr.Column(): + num_change = gr.Number(label="# Change Events", value=0) + num_load = gr.Number(label="# Upload Events", value=0) + num_clear = gr.Number(label="# Clear Events", value=0) + clear_value = gr.Textbox(label="Clear Value", value="") + input_3d.upload(lambda s, n: (s, n + 1), [input_3d, num_load], [output_3d, num_load]) + input_3d.change(lambda n: n + 1, num_change, num_change) + input_3d.clear(lambda s, n: (s, n + 1), [input_3d, num_clear], [clear_value, num_clear]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/multimodaltextbox_component/run.py b/demo/multimodaltextbox_component/run.py new file mode 100644 index 0000000..b25febc --- /dev/null +++ b/demo/multimodaltextbox_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.MultimodalTextbox(interactive=True) + +demo.launch() diff --git a/demo/multipage/run.py b/demo/multipage/run.py new file mode 100644 index 0000000..7ce0008 --- /dev/null +++ b/demo/multipage/run.py @@ -0,0 +1,42 @@ +import gradio as gr +import random +import time + +with gr.Blocks() as demo: + name = gr.Textbox(label="Name") + output = gr.Textbox(label="Output Box") + greet_btn = gr.Button("Greet") + @gr.on([greet_btn.click, name.submit], inputs=name, outputs=output) + def greet(name): + return "Hello " + name + "!" + + @gr.render(inputs=name, triggers=[output.change]) + def spell_out(name): + with gr.Row(): + for letter in name: + gr.Textbox(letter) + +with demo.route("Up") as incrementer_demo: + num = gr.Number() + incrementer_demo.load(lambda: time.sleep(1) or random.randint(10, 40), None, num) + + with gr.Row(): + inc_btn = gr.Button("Increase") + dec_btn = gr.Button("Decrease") + inc_btn.click(fn=lambda x: x + 1, inputs=num, outputs=num, api_name="increment") + dec_btn.click(fn=lambda x: x - 1, inputs=num, outputs=num, api_name="decrement") + for i in range(100): + gr.Textbox() + +def wait(x): + time.sleep(2) + return x + +identity_iface = gr.Interface(wait, "image", "image", api_name="predict") + +with demo.route("Interface") as incrementer_demo: + identity_iface.render() + gr.Interface(lambda x, y: x * y, ["number", "number"], "number", api_name="predict") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/musical_instrument_identification/DESCRIPTION.md b/demo/musical_instrument_identification/DESCRIPTION.md new file mode 100644 index 0000000..99b199f --- /dev/null +++ b/demo/musical_instrument_identification/DESCRIPTION.md @@ -0,0 +1 @@ +This demo identifies musical instruments from an audio file. It uses Gradio's Audio and Label components. \ No newline at end of file diff --git a/demo/musical_instrument_identification/data_setups.py b/demo/musical_instrument_identification/data_setups.py new file mode 100644 index 0000000..7a9ad47 --- /dev/null +++ b/demo/musical_instrument_identification/data_setups.py @@ -0,0 +1,80 @@ +# Make function to find classes in target directory +import os +import librosa # type: ignore +import torch +import numpy as np +from torchaudio.transforms import Resample # type: ignore + +SAMPLE_RATE = 44100 +AUDIO_LEN = 2.90 + +# Parameters to control the MelSpec generation +N_MELS = 128 +F_MIN = 20 +F_MAX = 16000 +N_FFT = 1024 +HOP_LEN = 512 + +# Make function to find classes in target directory +def find_classes(directory: str): + # 1. Get the class names by scanning the target directory + classes = sorted(entry.name for entry in os.scandir(directory) if entry.is_dir()) + # 2. Raise an error if class names not found + if not classes: + raise FileNotFoundError(f"Couldn't find any classes in {directory}.") + # 3. Crearte a dictionary of index labels (computers prefer numerical rather than string labels) + class_to_idx = {cls_name: i for i, cls_name in enumerate(classes)} + return classes, class_to_idx + +def resample(wav, sample_rate, new_sample_rate): + if wav.shape[0] >= 2: + wav = torch.mean(wav, dim=0) + else: + wav = wav.squeeze(0) + if sample_rate > new_sample_rate: + resampler = Resample(sample_rate, new_sample_rate) + wav = resampler(wav) + return wav + +def mono_to_color(X, eps=1e-6, mean=None, std=None): + X = np.stack([X, X, X], axis=-1) + # Standardize + mean = mean or X.mean() + std = std or X.std() + X = (X - mean) / (std + eps) + # Normalize to [0, 255] + _min, _max = X.min(), X.max() + if (_max - _min) > eps: + V = np.clip(X, _min, _max) + V = 255 * (V - _min) / (_max - _min) + V = V.astype(np.uint8) + else: + V = np.zeros_like(X, dtype=np.uint8) + return V + +def normalize(image, mean=None, std=None): + image = image / 255.0 + if mean is not None and std is not None: + image = (image - mean) / std + return np.moveaxis(image, 2, 0).astype(np.float32) + +def compute_melspec(wav, sample_rate=SAMPLE_RATE): + melspec = librosa.feature.melspectrogram( + y=wav, + sr=sample_rate, + n_fft=N_FFT, + fmin=F_MIN, + fmax=F_MAX, + n_mels=N_MELS, + hop_length=HOP_LEN + ) + melspec = librosa.power_to_db(melspec).astype(np.float32) + return melspec + +def audio_preprocess(wav, sample_rate): + wav = wav.numpy() + melspec = compute_melspec(wav, sample_rate) + image = mono_to_color(melspec) + image = normalize(image, mean=None, std=None) + image = torch.from_numpy(image) + return image diff --git a/demo/musical_instrument_identification/requirements.txt b/demo/musical_instrument_identification/requirements.txt new file mode 100644 index 0000000..6d940e5 --- /dev/null +++ b/demo/musical_instrument_identification/requirements.txt @@ -0,0 +1,5 @@ +torch==1.12.0 +torchvision==0.13.0 +torchaudio==0.12.0 +librosa==0.9.2 +gdown \ No newline at end of file diff --git a/demo/musical_instrument_identification/run.py b/demo/musical_instrument_identification/run.py new file mode 100644 index 0000000..70c41c3 --- /dev/null +++ b/demo/musical_instrument_identification/run.py @@ -0,0 +1,51 @@ +import gradio as gr +import torch +import torchaudio # type: ignore +from timeit import default_timer as timer +from data_setups import audio_preprocess, resample # type: ignore +import gdown # type: ignore + +url = 'https://drive.google.com/uc?id=1X5CR18u0I-ZOi_8P0cNptCe5JGk9Ro0C' +output = 'piano.wav' +gdown.download(url, output, quiet=False) +url = 'https://drive.google.com/uc?id=1W-8HwmGR5SiyDbUcGAZYYDKdCIst07__' +output= 'torch_efficientnet_fold2_CNN.pth' +gdown.download(url, output, quiet=False) +device = "cuda" if torch.cuda.is_available() else "cpu" +SAMPLE_RATE = 44100 +AUDIO_LEN = 2.90 +model = torch.load("torch_efficientnet_fold2_CNN.pth", map_location=torch.device('cpu')) +LABELS = [ + "Cello", "Clarinet", "Flute", "Acoustic Guitar", "Electric Guitar", "Organ", "Piano", "Saxophone", "Trumpet", "Violin", "Voice" +] +example_list = [ + ["piano.wav"] +] + +def predict(audio_path): + start_time = timer() + wavform, sample_rate = torchaudio.load(audio_path) + wav = resample(wavform, sample_rate, SAMPLE_RATE) + if len(wav) > int(AUDIO_LEN * SAMPLE_RATE): + wav = wav[:int(AUDIO_LEN * SAMPLE_RATE)] + else: + print(f"input length {len(wav)} too small!, need over {int(AUDIO_LEN * SAMPLE_RATE)}") + return + img = audio_preprocess(wav, SAMPLE_RATE).unsqueeze(0) + model.eval() + with torch.inference_mode(): + pred_probs = torch.softmax(model(img), dim=1) + pred_labels_and_probs = {LABELS[i]: float(pred_probs[0][i]) for i in range(len(LABELS))} + pred_time = round(timer() - start_time, 5) + return pred_labels_and_probs, pred_time + +demo = gr.Interface(fn=predict, + inputs=gr.Audio(type="filepath"), + outputs=[gr.Label(num_top_classes=11, label="Predictions"), + gr.Number(label="Prediction time (s)")], + examples=example_list, + cache_examples=False, + api_name="predict" + ) + +demo.launch(debug=False) diff --git a/demo/native_plots/bar_plot_demo.py b/demo/native_plots/bar_plot_demo.py new file mode 100644 index 0000000..23bddf6 --- /dev/null +++ b/demo/native_plots/bar_plot_demo.py @@ -0,0 +1,80 @@ +import gradio as gr +from data import temp_sensor_data, food_rating_data # type: ignore + +with gr.Blocks() as bar_plots: + with gr.Row(): + start = gr.DateTime("2021-01-01 00:00:00", label="Start") + end = gr.DateTime("2021-01-05 00:00:00", label="End") + apply_btn = gr.Button("Apply", scale=0) + with gr.Row(): + group_by = gr.Radio(["None", "30m", "1h", "4h", "1d"], value="None", label="Group by") + aggregate = gr.Radio(["sum", "mean", "median", "min", "max"], value="sum", label="Aggregation") + + with gr.Draggable(): + temp_by_time = gr.BarPlot( + temp_sensor_data, + x="time", + y="temperature", + buttons=["export"], + ) + temp_by_time_location = gr.BarPlot( + temp_sensor_data, + x="time", + y="temperature", + color="location", + buttons=["export"], + ) + + time_graphs = [temp_by_time, temp_by_time_location] + group_by.change( + lambda group: [gr.BarPlot(x_bin=None if group == "None" else group)] * len(time_graphs), + group_by, + time_graphs + ) + aggregate.change( + lambda aggregate: [gr.BarPlot(y_aggregate=aggregate)] * len(time_graphs), + aggregate, + time_graphs + ) + + def rescale(select: gr.SelectData): + return select.index + rescale_evt = gr.on([plot.select for plot in time_graphs], rescale, None, [start, end]) + + for trigger in [apply_btn.click, rescale_evt.then]: + trigger( + lambda start, end: [gr.BarPlot(x_lim=[start, end])] * len(time_graphs), [start, end], time_graphs + ) + + with gr.Row(): + price_by_cuisine = gr.BarPlot( + food_rating_data, + x="cuisine", + y="price", + buttons=["export"], + ) + with gr.Column(scale=0): + gr.Button("Sort $ > $$$").click(lambda: gr.BarPlot(sort="y"), None, price_by_cuisine) + gr.Button("Sort $$$ > $").click(lambda: gr.BarPlot(sort="-y"), None, price_by_cuisine) + gr.Button("Sort A > Z").click(lambda: gr.BarPlot(sort=["Chinese", "Italian", "Mexican"]), None, price_by_cuisine) + + with gr.Row(): + price_by_rating = gr.BarPlot( + food_rating_data, + x="rating", + y="price", + x_bin=1, + buttons=["export"], + ) + price_by_rating_color = gr.BarPlot( + food_rating_data, + x="rating", + y="price", + color="cuisine", + x_bin=1, + color_map={"Italian": "red", "Mexican": "green", "Chinese": "blue"}, + buttons=["export"], + ) + +if __name__ == "__main__": + bar_plots.launch() diff --git a/demo/native_plots/data.py b/demo/native_plots/data.py new file mode 100644 index 0000000..52aef79 --- /dev/null +++ b/demo/native_plots/data.py @@ -0,0 +1,20 @@ +import pandas as pd +from random import randint, random + +temp_sensor_data = pd.DataFrame( + { + "time": pd.date_range("2021-01-01", end="2021-01-05", periods=200), + "temperature": [randint(50 + 10 * (i % 2), 65 + 15 * (i % 2)) for i in range(200)], + "humidity": [randint(50 + 10 * (i % 2), 65 + 15 * (i % 2)) for i in range(200)], + "location": ["indoor", "outdoor"] * 100, + } +) + +food_rating_data = pd.DataFrame( + { + "cuisine": [["Italian", "Mexican", "Chinese"][i % 3] for i in range(100)], + "rating": [random() * 4 + 0.5 * (i % 3) for i in range(100)], + "price": [randint(10, 50) + 4 * (i % 3) for i in range(100)], + "wait": [random() for i in range(100)], + } +) diff --git a/demo/native_plots/line_plot_demo.py b/demo/native_plots/line_plot_demo.py new file mode 100644 index 0000000..de89bf9 --- /dev/null +++ b/demo/native_plots/line_plot_demo.py @@ -0,0 +1,75 @@ +import gradio as gr +from data import temp_sensor_data, food_rating_data # type: ignore + +with gr.Blocks() as line_plots: + with gr.Row(): + start = gr.DateTime("2021-01-01 00:00:00", label="Start") + end = gr.DateTime("2021-01-05 00:00:00", label="End") + apply_btn = gr.Button("Apply", scale=0) + with gr.Row(): + group_by = gr.Radio(["None", "30m", "1h", "4h", "1d"], value="None", label="Group by") + aggregate = gr.Radio(["sum", "mean", "median", "min", "max"], value="sum", label="Aggregation") + + temp_by_time = gr.LinePlot( + temp_sensor_data, + x="time", + y="temperature", + buttons=["export"], + ) + temp_by_time_location = gr.LinePlot( + temp_sensor_data, + x="time", + y="temperature", + color="location", + buttons=["export"], + ) + + time_graphs = [temp_by_time, temp_by_time_location] + group_by.change( + lambda group: [gr.LinePlot(x_bin=None if group == "None" else group)] * len(time_graphs), + group_by, + time_graphs + ) + aggregate.change( + lambda aggregate: [gr.LinePlot(y_aggregate=aggregate)] * len(time_graphs), + aggregate, + time_graphs + ) + + def rescale(select: gr.SelectData): + return select.index + rescale_evt = gr.on([plot.select for plot in time_graphs], rescale, None, [start, end]) + + for trigger in [apply_btn.click, rescale_evt.then]: + trigger( + lambda start, end: [gr.LinePlot(x_lim=[start, end])] * len(time_graphs), [start, end], time_graphs + ) + + price_by_cuisine = gr.LinePlot( + food_rating_data, + x="cuisine", + y="price", + buttons=["export"], + ) + with gr.Row(): + price_by_rating = gr.LinePlot( + food_rating_data, + title="Price by Rating (for Ratings >2)", + x="rating", + y="price", + x_lim=[2, None], + buttons=["export"], + ) + price_by_rating_color = gr.LinePlot( + food_rating_data, + title="Price by Rating (for Ratings <4)", + x="rating", + y="price", + x_lim=[None, 4], + color="cuisine", + color_map={"Italian": "red", "Mexican": "green", "Chinese": "blue"}, + buttons=["export"], + ) + +if __name__ == "__main__": + line_plots.launch() diff --git a/demo/native_plots/requirements.txt b/demo/native_plots/requirements.txt new file mode 100644 index 0000000..fe1f1ac --- /dev/null +++ b/demo/native_plots/requirements.txt @@ -0,0 +1,2 @@ +vega_datasets +pandas diff --git a/demo/native_plots/run.py b/demo/native_plots/run.py new file mode 100644 index 0000000..afad277 --- /dev/null +++ b/demo/native_plots/run.py @@ -0,0 +1,17 @@ +import gradio as gr + +from scatter_plot_demo import scatter_plots # type: ignore +from line_plot_demo import line_plots # type: ignore +from bar_plot_demo import bar_plots # type: ignore + +with gr.Blocks() as demo: + with gr.Tabs(): + with gr.TabItem("Line Plot"): + line_plots.render() + with gr.TabItem("Scatter Plot"): + scatter_plots.render() + with gr.TabItem("Bar Plot"): + bar_plots.render() + +if __name__ == "__main__": + demo.launch() diff --git a/demo/native_plots/scatter_plot_demo.py b/demo/native_plots/scatter_plot_demo.py new file mode 100644 index 0000000..bdd6340 --- /dev/null +++ b/demo/native_plots/scatter_plot_demo.py @@ -0,0 +1,71 @@ +import gradio as gr +from data import temp_sensor_data, food_rating_data # type: ignore + +with gr.Blocks() as scatter_plots: + with gr.Row(): + start = gr.DateTime("2021-01-01 00:00:00", label="Start") + end = gr.DateTime("2021-01-05 00:00:00", label="End") + apply_btn = gr.Button("Apply", scale=0) + with gr.Row(): + group_by = gr.Radio(["None", "30m", "1h", "4h", "1d"], value="None", label="Group by") + aggregate = gr.Radio(["sum", "mean", "median", "min", "max"], value="sum", label="Aggregation") + + temp_by_time = gr.ScatterPlot( + temp_sensor_data, + x="time", + y="temperature", + buttons=["export"], + ) + temp_by_time_location = gr.ScatterPlot( + temp_sensor_data, + x="time", + y="temperature", + color="location", + buttons=["export"], + ) + + time_graphs = [temp_by_time, temp_by_time_location] + group_by.change( + lambda group: [gr.ScatterPlot(x_bin=None if group == "None" else group)] * len(time_graphs), + group_by, + time_graphs + ) + aggregate.change( + lambda aggregate: [gr.ScatterPlot(y_aggregate=aggregate)] * len(time_graphs), + aggregate, + time_graphs + ) + + # def rescale(select: gr.SelectData): + # return select.index + # rescale_evt = gr.on([plot.select for plot in time_graphs], rescale, None, [start, end]) + + # for trigger in [apply_btn.click, rescale_evt.then]: + # trigger( + # lambda start, end: [gr.ScatterPlot(x_lim=[start, end])] * len(time_graphs), [start, end], time_graphs + # ) + + price_by_cuisine = gr.ScatterPlot( + food_rating_data, + x="cuisine", + y="price", + buttons=["export"], + ) + with gr.Row(): + price_by_rating = gr.ScatterPlot( + food_rating_data, + x="rating", + y="price", + color="wait", + buttons=["actions", "export"], # type: ignore + ) + price_by_rating_color = gr.ScatterPlot( + food_rating_data, + x="rating", + y="price", + color="cuisine", + buttons=["export"], + ) + +if __name__ == "__main__": + scatter_plots.launch() diff --git a/demo/navbar_customization/run.py b/demo/navbar_customization/run.py new file mode 100644 index 0000000..3ddf3f5 --- /dev/null +++ b/demo/navbar_customization/run.py @@ -0,0 +1,15 @@ +import gradio as gr + +with gr.Blocks(title="Navbar Demo") as demo: + navbar = gr.Navbar(value=[("About Me", "https://x.com/abidlabs")], visible=True, main_page_name="Dashboard") + gr.Markdown("# Dashboard Page") + hide_btn = gr.Button("Hide Navbar") + hide_btn.click(fn=lambda : gr.Navbar(visible=False), outputs=navbar) + show_btn = gr.Button("Show Navbar") + show_btn.click(fn=lambda : gr.Navbar(visible=True, main_page_name="Dashboard is Back!"), outputs=navbar) + +with demo.route("Settings", "/settings"): + gr.Markdown("# Settings Page") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/navbar_query_params/run.py b/demo/navbar_query_params/run.py new file mode 100644 index 0000000..f741772 --- /dev/null +++ b/demo/navbar_query_params/run.py @@ -0,0 +1,65 @@ +"""This is a demo of a custom navbar that preserves query parameters from one page to the next.""" + +import gradio as gr + +def update_navbar_with_query(choice): + """Updates navbar links to preserve query parameters""" + if choice: + return [("Main Page", f"?option={choice}"), ("Page 2", f"second?option={choice}")] + return [("Main Page", ""), ("Page 2", "second")] + +def update_dropdown_based_on_query(request: gr.Request): + """Updates dropdown based on query parameters""" + return request.query_params.get("option") + +update_url_js = """ +(choice) => { + console.log("choice", choice); + if (choice) { + const url = new URL(window.location); + url.searchParams.set('option', choice); + window.history.replaceState({}, '', url); + } + return choice; +} +""" + +with gr.Blocks() as demo: + gr.Markdown("If you select an option, and go to the second page, the option will be preserved.") + + navbar1 = gr.Navbar( + value=[("Main Page", ""),("Page 2", "second")], + visible=True, + main_page_name=False, + ) + + dropdown1 = gr.Dropdown( + choices=["America", "Canada", "Pakistan"], + label="Select an option", + value=None + ) + + dropdown1.change( + fn=update_navbar_with_query, + inputs=[dropdown1], + outputs=[navbar1], + js=update_url_js + ) + +with demo.route("second", show_in_navbar=False) as second_page: + navbar2 = gr.Navbar( + visible=True, + main_page_name=False, + value=[("Main Page", ""),("Page 2", "second")] + ) + + dropdown2 = gr.Dropdown( + choices=["America", "Canada", "Pakistan"], + label="Select a value", + value=None + ) + + second_page.load(update_dropdown_based_on_query, None, dropdown2) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/neon-tts-plugin-coqui/DESCRIPTION.md b/demo/neon-tts-plugin-coqui/DESCRIPTION.md new file mode 100644 index 0000000..d9e5f1c --- /dev/null +++ b/demo/neon-tts-plugin-coqui/DESCRIPTION.md @@ -0,0 +1 @@ +This demo converts text to speech in 14 languages. \ No newline at end of file diff --git a/demo/neon-tts-plugin-coqui/packages.txt b/demo/neon-tts-plugin-coqui/packages.txt new file mode 100644 index 0000000..c5585ec --- /dev/null +++ b/demo/neon-tts-plugin-coqui/packages.txt @@ -0,0 +1,2 @@ +libsndfile1 +espeak-ng \ No newline at end of file diff --git a/demo/neon-tts-plugin-coqui/requirements.txt b/demo/neon-tts-plugin-coqui/requirements.txt new file mode 100644 index 0000000..96d8dd6 --- /dev/null +++ b/demo/neon-tts-plugin-coqui/requirements.txt @@ -0,0 +1 @@ +neon-tts-plugin-coqui==0.4.1a9 \ No newline at end of file diff --git a/demo/neon-tts-plugin-coqui/run.py b/demo/neon-tts-plugin-coqui/run.py new file mode 100644 index 0000000..7e73375 --- /dev/null +++ b/demo/neon-tts-plugin-coqui/run.py @@ -0,0 +1,19 @@ +import tempfile +import gradio as gr +from neon_tts_plugin_coqui import CoquiTTS # type: ignore + +LANGUAGES = list(CoquiTTS.langs.keys()) +coquiTTS = CoquiTTS() + +def tts(text: str, language: str): + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as fp: + coquiTTS.get_tts(text, fp, speaker = {"language" : language}) + return fp.name + +inputs = [gr.Textbox(label="Input", value=CoquiTTS.langs["en"]["sentence"], max_lines=3), + gr.Radio(label="Language", choices=LANGUAGES, value="en")] +outputs = gr.Audio(label="Output") + +demo = gr.Interface(fn=tts, inputs=inputs, outputs=outputs) + +demo.launch() diff --git a/demo/ner_pipeline/requirements.txt b/demo/ner_pipeline/requirements.txt new file mode 100644 index 0000000..39dab0f --- /dev/null +++ b/demo/ner_pipeline/requirements.txt @@ -0,0 +1,2 @@ +torch +transformers \ No newline at end of file diff --git a/demo/ner_pipeline/run.py b/demo/ner_pipeline/run.py new file mode 100644 index 0000000..d084d05 --- /dev/null +++ b/demo/ner_pipeline/run.py @@ -0,0 +1,22 @@ +from transformers import pipeline + +import gradio as gr + +ner_pipeline = pipeline("ner") # type: ignore + +examples = [ + "Does Chicago have any stores and does Joe live here?", +] + +def ner(text): + output = ner_pipeline(text) + return {"text": text, "entities": output} + +demo = gr.Interface(ner, + gr.Textbox(placeholder="Enter sentence here..."), + gr.HighlightedText(), + examples=examples, + api_name="predict") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/no_input/run.py b/demo/no_input/run.py new file mode 100644 index 0000000..85ab99d --- /dev/null +++ b/demo/no_input/run.py @@ -0,0 +1,16 @@ +import gradio as gr +import random + +sentence_list = [ + "Good morning!", + "Prayers are with you, have a safe day!", + "I love you!" +] + +def random_sentence(): + return sentence_list[random.randint(0, 2)] + +demo = gr.Interface(fn=random_sentence, inputs=None, outputs="text") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/number_component/run.py b/demo/number_component/run.py new file mode 100644 index 0000000..6837665 --- /dev/null +++ b/demo/number_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Number() + +demo.launch() diff --git a/demo/on_listener_basic/run.py b/demo/on_listener_basic/run.py new file mode 100644 index 0000000..26e3f2b --- /dev/null +++ b/demo/on_listener_basic/run.py @@ -0,0 +1,23 @@ +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") + trigger = gr.Textbox(label="Trigger Box") + + def greet(name, evt_data: gr.EventData): + return "Hello " + name + "!", evt_data.target.__class__.__name__ + + def clear_name(evt_data: gr.EventData): + return "" + + gr.on( + triggers=[name.submit, greet_btn.click], + fn=greet, + inputs=name, + outputs=[output, trigger], + ).then(clear_name, outputs=[name]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/on_listener_decorator/run.py b/demo/on_listener_decorator/run.py new file mode 100644 index 0000000..ea3e14c --- /dev/null +++ b/demo/on_listener_decorator/run.py @@ -0,0 +1,13 @@ +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 + "!" + +if __name__ == "__main__": + demo.launch() diff --git a/demo/on_listener_live/run.py b/demo/on_listener_live/run.py new file mode 100644 index 0000000..f22c7d4 --- /dev/null +++ b/demo/on_listener_live/run.py @@ -0,0 +1,15 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + num1 = gr.Slider(1, 10) + num2 = gr.Slider(1, 10) + num3 = gr.Slider(1, 10) + output = gr.Number(label="Sum") + + @gr.on(inputs=[num1, num2, num3], outputs=output) + def sum(a, b, c): + return a + b + c + +if __name__ == "__main__": + demo.launch() diff --git a/demo/on_listener_test/run.py b/demo/on_listener_test/run.py new file mode 100644 index 0000000..3fb6d17 --- /dev/null +++ b/demo/on_listener_test/run.py @@ -0,0 +1,24 @@ +import gradio as gr + +with gr.Blocks() as demo: + name = gr.Textbox(label="Name") + output = gr.Textbox(label="Output") + greet_btn = gr.Button("Greet") + trigger = gr.Textbox(label="Trigger 1") + trigger2 = gr.Textbox(label="Trigger 2") + + def greet(name, evt_data: gr.EventData): + return "Hello " + name + "!", evt_data.target.__class__.__name__ + + def clear_name(evt_data: gr.EventData): + return "", evt_data.target.__class__.__name__ + + gr.on( + triggers=[name.submit, greet_btn.click], + fn=greet, + inputs=name, + outputs=[output, trigger], + ).then(clear_name, outputs=[name, trigger2]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/outbreak_forecast/DESCRIPTION.md b/demo/outbreak_forecast/DESCRIPTION.md new file mode 100644 index 0000000..dda5925 --- /dev/null +++ b/demo/outbreak_forecast/DESCRIPTION.md @@ -0,0 +1 @@ +Generate a plot based on 5 inputs. \ No newline at end of file diff --git a/demo/outbreak_forecast/requirements.txt b/demo/outbreak_forecast/requirements.txt new file mode 100644 index 0000000..f513b41 --- /dev/null +++ b/demo/outbreak_forecast/requirements.txt @@ -0,0 +1,5 @@ +numpy +matplotlib +bokeh +plotly +altair diff --git a/demo/outbreak_forecast/run.py b/demo/outbreak_forecast/run.py new file mode 100644 index 0000000..3fa7c4c --- /dev/null +++ b/demo/outbreak_forecast/run.py @@ -0,0 +1,83 @@ +import gradio as gr +from math import sqrt +import numpy as np +import pandas as pd + +def outbreak(plot_type, r, month, countries, social_distancing): + months = ["January", "February", "March", "April", "May"] + m = months.index(month) + start_day = 30 * m + final_day = 30 * (m + 1) + x = np.arange(start_day, final_day + 1) + pop_count = {"USA": 350, "Canada": 40, "Mexico": 300, "UK": 120} + if social_distancing: + r = sqrt(r) + df = pd.DataFrame({"day": x}) + for country in countries: + df[country] = x ** (r) * (pop_count[country] + 1) + + if plot_type == "Matplotlib": + import matplotlib.pyplot as plt + + fig = plt.figure() + plt.plot(df["day"], df[countries].to_numpy()) + plt.title("Outbreak in " + month) + plt.ylabel("Cases") + plt.xlabel("Days since Day 0") + plt.legend(countries) + return fig + elif plot_type == "Plotly": + import plotly.express as px # type: ignore + + fig = px.line(df, x="day", y=countries) + fig.update_layout( + title="Outbreak in " + month, + xaxis_title="Cases", + yaxis_title="Days Since Day 0", + ) + return fig + elif plot_type == "Altair": + import altair + + df = df.melt(id_vars="day").rename(columns={"variable": "country"}) + fig = altair.Chart(df).mark_line().encode(x="day", y="value", color="country") + return fig + elif plot_type == "Bokeh": + from bokeh.plotting import figure # type: ignore + from bokeh.models import ColumnDataSource # type: ignore + + source = ColumnDataSource(df) + fig = figure(title="Outbreak in " + month, x_axis_label="Days since Day 0", y_axis_label="Cases") + for country in countries: + fig.line("day", country, source=source, legend_label=country) + return fig + else: + raise ValueError("A plot type must be selected") + +inputs = [ + gr.Dropdown(["Matplotlib", "Plotly", "Altair", "Bokeh"], label="Plot Type", value="Matplotlib"), + gr.Slider(1, 4, 3.2, label="R"), + gr.Dropdown(["January", "February", "March", "April", "May"], label="Month", value="March"), + gr.CheckboxGroup( + ["USA", "Canada", "Mexico", "UK"], label="Countries", value=["USA", "Canada"] + ), + gr.Checkbox(label="Social Distancing?"), +] +outputs = gr.Plot() + +demo = gr.Interface( + fn=outbreak, + inputs=inputs, + outputs=outputs, + examples=[ + ["Matplotlib", 2, "March", ["Mexico", "UK"], True], + ["Altair", 2, "March", ["Mexico", "Canada"], True], + ["Plotly", 3.6, "February", ["Canada", "Mexico", "UK"], False], + ["Bokeh", 3.2, "April", ["Canada", "UK"], False], + ], + cache_examples=True, + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/paramviewer_component/run.py b/demo/paramviewer_component/run.py new file mode 100644 index 0000000..f9e52a5 --- /dev/null +++ b/demo/paramviewer_component/run.py @@ -0,0 +1,21 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown("The `round()` function in Python takes two parameters") + gr.ParamViewer( + { # type: ignore + "number": { + "type": "int | float", + "description": "The number to round", + "default": None + }, + "ndigits": { + "type": "int", + "description": "The number of digits to round to", + "default": "0" + } + } + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/pending_inputs/run.py b/demo/pending_inputs/run.py new file mode 100644 index 0000000..4203caa --- /dev/null +++ b/demo/pending_inputs/run.py @@ -0,0 +1,111 @@ +import gradio as gr + + +with gr.Blocks() as demo: + gr.Markdown("# Pending Input Components") + with gr.Row(): + with gr.Column(): + file = gr.File() + btn = gr.Button("Upload") + with gr.Column(): + output_file = gr.File() + btn.click( + lambda s: (s), + file, + output_file, + ) + with gr.Row(): + with gr.Column(): + img = gr.Image(type="filepath") + btn_2 = gr.Button("Upload") + with gr.Column(): + output_file_2 = gr.File() + btn_2.click( + lambda s: (s), + img, + output_file_2, + ) + with gr.Row(): + with gr.Column(): + audio = gr.Audio(type="filepath") + btn_3 = gr.Button("Upload") + with gr.Column(): + output_file_3 = gr.File() + btn_3.click( + lambda s: (s), + audio, + output_file_3, + ) + with gr.Row(): + with gr.Column(): + video = gr.Video() + btn_3 = gr.Button("Upload") + with gr.Column(): + output_file_4 = gr.File() + btn_3.click( + lambda s: (s), + video, + output_file_4, + ) + with gr.Row(): + with gr.Column(): + model3d = gr.Model3D() + btn_4 = gr.Button("Upload") + with gr.Column(): + output_file_4 = gr.File() + btn_4.click( + lambda s: (s), + model3d, + output_file_4, + ) + + with gr.Row(): + with gr.Column(): + gallery = gr.Gallery() + btn_5 = gr.Button("Upload") + with gr.Column(): + output_file_5 = gr.File(file_count="multiple") + btn_5.click( + lambda s: [x[0] for x in s], + gallery, + output_file_5, + ) + # with gr.Row(): + # with gr.Column(): + # df = gr.Dataframe() + # btn_6 = gr.Button("Upload") + # with gr.Column(): + # output_file_6 = gr.File() + # btn_6.click( + # lambda s: (s), + # df, + # output_file_6, + # ) + with gr.Row(): + with gr.Column(): + imageslider = gr.ImageSlider(type="filepath") + btn_7 = gr.Button("Upload") + with gr.Column(): + output_file_7 = gr.File() + btn_7.click( + lambda s: s[0], + imageslider, + output_file_7, + ) + with gr.Row(): + with gr.Column(): + text = gr.MultimodalTextbox() + btn_8 = gr.Button("Upload") + with gr.Column(): + output_file_8 = gr.File() + btn_8.click( + lambda s: s["files"], + text, + output_file_8, + ) + + +if __name__ == "__main__": + demo.launch( + allowed_paths=["/private/var/folders/3w/6btg016509v7b2lz9h7vwqv00000gn/T"] + ) diff --git a/demo/playback_position/run.py b/demo/playback_position/run.py new file mode 100644 index 0000000..677496b --- /dev/null +++ b/demo/playback_position/run.py @@ -0,0 +1,53 @@ +import gradio as gr +from gradio.media import get_audio, get_video + +# Get the directory where this script is located +with gr.Blocks() as demo: + with gr.Tab("Audio"): + gr.Markdown("## Audio Playback Position") + gr.Markdown("Click the button to see the current playback position of the audio.") + + audio = gr.Audio( + value=get_audio("sax.wav"), + playback_position=2.0, + elem_id="audio", + ) + audio_btn = gr.Button("Get Audio Playback Position") + audio_position = gr.Number(label="Current Audio Position (seconds)") + + def print_audio_playback_pos(a: gr.Audio): + return a.playback_position + + audio_btn.click(print_audio_playback_pos, inputs=audio, outputs=audio_position) + + set_audio_time_btn = gr.Button("Set Audio Playback Position to 10 seconds") + def set_audio_playback_pos(): + return gr.Audio(playback_position=10.0) + + set_audio_time_btn.click(set_audio_playback_pos, outputs=audio) + + with gr.Tab("Video"): + gr.Markdown("## Video Playback Position") + gr.Markdown("Click the button to see the current playback position of the video.") + + video = gr.Video( + value=get_video("world.mp4"), + playback_position=5.0, + elem_id="video", + ) + video_btn = gr.Button("Get Video Playback Position") + video_position = gr.Number(label="Current Video Position (seconds)") + + def print_video_playback_pos(v: gr.Video): + return v.playback_position + + video_btn.click(print_video_playback_pos, inputs=video, outputs=video_position) + + set_video_time_btn = gr.Button("Set Video Playback Position to 8 seconds") + def set_video_playback_pos(): + return gr.Video(playback_position=8.0) + + set_video_time_btn.click(set_video_playback_pos, outputs=video) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/playback_position/sax.wav b/demo/playback_position/sax.wav new file mode 100644 index 0000000..de6d16a Binary files /dev/null and b/demo/playback_position/sax.wav differ diff --git a/demo/playback_position/world.mp4 b/demo/playback_position/world.mp4 new file mode 100644 index 0000000..b11552f Binary files /dev/null and b/demo/playback_position/world.mp4 differ diff --git a/demo/plot_component/requirements.txt b/demo/plot_component/requirements.txt new file mode 100644 index 0000000..8248f97 --- /dev/null +++ b/demo/plot_component/requirements.txt @@ -0,0 +1,2 @@ +matplotlib +numpy \ No newline at end of file diff --git a/demo/plot_component/run.py b/demo/plot_component/run.py new file mode 100644 index 0000000..8c92c01 --- /dev/null +++ b/demo/plot_component/run.py @@ -0,0 +1,15 @@ +import gradio as gr +import matplotlib.pyplot as plt +import numpy as np + +Fs = 8000 +f = 5 +sample = 8000 +x = np.arange(sample) +y = np.sin(2 * np.pi * f * x / Fs) +plt.plot(x, y) + +with gr.Blocks() as demo: + gr.Plot(value=plt) + +demo.launch() diff --git a/demo/plot_guide_aggregate_nominal/data.py b/demo/plot_guide_aggregate_nominal/data.py new file mode 100644 index 0000000..820e047 --- /dev/null +++ b/demo/plot_guide_aggregate_nominal/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_aggregate_nominal/run.py b/demo/plot_guide_aggregate_nominal/run.py new file mode 100644 index 0000000..929c914 --- /dev/null +++ b/demo/plot_guide_aggregate_nominal/run.py @@ -0,0 +1,8 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + gr.BarPlot(df, x="ethnicity", y="height", y_aggregate="mean") + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_aggregate_quantitative/data.py b/demo/plot_guide_aggregate_quantitative/data.py new file mode 100644 index 0000000..820e047 --- /dev/null +++ b/demo/plot_guide_aggregate_quantitative/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_aggregate_quantitative/run.py b/demo/plot_guide_aggregate_quantitative/run.py new file mode 100644 index 0000000..2365a89 --- /dev/null +++ b/demo/plot_guide_aggregate_quantitative/run.py @@ -0,0 +1,8 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + gr.BarPlot(df, x="weight", y="height", x_bin=10, y_aggregate="sum") + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_aggregate_temporal/data.py b/demo/plot_guide_aggregate_temporal/data.py new file mode 100644 index 0000000..e9c8e08 --- /dev/null +++ b/demo/plot_guide_aggregate_temporal/data.py @@ -0,0 +1,13 @@ +import pandas as pd +import numpy as np +import random +from datetime import datetime, timedelta + +now = datetime.now() + +df = pd.DataFrame({ + 'time': [now - timedelta(minutes=5*i) for i in range(25)], + 'price': np.random.randint(100, 1000, 25), + 'origin': [random.choice(["DFW", "DAL", "HOU"]) for _ in range(25)], + 'destination': [random.choice(["JFK", "LGA", "EWR"]) for _ in range(25)], +}) diff --git a/demo/plot_guide_aggregate_temporal/run.py b/demo/plot_guide_aggregate_temporal/run.py new file mode 100644 index 0000000..88b1fe9 --- /dev/null +++ b/demo/plot_guide_aggregate_temporal/run.py @@ -0,0 +1,11 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + plot = gr.BarPlot(df, x="time", y="price", x_bin="10m") + + bins = gr.Radio(["10m", "30m", "1h"], label="Bin Size") + bins.change(lambda bins: gr.BarPlot(x_bin=bins), bins, plot) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_datetime/data.py b/demo/plot_guide_datetime/data.py new file mode 100644 index 0000000..e9c8e08 --- /dev/null +++ b/demo/plot_guide_datetime/data.py @@ -0,0 +1,13 @@ +import pandas as pd +import numpy as np +import random +from datetime import datetime, timedelta + +now = datetime.now() + +df = pd.DataFrame({ + 'time': [now - timedelta(minutes=5*i) for i in range(25)], + 'price': np.random.randint(100, 1000, 25), + 'origin': [random.choice(["DFW", "DAL", "HOU"]) for _ in range(25)], + 'destination': [random.choice(["JFK", "LGA", "EWR"]) for _ in range(25)], +}) diff --git a/demo/plot_guide_datetime/run.py b/demo/plot_guide_datetime/run.py new file mode 100644 index 0000000..8865619 --- /dev/null +++ b/demo/plot_guide_datetime/run.py @@ -0,0 +1,14 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + with gr.Row(): + start = gr.DateTime("now - 24h") + end = gr.DateTime("now") + apply_btn = gr.Button("Apply") + plot = gr.LinePlot(df, x="time", y="price") + + apply_btn.click(lambda start, end: gr.BarPlot(x_lim=[start, end]), [start, end], plot) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_datetimerange/data.py b/demo/plot_guide_datetimerange/data.py new file mode 100644 index 0000000..e9c8e08 --- /dev/null +++ b/demo/plot_guide_datetimerange/data.py @@ -0,0 +1,13 @@ +import pandas as pd +import numpy as np +import random +from datetime import datetime, timedelta + +now = datetime.now() + +df = pd.DataFrame({ + 'time': [now - timedelta(minutes=5*i) for i in range(25)], + 'price': np.random.randint(100, 1000, 25), + 'origin': [random.choice(["DFW", "DAL", "HOU"]) for _ in range(25)], + 'destination': [random.choice(["JFK", "LGA", "EWR"]) for _ in range(25)], +}) diff --git a/demo/plot_guide_datetimerange/requirements.txt b/demo/plot_guide_datetimerange/requirements.txt new file mode 100644 index 0000000..b0c6fc9 --- /dev/null +++ b/demo/plot_guide_datetimerange/requirements.txt @@ -0,0 +1 @@ +gradio_datetimerange \ No newline at end of file diff --git a/demo/plot_guide_datetimerange/run.py b/demo/plot_guide_datetimerange/run.py new file mode 100644 index 0000000..ef9c3a9 --- /dev/null +++ b/demo/plot_guide_datetimerange/run.py @@ -0,0 +1,12 @@ +import gradio as gr +from gradio_datetimerange import DateTimeRange # type: ignore +from data import df # type: ignore + +with gr.Blocks() as demo: + daterange = DateTimeRange(["now - 24h", "now"]) + plot1 = gr.LinePlot(df, x="time", y="price") + plot2 = gr.LinePlot(df, x="time", y="price", color="origin") + daterange.bind([plot1, plot2]) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_filters/data.py b/demo/plot_guide_filters/data.py new file mode 100644 index 0000000..e9c8e08 --- /dev/null +++ b/demo/plot_guide_filters/data.py @@ -0,0 +1,13 @@ +import pandas as pd +import numpy as np +import random +from datetime import datetime, timedelta + +now = datetime.now() + +df = pd.DataFrame({ + 'time': [now - timedelta(minutes=5*i) for i in range(25)], + 'price': np.random.randint(100, 1000, 25), + 'origin': [random.choice(["DFW", "DAL", "HOU"]) for _ in range(25)], + 'destination': [random.choice(["JFK", "LGA", "EWR"]) for _ in range(25)], +}) diff --git a/demo/plot_guide_filters/run.py b/demo/plot_guide_filters/run.py new file mode 100644 index 0000000..985548d --- /dev/null +++ b/demo/plot_guide_filters/run.py @@ -0,0 +1,21 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + with gr.Row(): + origin = gr.Dropdown(["All", "DFW", "DAL", "HOU"], value="All", label="Origin") + destination = gr.Dropdown(["All", "JFK", "LGA", "EWR"], value="All", label="Destination") + max_price = gr.Slider(0, 1000, value=1000, label="Max Price") + + def filtered_data(origin, destination, max_price): + _df = df[df["price"] <= max_price] + if origin != "All": + _df = _df[_df["origin"] == origin] + if destination != "All": + _df = _df[_df["destination"] == destination] + return _df + + gr.ScatterPlot(filtered_data, x="time", y="price", inputs=[origin, destination, max_price]) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_filters_events/data.py b/demo/plot_guide_filters_events/data.py new file mode 100644 index 0000000..e9c8e08 --- /dev/null +++ b/demo/plot_guide_filters_events/data.py @@ -0,0 +1,13 @@ +import pandas as pd +import numpy as np +import random +from datetime import datetime, timedelta + +now = datetime.now() + +df = pd.DataFrame({ + 'time': [now - timedelta(minutes=5*i) for i in range(25)], + 'price': np.random.randint(100, 1000, 25), + 'origin': [random.choice(["DFW", "DAL", "HOU"]) for _ in range(25)], + 'destination': [random.choice(["JFK", "LGA", "EWR"]) for _ in range(25)], +}) diff --git a/demo/plot_guide_filters_events/run.py b/demo/plot_guide_filters_events/run.py new file mode 100644 index 0000000..d2dc070 --- /dev/null +++ b/demo/plot_guide_filters_events/run.py @@ -0,0 +1,23 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + with gr.Row(): + origin = gr.Dropdown(["All", "DFW", "DAL", "HOU"], value="All", label="Origin") + destination = gr.Dropdown(["All", "JFK", "LGA", "EWR"], value="All", label="Destination") + max_price = gr.Slider(0, 1000, value=1000, label="Max Price") + + plt = gr.ScatterPlot(df, x="time", y="price", inputs=[origin, destination, max_price]) + + @gr.on(inputs=[origin, destination, max_price], outputs=plt) + def filtered_data(origin, destination, max_price): + _df = df[df["price"] <= max_price] + if origin != "All": + _df = _df[_df["origin"] == origin] + if destination != "All": + _df = _df[_df["destination"] == destination] + return _df + + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_interactive/data.py b/demo/plot_guide_interactive/data.py new file mode 100644 index 0000000..820e047 --- /dev/null +++ b/demo/plot_guide_interactive/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_interactive/run.py b/demo/plot_guide_interactive/run.py new file mode 100644 index 0000000..5003bba --- /dev/null +++ b/demo/plot_guide_interactive/run.py @@ -0,0 +1,18 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + with gr.Row(): + ethnicity = gr.Dropdown(["all", "white", "black", "asian"], value="all") + max_age = gr.Slider(18, 65, value=65) + + def filtered_df(ethnic, age): + _df = df if ethnic == "all" else df[df["ethnicity"] == ethnic] + _df = _df[_df["age"] < age] + return _df + + gr.ScatterPlot(filtered_df, inputs=[ethnicity, max_age], x="weight", y="height", title="Weight x Height") + gr.LinePlot(filtered_df, inputs=[ethnicity, max_age], x="age", y="height", title="Age x Height") + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_line/requirements.txt b/demo/plot_guide_line/requirements.txt new file mode 100644 index 0000000..5da331c --- /dev/null +++ b/demo/plot_guide_line/requirements.txt @@ -0,0 +1,2 @@ +numpy +pandas diff --git a/demo/plot_guide_line/run.py b/demo/plot_guide_line/run.py new file mode 100644 index 0000000..10418e1 --- /dev/null +++ b/demo/plot_guide_line/run.py @@ -0,0 +1,17 @@ +import gradio as gr +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) + +with gr.Blocks() as demo: + gr.LinePlot(df, x="weight", y="height") + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_scatter/data.py b/demo/plot_guide_scatter/data.py new file mode 100644 index 0000000..820e047 --- /dev/null +++ b/demo/plot_guide_scatter/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_scatter/run.py b/demo/plot_guide_scatter/run.py new file mode 100644 index 0000000..6258559 --- /dev/null +++ b/demo/plot_guide_scatter/run.py @@ -0,0 +1,8 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + gr.ScatterPlot(df, x="weight", y="height") + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_scatter_nominal/data.py b/demo/plot_guide_scatter_nominal/data.py new file mode 100644 index 0000000..820e047 --- /dev/null +++ b/demo/plot_guide_scatter_nominal/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_scatter_nominal/run.py b/demo/plot_guide_scatter_nominal/run.py new file mode 100644 index 0000000..51ffa07 --- /dev/null +++ b/demo/plot_guide_scatter_nominal/run.py @@ -0,0 +1,8 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + gr.ScatterPlot(df, x="ethnicity", y="height") + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_selection/data.py b/demo/plot_guide_selection/data.py new file mode 100644 index 0000000..820e047 --- /dev/null +++ b/demo/plot_guide_selection/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_selection/run.py b/demo/plot_guide_selection/run.py new file mode 100644 index 0000000..1fc8db5 --- /dev/null +++ b/demo/plot_guide_selection/run.py @@ -0,0 +1,15 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + plt = gr.LinePlot(df, x="weight", y="height") + selection_total = gr.Number(label="Total Weight of Selection") + + def select_region(selection: gr.SelectData): + min_w, max_w = selection.index + return df[(df["weight"] >= min_w) & (df["weight"] <= max_w)]["weight"].sum() + + plt.select(select_region, None, selection_total) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_series_nominal/data.py b/demo/plot_guide_series_nominal/data.py new file mode 100644 index 0000000..820e047 --- /dev/null +++ b/demo/plot_guide_series_nominal/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_series_nominal/run.py b/demo/plot_guide_series_nominal/run.py new file mode 100644 index 0000000..7397501 --- /dev/null +++ b/demo/plot_guide_series_nominal/run.py @@ -0,0 +1,8 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + gr.ScatterPlot(df, x="weight", y="height", color="ethnicity") + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_series_quantitative/data.py b/demo/plot_guide_series_quantitative/data.py new file mode 100644 index 0000000..820e047 --- /dev/null +++ b/demo/plot_guide_series_quantitative/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_series_quantitative/run.py b/demo/plot_guide_series_quantitative/run.py new file mode 100644 index 0000000..fbf037b --- /dev/null +++ b/demo/plot_guide_series_quantitative/run.py @@ -0,0 +1,8 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + gr.ScatterPlot(df, x="weight", y="height", color="age") + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_tables_stats/data.py b/demo/plot_guide_tables_stats/data.py new file mode 100644 index 0000000..e9c8e08 --- /dev/null +++ b/demo/plot_guide_tables_stats/data.py @@ -0,0 +1,13 @@ +import pandas as pd +import numpy as np +import random +from datetime import datetime, timedelta + +now = datetime.now() + +df = pd.DataFrame({ + 'time': [now - timedelta(minutes=5*i) for i in range(25)], + 'price': np.random.randint(100, 1000, 25), + 'origin': [random.choice(["DFW", "DAL", "HOU"]) for _ in range(25)], + 'destination': [random.choice(["JFK", "LGA", "EWR"]) for _ in range(25)], +}) diff --git a/demo/plot_guide_tables_stats/run.py b/demo/plot_guide_tables_stats/run.py new file mode 100644 index 0000000..72b163d --- /dev/null +++ b/demo/plot_guide_tables_stats/run.py @@ -0,0 +1,12 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + with gr.Row(): + gr.Label(len(df), label="Flight Count") + gr.Label(f"${df['price'].min()}", label="Cheapest Flight") + gr.DataFrame(df) + + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_temporal/requirements.txt b/demo/plot_guide_temporal/requirements.txt new file mode 100644 index 0000000..5da331c --- /dev/null +++ b/demo/plot_guide_temporal/requirements.txt @@ -0,0 +1,2 @@ +numpy +pandas diff --git a/demo/plot_guide_temporal/run.py b/demo/plot_guide_temporal/run.py new file mode 100644 index 0000000..763d0d8 --- /dev/null +++ b/demo/plot_guide_temporal/run.py @@ -0,0 +1,21 @@ +import gradio as gr +import pandas as pd +import numpy as np +import random + +from datetime import datetime, timedelta +now = datetime.now() + +df = pd.DataFrame({ + 'time': [now - timedelta(minutes=5*i) for i in range(25)], + 'price': np.random.randint(100, 1000, 25), + 'origin': [random.choice(["DFW", "DAL", "HOU"]) for _ in range(25)], + 'destination': [random.choice(["JFK", "LGA", "EWR"]) for _ in range(25)], +}) + +with gr.Blocks() as demo: + gr.LinePlot(df, x="time", y="price") + gr.ScatterPlot(df, x="time", y="price", color="origin") + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_zoom/data.py b/demo/plot_guide_zoom/data.py new file mode 100644 index 0000000..820e047 --- /dev/null +++ b/demo/plot_guide_zoom/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_zoom/run.py b/demo/plot_guide_zoom/run.py new file mode 100644 index 0000000..2e1545b --- /dev/null +++ b/demo/plot_guide_zoom/run.py @@ -0,0 +1,15 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + plt = gr.LinePlot(df, x="weight", y="height") + + def select_region(selection: gr.SelectData): + min_w, max_w = selection.index + return gr.LinePlot(x_lim=(min_w, max_w)) # type: ignore + + plt.select(select_region, None, plt) + plt.double_click(lambda: gr.LinePlot(x_lim=None), None, plt) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/plot_guide_zoom_sync/data.py b/demo/plot_guide_zoom_sync/data.py new file mode 100644 index 0000000..820e047 --- /dev/null +++ b/demo/plot_guide_zoom_sync/data.py @@ -0,0 +1,10 @@ +import pandas as pd +import numpy as np +import random + +df = pd.DataFrame({ + 'height': np.random.randint(50, 70, 25), + 'weight': np.random.randint(120, 320, 25), + 'age': np.random.randint(18, 65, 25), + 'ethnicity': [random.choice(["white", "black", "asian"]) for _ in range(25)] +}) diff --git a/demo/plot_guide_zoom_sync/run.py b/demo/plot_guide_zoom_sync/run.py new file mode 100644 index 0000000..ee28095 --- /dev/null +++ b/demo/plot_guide_zoom_sync/run.py @@ -0,0 +1,18 @@ +import gradio as gr +from data import df # type: ignore + +with gr.Blocks() as demo: + plt1 = gr.LinePlot(df, x="weight", y="height") + plt2 = gr.BarPlot(df, x="weight", y="age", x_bin=10) + plots = [plt1, plt2] + + def select_region(selection: gr.SelectData): + min_w, max_w = selection.index + return [gr.LinePlot(x_lim=(min_w, max_w))] * len(plots) # type: ignore + + for plt in plots: + plt.select(select_region, None, plots) + plt.double_click(lambda: [gr.LinePlot(x_lim=None)] * len(plots), None, plots) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/progress/requirements.txt b/demo/progress/requirements.txt new file mode 100644 index 0000000..8f2403e --- /dev/null +++ b/demo/progress/requirements.txt @@ -0,0 +1,2 @@ +tqdm +datasets \ No newline at end of file diff --git a/demo/progress/run.py b/demo/progress/run.py new file mode 100644 index 0000000..33b6cc5 --- /dev/null +++ b/demo/progress/run.py @@ -0,0 +1,96 @@ +import gradio as gr +import random +import time +import tqdm +from datasets import load_dataset # type: ignore +import shutil +from uuid import uuid4 + +with gr.Blocks() as demo: + with gr.Row(): + text = gr.Textbox() + textb = gr.Textbox() + with gr.Row(): + load_set_btn = gr.Button("Load Set") + load_nested_set_btn = gr.Button("Load Nested Set") + load_random_btn = gr.Button("Load Random") + clean_imgs_btn = gr.Button("Clean Images") + wait_btn = gr.Button("Wait") + do_all_btn = gr.Button("Do All") + track_tqdm_btn = gr.Button("Bind TQDM") + bind_internal_tqdm_btn = gr.Button("Bind Internal TQDM") + + text2 = gr.Textbox() + + # track list + def load_set(text, text2, progress=gr.Progress()): + imgs = [None] * 24 + for img in progress.tqdm(imgs, desc="Loading from list"): + time.sleep(0.1) + return "done" + load_set_btn.click(load_set, [text, textb], text2) + + # track nested list + def load_nested_set(text, text2, progress=gr.Progress()): + imgs = [[None] * 8] * 3 + for img_set in progress.tqdm(imgs, desc="Nested list"): + time.sleep(2) + for img in progress.tqdm(img_set, desc="inner list"): + time.sleep(0.1) + return "done" + load_nested_set_btn.click(load_nested_set, [text, textb], text2) + + # track iterable of unknown length + def load_random(data, progress=gr.Progress()): + def yielder(): + for i in range(0, random.randint(15, 20)): + time.sleep(0.1) + yield None + for img in progress.tqdm(yielder()): + pass + return "done" + load_random_btn.click(load_random, {text, textb}, text2) + + # manual progress + def clean_imgs(text, progress=gr.Progress()): + progress(0.2, desc="Collecting Images") + time.sleep(1) + progress(0.5, desc="Cleaning Images") + time.sleep(1.5) + progress(0.8, desc="Sending Images") + time.sleep(1.5) + return "done" + clean_imgs_btn.click(clean_imgs, text, text2) + + # no progress + def wait(text): + time.sleep(4) + return "done" + wait_btn.click(wait, text, text2) + + # multiple progressions + def do_all(data, progress=gr.Progress()): + load_set(data[text], data[textb], progress) + load_random(data, progress) + clean_imgs(data[text], progress) + progress(None) + wait(text) + return "done" + do_all_btn.click(do_all, {text, textb}, text2) + + def track_tqdm(data, progress=gr.Progress(track_tqdm=True)): + for i in tqdm.tqdm(range(5), desc="outer"): + for j in tqdm.tqdm(range(4), desc="inner"): + time.sleep(1) + return "done" + track_tqdm_btn.click(track_tqdm, {text, textb}, text2) + + def bind_internal_tqdm(data, progress=gr.Progress(track_tqdm=True)): + outdir = "__tmp/" + str(uuid4()) + load_dataset("beans", split="train", cache_dir=outdir) + shutil.rmtree(outdir) + return "done" + bind_internal_tqdm_btn.click(bind_internal_tqdm, {text, textb}, text2) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/progress_component/requirements.txt b/demo/progress_component/requirements.txt new file mode 100644 index 0000000..fa9cf06 --- /dev/null +++ b/demo/progress_component/requirements.txt @@ -0,0 +1 @@ +tqdm \ No newline at end of file diff --git a/demo/progress_component/run.py b/demo/progress_component/run.py new file mode 100644 index 0000000..e824b2e --- /dev/null +++ b/demo/progress_component/run.py @@ -0,0 +1,15 @@ +import gradio as gr +import time + +def load_set(progress=gr.Progress()): + imgs = [None] * 24 + for img in progress.tqdm(imgs, desc="Loading..."): + time.sleep(0.1) + return "Loaded" + +with gr.Blocks() as demo: + load = gr.Button("Load") + label = gr.Label(label="Loader") + load.click(load_set, outputs=label) + +demo.launch() diff --git a/demo/progress_simple/run.py b/demo/progress_simple/run.py new file mode 100644 index 0000000..e3acd8e --- /dev/null +++ b/demo/progress_simple/run.py @@ -0,0 +1,17 @@ +import gradio as gr +import time + +def slowly_reverse(word, progress=gr.Progress()): + progress(0, desc="Starting") + time.sleep(1) + progress(0.05) + new_string = "" + for letter in progress.tqdm(word, desc="Reversing"): + time.sleep(0.25) + new_string = letter + new_string # type: ignore + return new_string + +demo = gr.Interface(slowly_reverse, gr.Text(), gr.Text(), api_name="predict") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/queue_full_e2e_test/run.py b/demo/queue_full_e2e_test/run.py new file mode 100644 index 0000000..140d013 --- /dev/null +++ b/demo/queue_full_e2e_test/run.py @@ -0,0 +1,37 @@ +import gradio as gr +import time +import random + +n_calls = 0 + +def get_random_number(): + global n_calls + if n_calls == 1: + n_calls += 1 + raise gr.Error("This is a gradio error") + n_calls += 1 + time.sleep(5) + return random.randrange(1, 10) + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + first = gr.Button("First Call") + second = gr.Button("Second Call") + third = gr.Button("Third Call") + fourth = gr.Button("Fourth Call") + with gr.Column(): + first_o = gr.Number(label="First Result") + second_o = gr.Number(label="Second Result") + third_o = gr.Number(label="Third Result") + fourth_o = gr.Number(label="Fourth Result") + + first.click(get_random_number, None, first_o, concurrency_id="f") + second.click(get_random_number, None, second_o, concurrency_id="f") + third.click(get_random_number, None, third_o, concurrency_id="f") + fourth.click(get_random_number, None, fourth_o, concurrency_id="f") + +demo.queue(max_size=2) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/radio_component/run.py b/demo/radio_component/run.py new file mode 100644 index 0000000..0abbccd --- /dev/null +++ b/demo/radio_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Radio(choices=["First Choice", "Second Choice", "Third Choice"]) + +demo.launch() diff --git a/demo/random_demos.py b/demo/random_demos.py new file mode 100644 index 0000000..c5d25fe --- /dev/null +++ b/demo/random_demos.py @@ -0,0 +1,38 @@ +"""Opens X demos randomly for quick inspection + +Usage: python random_demos.py +Example: python random_demos.py 8 +""" + +from __future__ import annotations + +import argparse +import importlib +import pathlib +import os +import random + +import gradio as gr + +parser = argparse.ArgumentParser() +parser.add_argument("num_demos", help="number of demos to launch", type=int, default=4) +args = parser.parse_args() + +# get the list of directory names +demos_list = next(os.walk(pathlib.Path(__file__).parent))[1] + +# Some demos are just too large or need to be run in a special way, so we'll just skip them +large_demos = ['streaming_wav2vec', 'blocks_neural_instrument_coding', '.gradio/flagged'] +for large_demo in large_demos: + if large_demo in demos_list: + demos_list.remove(large_demo) + +for d, demo_name in enumerate(random.sample(demos_list, args.num_demos)): + print(f"Launching demo {d+1}/{args.num_demos}: {demo_name}") + # import the run.py file from inside the directory specified by args.demo_name + run = importlib.import_module(f"{demo_name}.run") + demo: gr.Blocks = run.demo # type: ignore + if d == args.num_demos - 1: + demo.launch(prevent_thread_lock=False, inbrowser=True) # prevent main thread from exiting + else: + demo.launch(prevent_thread_lock=True, inbrowser=True) diff --git a/demo/rapid_generation/run.py b/demo/rapid_generation/run.py new file mode 100644 index 0000000..3d967b0 --- /dev/null +++ b/demo/rapid_generation/run.py @@ -0,0 +1,44 @@ +import gradio as gr + +with gr.Blocks() as demo: + chatbot = gr.Chatbot(elem_id="chatbot") + + with gr.Row(): + num1 = gr.Number(label="a") + num2 = gr.Number(label="b") + with gr.Row(): + num3 = gr.Number(label="c") + num4 = gr.Number(label="d") + + btn = gr.Button("Start") + + def add_user(history): + new_response = {"role": "user", "content": ""} + history.append(new_response) + for _ in range(10): + new_response["content"] += f"{len(history)} " + yield history + + def add_bot(history): + response = {"role": "assistant", "content": ""} + history.append(response) + for _ in range(10): + response["content"] += f"{len(history)} " + yield history + + chat_evt = btn.click(add_user, chatbot, chatbot).then(add_bot, chatbot, chatbot) + for _ in range(10): + chat_evt = chat_evt.then(add_user, chatbot, chatbot).then( + add_bot, chatbot, chatbot + ) + + increase = lambda x: x + 1 + + btn_evt = btn.click(increase, num1, num2).then(increase, num2, num1) + btn_evt2 = btn.click(increase, num3, num4).then(increase, num4, num3) + for _ in range(10): + btn_evt = btn_evt.then(increase, num1, num2).then(increase, num2, num1) + btn_evt2 = btn_evt2.then(increase, num3, num4).then(increase, num4, num3) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/rate_limit/run.py b/demo/rate_limit/run.py new file mode 100644 index 0000000..bbcf1f0 --- /dev/null +++ b/demo/rate_limit/run.py @@ -0,0 +1,114 @@ +import gradio as gr +from datetime import datetime, timedelta +from collections import defaultdict +import threading + +rate_limit_data = defaultdict(list) +lock = threading.Lock() + +UNAUTH_RATE_LIMIT = 3 +AUTH_RATE_LIMIT = 30 +RATE_LIMIT_WINDOW = 60 + +def clean_old_entries(user_id): + """Remove entries older than the rate limit window""" + current_time = datetime.now() + cutoff_time = current_time - timedelta(seconds=RATE_LIMIT_WINDOW) + rate_limit_data[user_id] = [ + timestamp for timestamp in rate_limit_data[user_id] + if timestamp > cutoff_time + ] + +def get_user_identifier(profile: gr.OAuthProfile | None, request: gr.Request) -> tuple[str, bool]: + """Get user identifier and whether they're authenticated""" + if profile is not None: + return profile.username, True + else: + if request: + return f"ip_{request.client.host}", False + return "ip_unknown", False + +def check_rate_limit(user_id: str, is_authenticated: bool) -> tuple[bool, int, int]: + """ + Check if user has exceeded rate limit + Returns: (can_proceed, clicks_used, max_clicks) + """ + with lock: + clean_old_entries(user_id) + + max_clicks = AUTH_RATE_LIMIT if is_authenticated else UNAUTH_RATE_LIMIT + clicks_used = len(rate_limit_data[user_id]) + + can_proceed = clicks_used < max_clicks + + return can_proceed, clicks_used, max_clicks + +def add_click(user_id: str): + """Add a click timestamp for the user""" + with lock: + rate_limit_data[user_id].append(datetime.now()) + +def update_status(profile: gr.OAuthProfile | None, request: gr.Request) -> str: + """Update the status message showing current rate limit info""" + user_id, is_authenticated = get_user_identifier(profile, request) + _, clicks_used, max_clicks = check_rate_limit(user_id, is_authenticated) + + if is_authenticated: + return f"✅ You are logged in as '{profile.username}'. You have clicked {clicks_used} times this minute. You have {max_clicks} total clicks per minute." # type: ignore + else: + return f"⚠️ You are not logged in. You have clicked {clicks_used} times this minute. You have {max_clicks} total clicks per minute." + +def run_action(profile: gr.OAuthProfile | None, request: gr.Request) -> tuple[str, str]: + """Handle the run button click with rate limiting""" + user_id, is_authenticated = get_user_identifier(profile, request) + can_proceed, clicks_used, max_clicks = check_rate_limit(user_id, is_authenticated) + + if not can_proceed: + result = f"❌ Rate limit exceeded! You've used all {max_clicks} clicks for this minute. Please wait before trying again." + status = update_status(profile, request) + return result, status + + add_click(user_id) + + _, new_clicks_used, _ = check_rate_limit(user_id, is_authenticated) + + result = f"✅ Action executed successfully! (Click #{new_clicks_used})" + status = update_status(profile, request) + + return result, status + +with gr.Blocks(title="Rate Limiting Demo") as demo: + gr.Markdown("# Rate Limiting Demo App") + gr.Markdown("This app demonstrates rate limiting based on authentication status.") + + gr.LoginButton() + + status_text = gr.Markdown("Loading status...") + + with gr.Row(): + run_btn = gr.Button("🚀 Run Action", variant="primary", scale=1) + + result_text = gr.Markdown("") + + demo.load(update_status, inputs=None, outputs=status_text) + + run_btn.click( + run_action, + inputs=None, + outputs=[result_text, status_text] + ) + + gr.Markdown("---") + gr.Markdown(""" + ### Rate Limits: + - **Not logged in:** 3 clicks per minute (based on IP address) + - **Logged in:** 30 clicks per minute (based on HF username) + + ### How it works: + - Click the **Login** button to authenticate with Hugging Face + - Click the **Run Action** button to test the rate limiting + - The system tracks your clicks over a rolling 1-minute window + """) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/reload_mode/functions.py b/demo/reload_mode/functions.py new file mode 100644 index 0000000..cd391eb --- /dev/null +++ b/demo/reload_mode/functions.py @@ -0,0 +1,8 @@ +import gradio as gr + +if gr.NO_RELOAD: + def get_status(): # type: ignore + return "full" +else: + def get_status(): + return "reloaded" diff --git a/demo/reload_mode/run.py b/demo/reload_mode/run.py new file mode 100644 index 0000000..85cd7e6 --- /dev/null +++ b/demo/reload_mode/run.py @@ -0,0 +1,23 @@ +import gradio as gr +from functions import get_status # type: ignore + +if gr.NO_RELOAD: + def eat(food): # type: ignore + if food > 0: + return {food_box: food - 1, status_box: "full"} + else: + return {status_box: "hungry"} +else: + def eat(food): + return {status_box: get_status()} + + +with gr.Blocks() as demo: + food_box = gr.Number(value=10, label="Food Count!!") + status_box = gr.Textbox(label="Status") + + gr.Button("Eat").click(fn=eat, + inputs=food_box, + outputs=[food_box, status_box]) + +demo.launch() diff --git a/demo/render_heavy_concurrently/run.py b/demo/render_heavy_concurrently/run.py new file mode 100644 index 0000000..798ced2 --- /dev/null +++ b/demo/render_heavy_concurrently/run.py @@ -0,0 +1,21 @@ +import gradio as gr +import random + +with gr.Blocks() as demo: + + with gr.Row(): + + @gr.render() + def render(): + for _ in range(500): + gr.Textbox(str(random.randint(0, 100))) + gr.Button("DONE 1") + + @gr.render() + def render2(): + for _ in range(500): + gr.Textbox(str(random.randint(0, 100))) + gr.Button("DONE 2") + +if __name__ == "__main__": + demo.queue(default_concurrency_limit=64).launch() diff --git a/demo/render_merge/run.py b/demo/render_merge/run.py new file mode 100644 index 0000000..1f37106 --- /dev/null +++ b/demo/render_merge/run.py @@ -0,0 +1,40 @@ +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() + +if __name__ == "__main__": + demo.launch() diff --git a/demo/render_merge_simple/run.py b/demo/render_merge_simple/run.py new file mode 100644 index 0000000..4632d58 --- /dev/null +++ b/demo/render_merge_simple/run.py @@ -0,0 +1,24 @@ +import gradio as gr + +with gr.Blocks() as demo: + text_count = gr.State(1) + add_btn = gr.Button("Add Box") + add_btn.click(lambda x: x + 1, text_count, text_count) + + @gr.render(inputs=text_count) + def render_count(count): + boxes = [] + for i in range(count): + box = gr.Textbox(key=i, label=f"Box {i}") + boxes.append(box) + + def merge(*args): + return " ".join(args) + + merge_btn.click(merge, boxes, output) + + merge_btn = gr.Button("Merge") + output = gr.Textbox(label="Merged Output") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/render_queue_false/run.py b/demo/render_queue_false/run.py new file mode 100644 index 0000000..5c52a4a --- /dev/null +++ b/demo/render_queue_false/run.py @@ -0,0 +1,19 @@ +import gradio as gr + +with gr.Blocks() as demo: + input_text = gr.Textbox(label="Input Text") + + @gr.render(inputs=input_text, queue=False) + def show_split(text): + if len(text) == 0: + gr.Markdown("## No Input Provided") + else: + for letter in text: + with gr.Row(): + text = gr.Textbox(letter, label=f"Letter {letter}") + btn = gr.Button("Clear") + btn.click(lambda: gr.Textbox(value=""), None, text) + + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/render_split/run.py b/demo/render_split/run.py new file mode 100644 index 0000000..f65b120 --- /dev/null +++ b/demo/render_split/run.py @@ -0,0 +1,19 @@ +import gradio as gr + +with gr.Blocks() as demo: + input_text = gr.Textbox(label="input") + mode = gr.Radio(["textbox", "button"], value="textbox") + + @gr.render(inputs=[input_text, mode], triggers=[input_text.submit]) + def show_split(text, mode): + if len(text) == 0: + gr.Markdown("## No Input Provided") + else: + for letter in text: + if mode == "textbox": + gr.Textbox(letter) + else: + gr.Button(letter) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/render_split_simple/run.py b/demo/render_split_simple/run.py new file mode 100644 index 0000000..0863241 --- /dev/null +++ b/demo/render_split_simple/run.py @@ -0,0 +1,15 @@ +import gradio as gr + +with gr.Blocks() as demo: + input_text = gr.Textbox(label="input") + + @gr.render(inputs=input_text) + def show_split(text): + if len(text) == 0: + gr.Markdown("## No Input Provided") + else: + for letter in text: + gr.Textbox(letter) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/render_tests/run.py b/demo/render_tests/run.py new file mode 100644 index 0000000..8ce496f --- /dev/null +++ b/demo/render_tests/run.py @@ -0,0 +1,89 @@ +from datetime import datetime + +import time +import gradio as gr + +def update_log(): + return datetime.now().timestamp() + +def get_target(evt: gr.EventData): + return evt.target + +def get_select_index(evt: gr.SelectData): + return evt.index + +with gr.Blocks() as demo: + gr.Textbox(value=update_log, every=0.5, label="Time") + + slider = gr.Slider(1, 10, step=1, label="Slider") + @gr.render(inputs=[slider]) + def show_log(s): + with gr.Row(): + for i in range(s): + gr.Textbox(value=update_log, every=0.5, label=f"Render {i + 1}") + + slider2 = gr.Slider(1, 10, step=1, label="Box Count") + btn = gr.Button("Create Boxes") + @gr.render(inputs=[slider2], triggers=[btn.click]) + def show_log_2(s): + for i in range(s): + gr.Textbox(value=str(i), label=f"Count {i + 1}") + + with gr.Row(): + selected_btn = gr.Textbox(label="Selected Button") + selected_chat = gr.Textbox(label="Selected Chat") + @gr.render(inputs=[slider]) + def show_buttons(s): + with gr.Row(): + with gr.Column(): + for i in range(s): + btn = gr.Button(f"Button {i + 1}") + btn.click(get_target, None, selected_btn) + chatbot = gr.Chatbot([{"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Hi"}, {"role": "user", "content": "How are you?"}, {"role": "assistant", "content": "I'm good."}]) # type: ignore + chatbot.select(get_select_index, None, selected_chat) + + selectable_chat = gr.Chatbot([{"role": "user", "content": "chat1"}, {"role": "assistant", "content": "chat2"}, {"role": "user", "content": "chat3"}, {"role": "assistant", "content": "chat4"}]) # type: ignore + + @gr.render(triggers=[selectable_chat.select]) + def show_selected_chat(selection: gr.SelectData): + gr.Textbox(label="Trigger Index", value=selection.index) + + @gr.render() + def examples_in_interface(): + gr.Interface(lambda x:x, gr.Textbox(label="input"), gr.Textbox(), examples=[["test"]]) + + @gr.render() + def examples_in_blocks(): + a = gr.Textbox(label="little textbox") + gr.Examples([["abc"], ["def"]], [a]) + + choices_count = gr.Slider(1, 10, 3, step=1, label="Choices") + @gr.render(choices_count) + def show_choices(count): + with gr.Row(): + letter_choices = gr.Radio(list('abcdefghij')[:int(count)], label="Choices", key="choices", preserved_by_key=["value", "label"], interactive=True) + textbox = gr.Textbox(label="Set Label", value="Choices") + + textbox.change(lambda l: gr.Radio(label=l), textbox, letter_choices) + + timer = gr.Timer(0.5) + + @gr.render(triggers=[timer.tick], show_progress="hidden") + def render(): + a = gr.Textbox(label="box-a", key="a") + b = gr.Textbox(label="box-b", key="b") + a.change(lambda x: time.sleep(0.5) or x, a, b, key="a-b") + gr.Number(value=round(time.time(), 2)) + + + render_tab = gr.Button("Render Tab") + @gr.render(triggers=[render_tab.click]) + def test_render(): + with gr.Tabs(): + with gr.Tab(): + gr.Textbox("This is a rendered tab", label="Rendered Tab") + with gr.Tab(): + gr.Textbox("Another Tab", label="Another Tab") + +if __name__ == '__main__': + demo.launch() diff --git a/demo/render_visibility/run.py b/demo/render_visibility/run.py new file mode 100644 index 0000000..6c65cee --- /dev/null +++ b/demo/render_visibility/run.py @@ -0,0 +1,44 @@ +import gradio as gr + + +def greet(name): + return "Hello " + name + "!" + + +with gr.Blocks() as demo: + with gr.Tabs(): + with gr.TabItem("Tab 1"): + t = gr.Textbox("Some value", label="Name", visible=False, interactive=True) + with gr.Row(): + show_btn = gr.Button("Show") + show_btn.click(lambda: gr.Textbox(visible=True), inputs=None, outputs=t) + hide_btn = gr.Button("Hide") + hide_btn.click(lambda: gr.Textbox(visible=False), inputs=None, outputs=t) + + with gr.TabItem("Tab 2"): + t2 = gr.Textbox( + "Some other value", label="Name", visible=False, interactive=True + ) + with gr.Row(): + show_btn2 = gr.Button("Show") + show_btn2.click( + lambda: gr.Textbox(visible=True), inputs=None, outputs=t2 + ) + hide_btn2 = gr.Button("Hide") + hide_btn2.click( + lambda: gr.Textbox(visible=False), inputs=None, outputs=t2 + ) + with gr.TabItem("Tab 3"): + t3 = gr.ImageEditor(label="Name", visible=False, interactive=True) + with gr.Row(): + show_btn3 = gr.Button("Show") + show_btn3.click( + lambda: gr.Textbox(visible=True), inputs=None, outputs=t3 + ) + hide_btn3 = gr.Button("Hide") + hide_btn3.click( + lambda: gr.Textbox(visible=False), inputs=None, outputs=t3 + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/request_ip_headers/run.py b/demo/request_ip_headers/run.py new file mode 100644 index 0000000..7470229 --- /dev/null +++ b/demo/request_ip_headers/run.py @@ -0,0 +1,13 @@ +import gradio as gr + +def predict(text, request: gr.Request): + headers = request.headers + host = request.client.host + user_agent = request.headers["user-agent"] + return { + "ip": host, + "user_agent": user_agent, + "headers": headers, + } + +gr.Interface(predict, "text", "json", api_name="predict").launch() diff --git a/demo/reverse_audio/requirements.txt b/demo/reverse_audio/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/reverse_audio/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/reverse_audio/run.py b/demo/reverse_audio/run.py new file mode 100644 index 0000000..63eb477 --- /dev/null +++ b/demo/reverse_audio/run.py @@ -0,0 +1,27 @@ + +import numpy as np + +import gradio as gr + +def reverse_audio(audio): + sr, data = audio + return (sr, np.flipud(data)) + +input_audio = gr.Audio( + sources=["microphone"], + waveform_options=gr.WaveformOptions( + waveform_color="#01C6FF", + waveform_progress_color="#0066B4", + skip_length=2, + show_recording_waveform=False, + ), +) +demo = gr.Interface( + fn=reverse_audio, + inputs=input_audio, + outputs="audio", + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/reverse_audio/screenshot.png b/demo/reverse_audio/screenshot.png new file mode 100644 index 0000000..7961516 Binary files /dev/null and b/demo/reverse_audio/screenshot.png differ diff --git a/demo/reverse_audio_2/requirements.txt b/demo/reverse_audio_2/requirements.txt new file mode 100644 index 0000000..296d654 --- /dev/null +++ b/demo/reverse_audio_2/requirements.txt @@ -0,0 +1 @@ +numpy \ No newline at end of file diff --git a/demo/reverse_audio_2/run.py b/demo/reverse_audio_2/run.py new file mode 100644 index 0000000..83b6546 --- /dev/null +++ b/demo/reverse_audio_2/run.py @@ -0,0 +1,13 @@ +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") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/reversible_flow/run.py b/demo/reversible_flow/run.py new file mode 100644 index 0000000..c19c9bb --- /dev/null +++ b/demo/reversible_flow/run.py @@ -0,0 +1,15 @@ +import gradio as gr + +def increase(num): + return num + 1 + +with gr.Blocks() as demo: + a = gr.Number(label="a") + b = gr.Number(label="b") + atob = gr.Button("a > b") + btoa = gr.Button("b > a") + atob.click(increase, a, b) + btoa.click(increase, b, a) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/rows_and_columns/run.py b/demo/rows_and_columns/run.py new file mode 100644 index 0000000..b065098 --- /dev/null +++ b/demo/rows_and_columns/run.py @@ -0,0 +1,20 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + text1 = gr.Textbox(label="t1") + slider2 = gr.Textbox(label="s2") + drop3 = gr.Dropdown(["a", "b", "c"], label="d3") + with gr.Row(): + with gr.Column(scale=1, min_width=300): + text1 = gr.Textbox(label="prompt 1") + text2 = gr.Textbox(label="prompt 2") + inbtw = gr.Button("Between") + text4 = gr.Textbox(label="prompt 1") + text5 = gr.Textbox(label="prompt 2") + with gr.Column(scale=2, min_width=300): + img1 = gr.Image("images/cheetah.jpg") + btn = gr.Button("Go") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/rt-detr-object-detection/3285790-hd_1920_1080_30fps.mp4 b/demo/rt-detr-object-detection/3285790-hd_1920_1080_30fps.mp4 new file mode 100644 index 0000000..4f1821d Binary files /dev/null and b/demo/rt-detr-object-detection/3285790-hd_1920_1080_30fps.mp4 differ diff --git a/demo/rt-detr-object-detection/draw_boxes.py b/demo/rt-detr-object-detection/draw_boxes.py new file mode 100644 index 0000000..9a0442e --- /dev/null +++ b/demo/rt-detr-object-detection/draw_boxes.py @@ -0,0 +1,45 @@ +from PIL import ImageDraw, ImageFont # type: ignore +import colorsys + + +def get_color(label): + # Simple hash function to generate consistent colors for each label + hash_value = hash(label) + hue = (hash_value % 100) / 100.0 + saturation = 0.7 + value = 0.9 + rgb = colorsys.hsv_to_rgb(hue, saturation, value) + return tuple(int(x * 255) for x in rgb) + + +def draw_bounding_boxes(image, results: dict, model, threshold=0.3): + draw = ImageDraw.Draw(image) + font = ImageFont.load_default() + + for score, label_id, box in zip( + results["scores"], results["labels"], results["boxes"] + ): + if score > threshold: + label = model.config.id2label[label_id.item()] + box = [round(i, 2) for i in box.tolist()] + color = get_color(label) + + # Draw bounding box + draw.rectangle(box, outline=color, width=3) # type: ignore + + # Prepare text + text = f"{label}: {score:.2f}" + text_bbox = draw.textbbox((0, 0), text, font=font) + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] + + # Draw text background + draw.rectangle( + [box[0], box[1] - text_height - 4, box[0] + text_width, box[1]], # type: ignore + fill=color, # type: ignore + ) + + # Draw text + draw.text((box[0], box[1] - text_height - 4), text, fill="white", font=font) + + return image diff --git a/demo/rt-detr-object-detection/requirements.txt b/demo/rt-detr-object-detection/requirements.txt new file mode 100644 index 0000000..b1c8b94 --- /dev/null +++ b/demo/rt-detr-object-detection/requirements.txt @@ -0,0 +1,5 @@ +safetensors==0.4.3 +opencv-python +torch +transformers>=4.43.0 +Pillow diff --git a/demo/rt-detr-object-detection/run.py b/demo/rt-detr-object-detection/run.py new file mode 100644 index 0000000..4c320f1 --- /dev/null +++ b/demo/rt-detr-object-detection/run.py @@ -0,0 +1,121 @@ +# type: ignore +import spaces +import gradio as gr +import cv2 # type: ignore +from PIL import Image +import torch +import time +import numpy as np +import uuid + +from transformers import RTDetrForObjectDetection, RTDetrImageProcessor # type: ignore + +from draw_boxes import draw_bounding_boxes + +image_processor = RTDetrImageProcessor.from_pretrained("PekingU/rtdetr_r50vd") +model = RTDetrForObjectDetection.from_pretrained("PekingU/rtdetr_r50vd").to("cuda") + + +SUBSAMPLE = 2 + + +@spaces.GPU +def stream_object_detection(video, conf_threshold): + cap = cv2.VideoCapture(video) + + video_codec = cv2.VideoWriter_fourcc(*"mp4v") # type: ignore + fps = int(cap.get(cv2.CAP_PROP_FPS)) + + desired_fps = fps // SUBSAMPLE + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) // 2 + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) // 2 + + iterating, frame = cap.read() + + n_frames = 0 + + name = f"output_{uuid.uuid4()}.mp4" + segment_file = cv2.VideoWriter(name, video_codec, desired_fps, (width, height)) # type: ignore + batch = [] + + while iterating: + frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + if n_frames % SUBSAMPLE == 0: + batch.append(frame) + if len(batch) == 2 * desired_fps: + inputs = image_processor(images=batch, return_tensors="pt").to("cuda") + + print(f"starting batch of size {len(batch)}") + start = time.time() + with torch.no_grad(): + outputs = model(**inputs) + end = time.time() + print("time taken for inference", end - start) + + start = time.time() + boxes = image_processor.post_process_object_detection( + outputs, + target_sizes=torch.tensor([(height, width)] * len(batch)), + threshold=conf_threshold, + ) + + for _, (array, box) in enumerate(zip(batch, boxes)): + pil_image = draw_bounding_boxes( + Image.fromarray(array), box, model, conf_threshold + ) + frame = np.array(pil_image) + # Convert RGB to BGR + frame = frame[:, :, ::-1].copy() + segment_file.write(frame) + + batch = [] + segment_file.release() + yield name + end = time.time() + print("time taken for processing boxes", end - start) + name = f"output_{uuid.uuid4()}.mp4" + segment_file = cv2.VideoWriter( + name, video_codec, desired_fps, (width, height) + ) # type: ignore + + iterating, frame = cap.read() + n_frames += 1 + + +with gr.Blocks() as demo: + gr.HTML( + """ +

+ Video Object Detection with RT-DETR +

+ """ + ) + with gr.Row(): + with gr.Column(): + video = gr.Video(label="Video Source") + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + with gr.Column(): + output_video = gr.Video( + label="Processed Video", streaming=True, autoplay=True + ) + + video.upload( + fn=stream_object_detection, + inputs=[video, conf_threshold], + outputs=[output_video], + ) + + gr.Examples( + examples=["3285790-hd_1920_1080_30fps.mp4"], + inputs=[video], + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/sales_projections/requirements.txt b/demo/sales_projections/requirements.txt new file mode 100644 index 0000000..44974cf --- /dev/null +++ b/demo/sales_projections/requirements.txt @@ -0,0 +1,3 @@ +pandas +numpy +matplotlib \ No newline at end of file diff --git a/demo/sales_projections/run.py b/demo/sales_projections/run.py new file mode 100644 index 0000000..9ae38cc --- /dev/null +++ b/demo/sales_projections/run.py @@ -0,0 +1,35 @@ +import matplotlib.pyplot as plt +import numpy as np + +import gradio as gr + +def sales_projections(employee_data): + sales_data = employee_data.iloc[:, 1:4].astype("int").to_numpy() + regression_values = np.apply_along_axis( + lambda row: np.array(np.poly1d(np.polyfit([0, 1, 2], row, 2))), 0, sales_data + ) + projected_months = np.repeat( + np.expand_dims(np.arange(3, 12), 0), len(sales_data), axis=0 + ) + projected_values = np.array( + [ + month * month * regression[0] + month * regression[1] + regression[2] + for month, regression in zip(projected_months, regression_values) + ] + ) + plt.plot(projected_values.T) + plt.legend(employee_data["Name"]) + return employee_data, plt.gcf(), regression_values + +demo = gr.Interface( + sales_projections, + gr.Dataframe( + headers=["Name", "Jan Sales", "Feb Sales", "Mar Sales"], + value=[["Jon", 12, 14, 18], ["Alice", 14, 17, 2], ["Sana", 8, 9.5, 12]], + ), + ["dataframe", "plot", "numpy"], + description="Enter sales figures for employees to predict sales trajectory over year.", + api_name="predict" +) +if __name__ == "__main__": + demo.launch() diff --git a/demo/sales_projections/screenshot.gif b/demo/sales_projections/screenshot.gif new file mode 100644 index 0000000..c13ffbc Binary files /dev/null and b/demo/sales_projections/screenshot.gif differ diff --git a/demo/same-person-or-different/DESCRIPTION.md b/demo/same-person-or-different/DESCRIPTION.md new file mode 100644 index 0000000..f2821dc --- /dev/null +++ b/demo/same-person-or-different/DESCRIPTION.md @@ -0,0 +1 @@ +This demo identifies if two speakers are the same person using Gradio's Audio and HTML components. \ No newline at end of file diff --git a/demo/same-person-or-different/packages.txt b/demo/same-person-or-different/packages.txt new file mode 100644 index 0000000..a9f1eea --- /dev/null +++ b/demo/same-person-or-different/packages.txt @@ -0,0 +1 @@ +ffmpeg \ No newline at end of file diff --git a/demo/same-person-or-different/requirements.txt b/demo/same-person-or-different/requirements.txt new file mode 100644 index 0000000..baca898 --- /dev/null +++ b/demo/same-person-or-different/requirements.txt @@ -0,0 +1,2 @@ +git+https://github.com/huggingface/transformers +torchaudio diff --git a/demo/same-person-or-different/run.py b/demo/same-person-or-different/run.py new file mode 100644 index 0000000..296c538 --- /dev/null +++ b/demo/same-person-or-different/run.py @@ -0,0 +1,104 @@ +# type: ignore +import gradio as gr +import torch +from torchaudio.sox_effects import apply_effects_file # type: ignore +from transformers import AutoFeatureExtractor, AutoModelForAudioXVector +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +OUTPUT_OK = ( + """ +
+

The speakers are

+

{:.1f}%

+

similar

+

Welcome, human!

+
(You must get at least 85% to be considered the same person)
+
+""" +) +OUTPUT_FAIL = ( + """ +
+

The speakers are

+

{:.1f}%

+

similar

+

You shall not pass!

+
(You must get at least 85% to be considered the same person)
+
+""" +) + +EFFECTS = [ + ["remix", "-"], + ["channels", "1"], + ["rate", "16000"], + ["gain", "-1.0"], + ["silence", "1", "0.1", "0.1%", "-1", "0.1", "0.1%"], + ["trim", "0", "10"], +] + +THRESHOLD = 0.85 + +model_name = "microsoft/unispeech-sat-base-plus-sv" +feature_extractor = AutoFeatureExtractor.from_pretrained(model_name) +model = AutoModelForAudioXVector.from_pretrained(model_name).to(device) +cosine_sim = torch.nn.CosineSimilarity(dim=-1) + +def similarity_fn(path1, path2): + if not (path1 and path2): + return 'ERROR: Please record audio for *both* speakers!' + + wav1, _ = apply_effects_file(path1, EFFECTS) + wav2, _ = apply_effects_file(path2, EFFECTS) + print(wav1.shape, wav2.shape) + + input1 = feature_extractor(wav1.squeeze(0), return_tensors="pt", sampling_rate=16000).input_values.to(device) + input2 = feature_extractor(wav2.squeeze(0), return_tensors="pt", sampling_rate=16000).input_values.to(device) + + with torch.no_grad(): + emb1 = model(input1).embeddings + emb2 = model(input2).embeddings + emb1 = torch.nn.functional.normalize(emb1, dim=-1).cpu() + emb2 = torch.nn.functional.normalize(emb2, dim=-1).cpu() + similarity = cosine_sim(emb1, emb2).numpy()[0] + + if similarity >= THRESHOLD: + output = OUTPUT_OK.format(similarity * 100) + else: + output = OUTPUT_FAIL.format(similarity * 100) + + return output + +inputs = [ + gr.Audio(sources=["microphone"], type="filepath", label="Speaker #1"), + gr.Audio(sources=["microphone"], type="filepath", label="Speaker #2"), +] +output = gr.HTML(label="") + +description = ( + "This demo from Microsoft will compare two speech samples and determine if they are from the same speaker. " + "Try it with your own voice!" +) +article = ( + "

" + "🎙️ Learn more about UniSpeech-SAT | " + "📚 UniSpeech-SAT paper | " + "📚 X-Vector paper" + "

" +) +examples = [ + ["samples/cate_blanch.mp3", "samples/cate_blanch_2.mp3"], + ["samples/cate_blanch.mp3", "samples/heath_ledger.mp3"], +] + +interface = gr.Interface( + fn=similarity_fn, + inputs=inputs, + outputs=output, + layout="horizontal", + flagging_mode="never", + live=False, + examples=examples, + cache_examples=False +) +interface.launch() diff --git a/demo/save_file_no_output/run.py b/demo/save_file_no_output/run.py new file mode 100644 index 0000000..64b1ac1 --- /dev/null +++ b/demo/save_file_no_output/run.py @@ -0,0 +1,17 @@ +import random +import string +import gradio as gr + +def save_image_random_name(image): + random_string = ''.join(random.choices(string.ascii_letters, k=20)) + '.png' + image.save(random_string) + print(f"Saved image to {random_string}!") + +demo = gr.Interface( + fn=save_image_random_name, + inputs=gr.Image(type="pil"), + outputs=None, + api_name="predict", +) +if __name__ == "__main__": + demo.launch() diff --git a/demo/scatter_plot/requirements.txt b/demo/scatter_plot/requirements.txt new file mode 100644 index 0000000..7e0e60e --- /dev/null +++ b/demo/scatter_plot/requirements.txt @@ -0,0 +1,2 @@ +vega_datasets +pandas \ No newline at end of file diff --git a/demo/scatter_plot/run.py b/demo/scatter_plot/run.py new file mode 100644 index 0000000..1adb917 --- /dev/null +++ b/demo/scatter_plot/run.py @@ -0,0 +1,65 @@ +import gradio as gr +from vega_datasets import data + +cars = data.cars() +iris = data.iris() + +# # Or generate your own fake data + +# import pandas as pd +# import random + +# cars_data = { +# "Name": ["car name " + f" {int(i/10)}" for i in range(400)], +# "Miles_per_Gallon": [random.randint(10, 30) for _ in range(400)], +# "Origin": [random.choice(["USA", "Europe", "Japan"]) for _ in range(400)], +# "Horsepower": [random.randint(50, 250) for _ in range(400)], +# } + +# iris_data = { +# "petalWidth": [round(random.uniform(0, 2.5), 2) for _ in range(150)], +# "petalLength": [round(random.uniform(0, 7), 2) for _ in range(150)], +# "species": [ +# random.choice(["setosa", "versicolor", "virginica"]) for _ in range(150) +# ], +# } + +# cars = pd.DataFrame(cars_data) +# iris = pd.DataFrame(iris_data) + +def scatter_plot_fn(dataset): + if dataset == "iris": + return gr.ScatterPlot( + value=iris, + x="petalWidth", + y="petalLength", + color="species", + title="Iris Dataset", + x_title="Petal Width", + y_title="Petal Length", + tooltip=["petalWidth", "petalLength", "species"], + caption="", + ) + else: + return gr.ScatterPlot( + value=cars, + x="Horsepower", + y="Miles_per_Gallon", + color="Origin", + tooltip=["Name"], + title="Car Data", + y_title="Miles per Gallon", + caption="MPG vs Horsepower of various cars", + ) + +with gr.Blocks() as scatter_plot: + with gr.Row(): + with gr.Column(): + dataset = gr.Dropdown(choices=["cars", "iris"], value="cars") + with gr.Column(): + plot = gr.ScatterPlot() + dataset.change(scatter_plot_fn, inputs=dataset, outputs=plot) + scatter_plot.load(fn=scatter_plot_fn, inputs=dataset, outputs=plot) + +if __name__ == "__main__": + scatter_plot.launch() diff --git a/demo/scatter_plot_demo/requirements.txt b/demo/scatter_plot_demo/requirements.txt new file mode 100644 index 0000000..fb6c7ed --- /dev/null +++ b/demo/scatter_plot_demo/requirements.txt @@ -0,0 +1 @@ +pandas diff --git a/demo/scatter_plot_demo/run.py b/demo/scatter_plot_demo/run.py new file mode 100644 index 0000000..0274b08 --- /dev/null +++ b/demo/scatter_plot_demo/run.py @@ -0,0 +1,78 @@ +import pandas as pd +from random import randint, random +import gradio as gr + + +temp_sensor_data = pd.DataFrame( + { + "time": pd.date_range("2021-01-01", end="2021-01-05", periods=200), + "temperature": [randint(50 + 10 * (i % 2), 65 + 15 * (i % 2)) for i in range(200)], + "humidity": [randint(50 + 10 * (i % 2), 65 + 15 * (i % 2)) for i in range(200)], + "location": ["indoor", "outdoor"] * 100, + } +) + +food_rating_data = pd.DataFrame( + { + "cuisine": [["Italian", "Mexican", "Chinese"][i % 3] for i in range(100)], + "rating": [random() * 4 + 0.5 * (i % 3) for i in range(100)], + "price": [randint(10, 50) + 4 * (i % 3) for i in range(100)], + "wait": [random() for i in range(100)], + } +) + +with gr.Blocks() as scatter_plots: + with gr.Row(): + start = gr.DateTime("2021-01-01 00:00:00", label="Start") + end = gr.DateTime("2021-01-05 00:00:00", label="End") + apply_btn = gr.Button("Apply", scale=0) + with gr.Row(): + group_by = gr.Radio(["None", "30m", "1h", "4h", "1d"], value="None", label="Group by") + aggregate = gr.Radio(["sum", "mean", "median", "min", "max"], value="sum", label="Aggregation") + + temp_by_time = gr.ScatterPlot( + temp_sensor_data, + x="time", + y="temperature", + ) + temp_by_time_location = gr.ScatterPlot( + temp_sensor_data, + x="time", + y="temperature", + color="location", + ) + + time_graphs = [temp_by_time, temp_by_time_location] + group_by.change( + lambda group: [gr.ScatterPlot(x_bin=None if group == "None" else group)] * len(time_graphs), + group_by, + time_graphs + ) + aggregate.change( + lambda aggregate: [gr.ScatterPlot(y_aggregate=aggregate)] * len(time_graphs), + aggregate, + time_graphs + ) + + price_by_cuisine = gr.ScatterPlot( + food_rating_data, + x="cuisine", + y="price", + ) + with gr.Row(): + price_by_rating = gr.ScatterPlot( + food_rating_data, + x="rating", + y="price", + color="wait", + buttons=["actions"], # type: ignore + ) + price_by_rating_color = gr.ScatterPlot( + food_rating_data, + x="rating", + y="price", + color="cuisine", + ) + +if __name__ == "__main__": + scatter_plots.launch() diff --git a/demo/scatterplot_component/requirements.txt b/demo/scatterplot_component/requirements.txt new file mode 100644 index 0000000..d1c8a7a --- /dev/null +++ b/demo/scatterplot_component/requirements.txt @@ -0,0 +1 @@ +vega_datasets \ No newline at end of file diff --git a/demo/scatterplot_component/run.py b/demo/scatterplot_component/run.py new file mode 100644 index 0000000..8fcf715 --- /dev/null +++ b/demo/scatterplot_component/run.py @@ -0,0 +1,19 @@ +import gradio as gr +from vega_datasets import data + +cars = data.cars() + +with gr.Blocks() as demo: + gr.ScatterPlot( + value=cars, + x="Horsepower", + y="Miles_per_Gallon", + color="Origin", + tooltip=["Name"], + title="Car Data", + y_title="Miles per Gallon", + container=False, + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/score_tracker/run.py b/demo/score_tracker/run.py new file mode 100644 index 0000000..c5570b4 --- /dev/null +++ b/demo/score_tracker/run.py @@ -0,0 +1,17 @@ +import gradio as gr + +scores = [] + +def track_score(score): + scores.append(score) + top_scores = sorted(scores, reverse=True)[:3] + return top_scores + +demo = gr.Interface( + track_score, + gr.Number(label="Score"), + gr.JSON(label="Top Scores"), + api_name="predict" +) +if __name__ == "__main__": + demo.launch() diff --git a/demo/sentence_builder/run.py b/demo/sentence_builder/run.py new file mode 100644 index 0000000..dcb0f25 --- /dev/null +++ b/demo/sentence_builder/run.py @@ -0,0 +1,31 @@ +import gradio as gr + +def sentence_builder(quantity, animal, countries, place, activity_list, morning): + return f"""The {quantity} {animal}s from {" and ".join(countries)} went to the {place} where they {" and ".join(activity_list)} until the {"morning" if morning else "night"}""" + +demo = gr.Interface( + sentence_builder, + [ + gr.Slider(2, 20, value=4, label="Count", info="Choose between 2 and 20"), + gr.Dropdown( + ["cat", "dog", "bird"], label="Animal", info="Will add more animals later!" + ), + gr.CheckboxGroup(["USA", "Japan", "Pakistan"], label="Countries", info="Where are they from?"), + gr.Radio(["park", "zoo", "road"], label="Location", info="Where did they go?"), + gr.Dropdown( + ["ran", "swam", "ate", "slept"], value=["swam", "slept"], multiselect=True, label="Activity", info="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed auctor, nisl eget ultricies aliquam, nunc nisl aliquet nunc, eget aliquam nisl nunc vel nisl." + ), + gr.Checkbox(label="Morning", info="Did they do it in the morning?"), + ], + "text", + api_name="predict", + examples=[ + [2, "cat", ["Japan", "Pakistan"], "park", ["ate", "swam"], True], + [4, "dog", ["Japan"], "zoo", ["ate", "swam"], False], + [10, "bird", ["USA", "Pakistan"], "road", ["ran"], False], + [8, "cat", ["Pakistan"], "zoo", ["ate"], True], + ] +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/sentence_builder/screenshot.png b/demo/sentence_builder/screenshot.png new file mode 100644 index 0000000..8cf53e4 Binary files /dev/null and b/demo/sentence_builder/screenshot.png differ diff --git a/demo/sentiment_analysis/DESCRIPTION.md b/demo/sentiment_analysis/DESCRIPTION.md new file mode 100644 index 0000000..affa1e8 --- /dev/null +++ b/demo/sentiment_analysis/DESCRIPTION.md @@ -0,0 +1 @@ +This sentiment analaysis demo takes in input text and returns its classification for either positive, negative or neutral using Gradio's Label output. \ No newline at end of file diff --git a/demo/sentiment_analysis/requirements.txt b/demo/sentiment_analysis/requirements.txt new file mode 100644 index 0000000..6fa2de4 --- /dev/null +++ b/demo/sentiment_analysis/requirements.txt @@ -0,0 +1 @@ +nltk \ No newline at end of file diff --git a/demo/sentiment_analysis/run.py b/demo/sentiment_analysis/run.py new file mode 100644 index 0000000..92e1693 --- /dev/null +++ b/demo/sentiment_analysis/run.py @@ -0,0 +1,20 @@ +import gradio as gr +import nltk # type: ignore +from nltk.sentiment.vader import SentimentIntensityAnalyzer # type: ignore + +nltk.download("vader_lexicon") +sid = SentimentIntensityAnalyzer() + +def sentiment_analysis(text): + scores = sid.polarity_scores(text) + del scores["compound"] + return scores + +demo = gr.Interface( + fn=sentiment_analysis, + inputs=gr.Textbox(placeholder="Enter a positive or negative sentence here..."), + outputs="label", + examples=[["This is wonderful!"]], + api_name="predict") + +demo.launch() diff --git a/demo/sepia_filter/requirements.txt b/demo/sepia_filter/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/sepia_filter/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/sepia_filter/run.py b/demo/sepia_filter/run.py new file mode 100644 index 0000000..da90875 --- /dev/null +++ b/demo/sepia_filter/run.py @@ -0,0 +1,16 @@ +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") +if __name__ == "__main__": + demo.launch() diff --git a/demo/sepia_filter/screenshot.gif b/demo/sepia_filter/screenshot.gif new file mode 100644 index 0000000..076bdc2 Binary files /dev/null and b/demo/sepia_filter/screenshot.gif differ diff --git a/demo/server_app/run.py b/demo/server_app/run.py new file mode 100644 index 0000000..6a9cf21 --- /dev/null +++ b/demo/server_app/run.py @@ -0,0 +1,55 @@ +from gradio import Server +from fastapi.responses import HTMLResponse + +app = Server() + +@app.mcp.tool(name="add") +@app.api(name="add") +def add(a: int, b: int) -> int: + """Add two numbers together.""" + return a + b + +@app.mcp.tool(name="multiply") +@app.api(name="multiply") +def multiply(a: int, b: int) -> int: + """Multiply two numbers together.""" + return a * b + +@app.get("/", response_class=HTMLResponse) +async def homepage(): + return """ + + +Calculator + + +
+
0
+ Operands +
+ Operation +
+
+ + +""" + +if __name__ == "__main__": + app.launch(mcp_server=True) diff --git a/demo/show_progress_on/run.py b/demo/show_progress_on/run.py new file mode 100644 index 0000000..846e8a1 --- /dev/null +++ b/demo/show_progress_on/run.py @@ -0,0 +1,25 @@ +# This demo needs to be run from the repo folder. +# python demo/fake_gan/run.py +import time +import gradio as gr +from gradio.media import get_image + +def fake_gan(): + time.sleep(5) + images = [ + (get_image("cheetah.jpg"), f"label {i}") + for i in range(3) + ] + return images, "Done" + +with gr.Blocks() as demo: + gallery = gr.Gallery( + label="Generated images", show_label=False, elem_id="gallery" + , columns=1, object_fit="contain", height="auto") + t = gr.Textbox(label="Progress", elem_id="progress_textbox") + btn = gr.Button("Generate images", scale=0) + + btn.click(fake_gan, None, [gallery, t], show_progress="minimal", show_progress_on=t) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/simple_state/run.py b/demo/simple_state/run.py new file mode 100644 index 0000000..40836a8 --- /dev/null +++ b/demo/simple_state/run.py @@ -0,0 +1,17 @@ +import gradio as gr + +with gr.Blocks() as demo: + cart = gr.State([]) + items_to_add = gr.CheckboxGroup(["Cereal", "Milk", "Orange Juice", "Water"]) + + def add_items(new_items, previous_cart): + cart = previous_cart + new_items + return cart + + gr.Button("Add Items").click(add_items, [items_to_add, cart], cart) + + cart_size = gr.Number(label="Cart Size") + cart.change(lambda cart: len(cart), cart, cart_size) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/sine_curve/requirements.txt b/demo/sine_curve/requirements.txt new file mode 100644 index 0000000..5c84b3c --- /dev/null +++ b/demo/sine_curve/requirements.txt @@ -0,0 +1,2 @@ +numpy +plotly diff --git a/demo/sine_curve/run.py b/demo/sine_curve/run.py new file mode 100644 index 0000000..4ffeb6e --- /dev/null +++ b/demo/sine_curve/run.py @@ -0,0 +1,29 @@ +import math +import gradio as gr +import plotly.express as px # type: ignore +import numpy as np + +plot_end = 2 * math.pi + +def get_plot(period=1): + global plot_end + x = np.arange(plot_end - 2 * math.pi, plot_end, 0.02) + y = np.sin(2*math.pi*period * x) + fig = px.line(x=x, y=y) + plot_end += 2 * math.pi + if plot_end > 1000: + plot_end = 2 * math.pi + return fig + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + gr.Markdown("Change the value of the slider to automatically update the plot") + period = gr.Slider(label="Period of plot", value=1, minimum=0, maximum=10, step=1) + plot = gr.Plot(label="Plot (updates every half second)") + + dep = demo.load(get_plot, None, plot, every=1) + period.change(get_plot, period, plot, every=1, cancels=[dep]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/skip/run.py b/demo/skip/run.py new file mode 100644 index 0000000..b43974b --- /dev/null +++ b/demo/skip/run.py @@ -0,0 +1,16 @@ +import random +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + clear_button = gr.Button("Clear") + skip_button = gr.Button("Skip") + random_button = gr.Button("Random") + numbers = [gr.Number(), gr.Number()] + + clear_button.click(lambda : (None, None), outputs=numbers) + skip_button.click(lambda : [gr.skip(), gr.skip()], outputs=numbers) + random_button.click(lambda : (random.randint(0, 100), random.randint(0, 100)), outputs=numbers) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/slider_component/run.py b/demo/slider_component/run.py new file mode 100644 index 0000000..9a14f44 --- /dev/null +++ b/demo/slider_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Slider() + +demo.launch() diff --git a/demo/slider_release/run.py b/demo/slider_release/run.py new file mode 100644 index 0000000..5564c61 --- /dev/null +++ b/demo/slider_release/run.py @@ -0,0 +1,18 @@ +import gradio as gr + +def identity(x, state): + state += 1 + return x, state, state + +with gr.Blocks() as demo: + slider = gr.Slider(0, 100, step=0.1) + state = gr.State(value=0) + with gr.Row(): + number = gr.Number(label="On release") + number2 = gr.Number(label="Number of events fired") + slider.release(identity, inputs=[slider, state], outputs=[number, state, number2], api_name="predict") + +if __name__ == "__main__": + print("here") + demo.launch() + print(demo.server_port) diff --git a/demo/sort_records/polars_sort.csv b/demo/sort_records/polars_sort.csv new file mode 100644 index 0000000..3620f4c --- /dev/null +++ b/demo/sort_records/polars_sort.csv @@ -0,0 +1,4 @@ +Item,Quantity +apple,56 +banana,12 +orange,30 diff --git a/demo/sort_records/run.py b/demo/sort_records/run.py new file mode 100644 index 0000000..450e974 --- /dev/null +++ b/demo/sort_records/run.py @@ -0,0 +1,21 @@ +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"], # type: ignore + row_count=3, + column_count=2, + column_limits=(2, 2), + type="polars" + ), + "dataframe", + description="Sort by Quantity" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/sound_alert/beep.mp3 b/demo/sound_alert/beep.mp3 new file mode 100644 index 0000000..cd3a3ec Binary files /dev/null and b/demo/sound_alert/beep.mp3 differ diff --git a/demo/sound_alert/run.py b/demo/sound_alert/run.py new file mode 100644 index 0000000..b99ddd0 --- /dev/null +++ b/demo/sound_alert/run.py @@ -0,0 +1,16 @@ +import time +import gradio as gr + +js_function = "() => {new Audio('file=beep.mp3').play();}" + +def task(x): + time.sleep(2) + return "Hello, " + x + +with gr.Blocks() as demo: + name = gr.Textbox(label="name") + greeting = gr.Textbox(label="greeting") + name.blur(task, name, greeting) + greeting.change(None, [], [], js=js_function) + +demo.launch() diff --git a/demo/spectogram/requirements.txt b/demo/spectogram/requirements.txt new file mode 100644 index 0000000..ca60345 --- /dev/null +++ b/demo/spectogram/requirements.txt @@ -0,0 +1,3 @@ +scipy +numpy +matplotlib \ No newline at end of file diff --git a/demo/spectogram/run.py b/demo/spectogram/run.py new file mode 100644 index 0000000..78942bc --- /dev/null +++ b/demo/spectogram/run.py @@ -0,0 +1,20 @@ +import matplotlib.pyplot as plt +import numpy as np +from scipy import signal # ty: ignore[unresolved-import] + +import gradio as gr + +def spectrogram(audio): + sr, data = audio + if len(data.shape) == 2: + data = np.mean(data, axis=0) + frequencies, times, spectrogram_data = signal.spectrogram( + data, sr, window="hamming" + ) + plt.pcolormesh(times, frequencies, np.log10(spectrogram_data)) + return plt + +demo = gr.Interface(spectrogram, "audio", "plot", api_name="predict") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/spectogram/screenshot.png b/demo/spectogram/screenshot.png new file mode 100644 index 0000000..3f8d9d1 Binary files /dev/null and b/demo/spectogram/screenshot.png differ diff --git a/demo/stable-diffusion/DESCRIPTION.md b/demo/stable-diffusion/DESCRIPTION.md new file mode 100644 index 0000000..0a59ff6 --- /dev/null +++ b/demo/stable-diffusion/DESCRIPTION.md @@ -0,0 +1 @@ +Note: This is a simplified version of the code needed to create the Stable Diffusion demo. See full code here: https://hf.co/spaces/stabilityai/stable-diffusion/tree/main \ No newline at end of file diff --git a/demo/stable-diffusion/requirements.txt b/demo/stable-diffusion/requirements.txt new file mode 100644 index 0000000..7ae07ca --- /dev/null +++ b/demo/stable-diffusion/requirements.txt @@ -0,0 +1,3 @@ +diffusers==0.32.2 +torch==2.5.1 + diff --git a/demo/stable-diffusion/run.py b/demo/stable-diffusion/run.py new file mode 100644 index 0000000..cde6de3 --- /dev/null +++ b/demo/stable-diffusion/run.py @@ -0,0 +1,88 @@ +import gradio as gr +import torch +from diffusers import StableDiffusionPipeline # type: ignore +from PIL import Image +import os + +auth_token = os.getenv("HF_TOKEN") +if not auth_token: + print( + "ERROR: No Hugging Face access token found.\n" + "Please define an environment variable 'auth_token' before running.\n" + "Example:\n" + " export HF_TOKEN=XXXXXXXX\n" + ) + +model_id = "CompVis/stable-diffusion-v1-4" +device = "cpu" +pipe = StableDiffusionPipeline.from_pretrained( + model_id, token=auth_token, variant="fp16", torch_dtype=torch.float16, +) +pipe = pipe.to(device) + + +def infer(prompt, samples, steps, scale, seed): + generator = torch.Generator(device=device).manual_seed(seed) + images_list = pipe( # type: ignore + [prompt] * samples, + num_inference_steps=steps, + guidance_scale=scale, + generator=generator, + ) + images = [] + safe_image = Image.open(r"unsafe.png") + for i, image in enumerate(images_list["sample"]): # type: ignore + if images_list["nsfw_content_detected"][i]: # type: ignore + images.append(safe_image) + else: + images.append(image) + return images + + +block = gr.Blocks() + +with block: + with gr.Group(): + with gr.Row(): + text = gr.Textbox( + label="Enter your prompt", + max_lines=1, + placeholder="Enter your prompt", + container=False, + ) + btn = gr.Button("Generate image") + gallery = gr.Gallery( + label="Generated images", + show_label=False, + elem_id="gallery", + columns=2, + ) + + advanced_button = gr.Button("Advanced options", elem_id="advanced-btn") + + with gr.Row(elem_id="advanced-options"): + samples = gr.Slider(label="Images", minimum=1, maximum=4, value=4, step=1) + steps = gr.Slider(label="Steps", minimum=1, maximum=50, value=45, step=1) + scale = gr.Slider( + label="Guidance Scale", minimum=0, maximum=50, value=7.5, step=0.1 + ) + seed = gr.Slider( + label="Seed", + minimum=0, + maximum=2147483647, + step=1, + randomize=True, + ) + gr.on( + [text.submit, btn.click], + infer, + inputs=[text, samples, steps, scale, seed], + outputs=gallery, + ) + advanced_button.click( + None, + [], + text, + ) + +block.launch() diff --git a/demo/star_rating_component/run.py b/demo/star_rating_component/run.py new file mode 100644 index 0000000..2fd4f56 --- /dev/null +++ b/demo/star_rating_component/run.py @@ -0,0 +1,45 @@ +import gradio as gr + +class StarRating(gr.HTML): + def __init__(self, label, value=0, **kwargs): + html_template = """ +

${label} rating:

+ ${Array.from({length: 5}, (_, i) => ``).join('')} + """ + css_template = """ + img { height: 50px; display: inline-block; cursor: pointer; } + .faded { filter: grayscale(100%); opacity: 0.3; } + """ + js_on_load = """ + const imgs = element.querySelectorAll('img'); + imgs.forEach((img, index) => { + img.addEventListener('click', () => { + props.value = index + 1; + }); + }); + """ + super().__init__(value=value, label=label, html_template=html_template, css_template=css_template, js_on_load=js_on_load, **kwargs) + + def api_info(self): + return {"type": "integer", "minimum": 0, "maximum": 5} + + +with gr.Blocks() as demo: + gr.Markdown("# Restaurant Review") + food_rating = StarRating(label="Food", value=3) + service_rating = StarRating(label="Service", value=3) + ambience_rating = StarRating(label="Ambience", value=3) + + average_btn = gr.Button("Calculate Average Rating") + + rating_output = StarRating(label="Average", value=3) + def calculate_average(food, service, ambience): + return round((food + service + ambience) / 3) + average_btn.click( + fn=calculate_average, + inputs=[food_rating, service_rating, ambience_rating], + outputs=rating_output + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/star_rating_events/run.py b/demo/star_rating_events/run.py new file mode 100644 index 0000000..f673723 --- /dev/null +++ b/demo/star_rating_events/run.py @@ -0,0 +1,31 @@ +import gradio as gr + +with gr.Blocks() as demo: + star_rating = gr.HTML( + value=3, + html_template=""" +

Star Rating:

+ ${Array.from({length: 5}, (_, i) => ``).join('')} + + """, + css_template=""" + img { height: 50px; display: inline-block; cursor: pointer; } + .faded { filter: grayscale(100%); opacity: 0.3; } + """, + js_on_load=""" + const imgs = element.querySelectorAll('img'); + imgs.forEach((img, index) => { + img.addEventListener('click', () => { + props.value = index + 1; + }); + }); + const submitBtn = element.querySelector('#submit-btn'); + submitBtn.addEventListener('click', () => { + trigger('submit'); + }); + """) + rating_output = gr.Textbox(label="Submitted Rating") + star_rating.submit(lambda x: x, inputs=star_rating, outputs=rating_output) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/star_rating_props/run.py b/demo/star_rating_props/run.py new file mode 100644 index 0000000..19e33ea --- /dev/null +++ b/demo/star_rating_props/run.py @@ -0,0 +1,23 @@ +import gradio as gr + +with gr.Blocks() as demo: + star_rating = gr.HTML( + 7, + size=40, + max_stars=10, + html_template=""" +

Star Rating:

+ ${Array.from({length: max_stars}, (_, i) => ``).join('')}""", + css_template=""" + img { height: ${size}px; display: inline-block; } + .faded { filter: grayscale(100%); opacity: 0.3; } + """ + ) + rating_slider = gr.Slider(0, 10, step=1, label="Select Rating") + rating_slider.change(fn=lambda x: x, inputs=rating_slider, outputs=star_rating) + + size_slider = gr.Slider(20, 100, 40, step=1, label="Select Size") + size_slider.change(fn=lambda x: gr.HTML(size=x), inputs=size_slider, outputs=star_rating) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/star_rating_simple/run.py b/demo/star_rating_simple/run.py new file mode 100644 index 0000000..cce160e --- /dev/null +++ b/demo/star_rating_simple/run.py @@ -0,0 +1,17 @@ +import gradio as gr + +with gr.Blocks() as demo: + three_star_rating = gr.HTML(""" +

Star Rating:

+ + + + + + """, css_template=""" + img { height: 50px; display: inline-block; } + .faded { filter: grayscale(100%); opacity: 0.3; } + """) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/star_rating_templates/run.py b/demo/star_rating_templates/run.py new file mode 100644 index 0000000..a3d36fd --- /dev/null +++ b/demo/star_rating_templates/run.py @@ -0,0 +1,17 @@ +import gradio as gr + +with gr.Blocks() as demo: + star_rating = gr.HTML( + value=3, + html_template=""" +

Star Rating:

+ ${Array.from({length: 5}, (_, i) => ``).join('')}""", + css_template=""" + img { height: 50px; display: inline-block; } + .faded { filter: grayscale(100%); opacity: 0.3; } + """) + rating_slider = gr.Slider(0, 5, 3, step=1, label="Select Rating") + rating_slider.change(fn=lambda x: x, inputs=rating_slider, outputs=star_rating) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/state_change/run.py b/demo/state_change/run.py new file mode 100644 index 0000000..6e935d8 --- /dev/null +++ b/demo/state_change/run.py @@ -0,0 +1,114 @@ +import gradio as gr + +with gr.Blocks() as demo: + + with gr.Row(): + state_a = gr.State(0) + btn_a = gr.Button("Increment A") + value_a = gr.Number(label="Number A") + btn_a.click(lambda x: x + 1, state_a, state_a) + state_a.change(lambda x: x, state_a, value_a) + with gr.Row(): + state_b = gr.State(0) + btn_b = gr.Button("Increment B") + value_b = gr.Number(label="Number B") + btn_b.click(lambda x: x + 1, state_b, state_b) + + @gr.on(inputs=state_b, outputs=value_b) + def identity(x): + return x + + @gr.render(inputs=[state_a, state_b]) + def render(a, b): + for x in range(a): + with gr.Row(): + for y in range(b): + gr.Button(f"Button {x}, {y}") + + list_state = gr.State([]) + dict_state = gr.State(dict()) + nested_list_state = gr.State([]) + set_state = gr.State(set()) + + def transform_list(x): + return {n: n for n in x}, [x[:] for _ in range(len(x))], set(x) + + list_state.change( + transform_list, + inputs=list_state, + outputs=[dict_state, nested_list_state, set_state], + ) + + all_textbox = gr.Textbox(label="Output") + click_count = gr.Number(label="Clicks") + change_count = gr.Number(label="Changes") + gr.on( + inputs=[change_count, dict_state, nested_list_state, set_state], + triggers=[dict_state.change, nested_list_state.change, set_state.change], + fn=lambda x, *args: (x + 1, "\n".join(str(arg) for arg in args)), + outputs=[change_count, all_textbox], + ) + + count_to_3_btn = gr.Button("Count to 3") + count_to_3_btn.click(lambda: [1, 2, 3], outputs=list_state) + zero_all_btn = gr.Button("Zero All") + zero_all_btn.click(lambda x: [0] * len(x), inputs=list_state, outputs=list_state) + + gr.on( + [count_to_3_btn.click, zero_all_btn.click], + lambda x: x + 1, + click_count, + click_count, + ) + + async def increment(x): + yield x + 1 + + n_text = gr.State(0) + add_btn = gr.Button("Iterator State Change") + add_btn.click(increment, n_text, n_text) + + @gr.render(inputs=n_text) + def render_count(count): + for i in range(int(count)): + gr.Markdown(value = f"Success Box {i} added", key=i) + + class CustomState(): + def __init__(self, val): + self.val = val + + def __hash__(self) -> int: + return self.val + + custom_state = gr.State(CustomState(5)) + with gr.Row(): + btn_10 = gr.Button("Set State to 10") + custom_changes = gr.Number(0, label="Custom State Changes") + custom_clicks = gr.Number(0, label="Custom State Clicks") + + custom_state.change(increment, custom_changes, custom_changes) + def set_to_10(cs: CustomState): + cs.val = 10 + return cs + + btn_10.click(set_to_10, custom_state, custom_state).then( + increment, custom_clicks, custom_clicks + ) + + @gr.render() + def render_state_changes(): + with gr.Row(): + box1 = gr.Textbox(label="Start State") + state1 = gr.State() + box2 = gr.Textbox() + state2 = gr.State() + box3 = gr.Textbox(label="End State") + + iden = lambda x: x + box1.change(iden, box1, state1) + state1.change(iden, state1, box2) + box2.change(iden, box2, state2) + state2.change(iden, state2, box3) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/state_cleanup/requirements.txt b/demo/state_cleanup/requirements.txt new file mode 100644 index 0000000..0d59398 --- /dev/null +++ b/demo/state_cleanup/requirements.txt @@ -0,0 +1,2 @@ +numpy +Pillow diff --git a/demo/state_cleanup/run.py b/demo/state_cleanup/run.py new file mode 100644 index 0000000..fd52155 --- /dev/null +++ b/demo/state_cleanup/run.py @@ -0,0 +1,50 @@ +from __future__ import annotations +import gradio as gr +import numpy as np +from PIL import Image +from pathlib import Path +import secrets +import shutil + +current_dir = Path(__file__).parent + +def generate_random_img(history: list[Image.Image], request: gr.Request): + """Generate a random red, green, blue, orange, yellor or purple image.""" + colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 165, 0), (255, 255, 0), (128, 0, 128)] + color = colors[np.random.randint(0, len(colors))] + img = Image.new('RGB', (100, 100), color) + + user_dir: Path = current_dir / str(request.session_hash) + user_dir.mkdir(exist_ok=True) + path = user_dir / f"{secrets.token_urlsafe(8)}.webp" + + img.save(path) + history.append(img) + + return img, history, history + +def delete_directory(req: gr.Request): + if not req.username: + return + user_dir: Path = current_dir / req.username + shutil.rmtree(str(user_dir)) + +with gr.Blocks(delete_cache=(60, 3600)) as demo: + gr.Markdown("""# State Cleanup Demo + 🖼️ Images are saved in a user-specific directory and deleted when the users closes the page via demo.unload. + """) + with gr.Row(): + with gr.Column(scale=1): + with gr.Row(): + img = gr.Image(label="Generated Image", height=300, width=300) + with gr.Row(): + gen = gr.Button(value="Generate") + with gr.Row(): + history = gr.Gallery(label="Previous Generations", height=500, columns=10) + state = gr.State(value=[], delete_callback=lambda v: print("STATE DELETED")) + + demo.load(generate_random_img, [state], [img, state, history]) + gen.click(generate_random_img, [state], [img, state, history]) + demo.unload(delete_directory) + +demo.launch() diff --git a/demo/state_component/run.py b/demo/state_component/run.py new file mode 100644 index 0000000..1daafc0 --- /dev/null +++ b/demo/state_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.State() + +demo.launch() diff --git a/demo/stock_forecast/requirements.txt b/demo/stock_forecast/requirements.txt new file mode 100644 index 0000000..806f221 --- /dev/null +++ b/demo/stock_forecast/requirements.txt @@ -0,0 +1,2 @@ +numpy +matplotlib \ No newline at end of file diff --git a/demo/stock_forecast/run.py b/demo/stock_forecast/run.py new file mode 100644 index 0000000..e7fea9e --- /dev/null +++ b/demo/stock_forecast/run.py @@ -0,0 +1,36 @@ +import matplotlib.pyplot as plt +import numpy as np + +import gradio as gr + +def plot_forecast(final_year, companies, noise, show_legend, point_style): + start_year = 2020 + x = np.arange(start_year, final_year + 1) + year_count = x.shape[0] + plt_format = ({"cross": "X", "line": "-", "circle": "o--"})[point_style] + fig = plt.figure() + ax = fig.add_subplot(111) + for i, company in enumerate(companies): + series = np.arange(0, year_count, dtype=float) + series = series**2 * (i + 1) + series += np.random.rand(year_count) * noise + ax.plot(x, series, plt_format) + if show_legend: + plt.legend(companies) + return fig + +demo = gr.Interface( + plot_forecast, + [ + gr.Radio([2025, 2030, 2035, 2040], label="Project to:"), + gr.CheckboxGroup(["Google", "Microsoft", "Gradio"], label="Company Selection"), + gr.Slider(1, 100, label="Noise Level"), + gr.Checkbox(label="Show Legend"), + gr.Dropdown(["cross", "line", "circle"], label="Style"), + ], + gr.Plot(label="forecast", format="png"), + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/stock_forecast/screenshot.png b/demo/stock_forecast/screenshot.png new file mode 100644 index 0000000..7dbecd2 Binary files /dev/null and b/demo/stock_forecast/screenshot.png differ diff --git a/demo/stream_asr/requirements.txt b/demo/stream_asr/requirements.txt new file mode 100644 index 0000000..0175df5 --- /dev/null +++ b/demo/stream_asr/requirements.txt @@ -0,0 +1,3 @@ +torch +torchaudio +transformers \ No newline at end of file diff --git a/demo/stream_asr/run.py b/demo/stream_asr/run.py new file mode 100644 index 0000000..6ca65be --- /dev/null +++ b/demo/stream_asr/run.py @@ -0,0 +1,32 @@ +import gradio as gr +from transformers import pipeline +import numpy as np + +transcriber = pipeline("automatic-speech-recognition", model="openai/whisper-base.en") + +def transcribe(stream, new_chunk): + sr, y = new_chunk + + # Convert to mono if stereo + if y.ndim > 1: + y = y.mean(axis=1) + + y = y.astype(np.float32) + y /= np.max(np.abs(y)) + + if stream is not None: + stream = np.concatenate([stream, y]) + else: + stream = y + return stream, transcriber({"sampling_rate": sr, "raw": stream})["text"] # type: ignore + +demo = gr.Interface( + transcribe, + ["state", gr.Audio(sources=["microphone"], streaming=True)], + ["state", "text"], + live=True, + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/stream_audio/requirements.txt b/demo/stream_audio/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/stream_audio/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/stream_audio/run.py b/demo/stream_audio/run.py new file mode 100644 index 0000000..edb974f --- /dev/null +++ b/demo/stream_audio/run.py @@ -0,0 +1,23 @@ +import gradio as gr +import numpy as np + +def add_to_stream(audio, instream): + if audio is None: + return gr.Audio(), instream + if instream is None: + ret = audio + else: + ret = (audio[0], np.concatenate((instream[1], audio[1]))) + return ret, ret + +with gr.Blocks() as demo: + inp = gr.Audio(sources=["microphone"]) + out = gr.Audio() + stream = gr.State() + clear = gr.Button("Clear") + + inp.stream(add_to_stream, [inp, stream], [out, stream]) + clear.click(lambda: [None, None, None], None, [inp, out, stream]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/stream_audio_out/run.py b/demo/stream_audio_out/run.py new file mode 100644 index 0000000..14f7d47 --- /dev/null +++ b/demo/stream_audio_out/run.py @@ -0,0 +1,60 @@ +import gradio as gr +from pydub import AudioSegment +from time import sleep +import os +import tempfile +from pathlib import Path + +with gr.Blocks() as demo: + input_audio = gr.Audio(label="Input Audio", type="filepath", format="mp3") + with gr.Row(): + with gr.Column(): + stream_as_file_btn = gr.Button("Stream as File") + format = gr.Radio(["wav", "mp3"], value="wav", label="Format") + stream_as_file_output = gr.Audio(streaming=True, elem_id="stream_as_file_output", autoplay=True, visible=False) + + def stream_file(audio_file, format): + audio = AudioSegment.from_file(audio_file) + i = 0 + chunk_size = 1000 + while chunk_size * i < len(audio): + chunk = audio[chunk_size * i : chunk_size * (i + 1)] + i += 1 + if chunk: + file = Path(tempfile.gettempdir()) / "stream_audio_demo" / f"{i}.{format}" + file.parent.mkdir(parents=True, exist_ok=True) + chunk.export(str(file), format=format) + yield file + sleep(0.5) + + stream_as_file_btn.click( + stream_file, [input_audio, format], stream_as_file_output + ) + + gr.Examples( + [[gr.get_audio("cantina.wav"), "wav"], + [gr.get_audio("cantina.wav"), "mp3"]], + [input_audio, format], + fn=stream_file, + outputs=stream_as_file_output, + cache_examples=False, + ) + + with gr.Column(): + stream_as_bytes_btn = gr.Button("Stream as Bytes") + stream_as_bytes_output = gr.Audio(streaming=True, elem_id="stream_as_bytes_output", autoplay=True) + + def stream_bytes(audio_file): + chunk_size = 20_000 + with open(audio_file, "rb") as f: + while True: + chunk = f.read(chunk_size) + if chunk: + yield chunk + sleep(1) + else: + break + stream_as_bytes_btn.click(stream_bytes, input_audio, stream_as_bytes_output) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/stream_frames/requirements.txt b/demo/stream_frames/requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/demo/stream_frames/requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/demo/stream_frames/run.py b/demo/stream_frames/run.py new file mode 100644 index 0000000..bdbef2e --- /dev/null +++ b/demo/stream_frames/run.py @@ -0,0 +1,15 @@ +import gradio as gr +import numpy as np + +def flip(im): + return np.flipud(im) + +demo = gr.Interface( + flip, + gr.Image(sources=["webcam"], streaming=True), + "image", + live=True, + api_name="predict", +) +if __name__ == "__main__": + demo.launch() diff --git a/demo/stream_video_out/requirements.txt b/demo/stream_video_out/requirements.txt new file mode 100644 index 0000000..1db7aea --- /dev/null +++ b/demo/stream_video_out/requirements.txt @@ -0,0 +1 @@ +opencv-python \ No newline at end of file diff --git a/demo/stream_video_out/run.py b/demo/stream_video_out/run.py new file mode 100644 index 0000000..bf27c4e --- /dev/null +++ b/demo/stream_video_out/run.py @@ -0,0 +1,101 @@ +import gradio as gr +import cv2 # type: ignore +import os +from pathlib import Path +import atexit + +current_dir = Path(__file__).resolve().parent + + +def delete_files(): + for p in Path(current_dir).glob("*.ts"): + p.unlink() + for p in Path(current_dir).glob("*.mp4"): + p.unlink() + + +atexit.register(delete_files) + + +def process_video(input_video, stream_as_mp4): + cap = cv2.VideoCapture(input_video) + + video_codec = cv2.VideoWriter_fourcc(*"mp4v") if stream_as_mp4 else cv2.VideoWriter_fourcc(*"x264") # type: ignore + fps = int(cap.get(cv2.CAP_PROP_FPS)) + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + iterating, frame = cap.read() + + n_frames = 0 + n_chunks = 0 + name = str(current_dir / f"output_{n_chunks}{'.mp4' if stream_as_mp4 else '.ts'}") + segment_file = cv2.VideoWriter(name, video_codec, fps, (width, height)) # type: ignore + + while iterating: + + # flip frame vertically + frame = cv2.flip(frame, 0) + display_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + segment_file.write(display_frame) + n_frames += 1 + if n_frames == 3 * fps: + n_chunks += 1 + segment_file.release() + n_frames = 0 + yield name + name = str( + current_dir / f"output_{n_chunks}{'.mp4' if stream_as_mp4 else '.ts'}" + ) + segment_file = cv2.VideoWriter(name, video_codec, fps, (width, height)) # type: ignore + + iterating, frame = cap.read() + + segment_file.release() + yield name + + +with gr.Blocks() as demo: + gr.Markdown("# Video Streaming Out 📹") + with gr.Row(): + with gr.Column(): + input_video = gr.Video(label="input") + checkbox = gr.Checkbox(label="Stream as MP4 file?", value=False) + with gr.Column(): + processed_frames = gr.Video( + label="stream", + streaming=True, + autoplay=True, + elem_id="stream_video_output", + ) + with gr.Row(): + process_video_btn = gr.Button("process video") + + process_video_btn.click(process_video, [input_video, checkbox], [processed_frames]) + + gr.Examples( + [ + [ + os.path.join( + os.path.dirname(__file__), + "video/compliment_bot_screen_recording_3x.mp4", + ), + False, + ], + [ + os.path.join( + os.path.dirname(__file__), + "video/compliment_bot_screen_recording_3x.mp4", + ), + True, + ], + ], + [input_video, checkbox], + fn=process_video, + outputs=processed_frames, + cache_examples=False, + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/stream_video_out/video/compliment_bot_screen_recording_3x.mp4 b/demo/stream_video_out/video/compliment_bot_screen_recording_3x.mp4 new file mode 100644 index 0000000..7a7395b Binary files /dev/null and b/demo/stream_video_out/video/compliment_bot_screen_recording_3x.mp4 differ diff --git a/demo/streaming_filter/requirements.txt b/demo/streaming_filter/requirements.txt new file mode 100644 index 0000000..b96544b --- /dev/null +++ b/demo/streaming_filter/requirements.txt @@ -0,0 +1,2 @@ +opencv-python +numpy diff --git a/demo/streaming_filter/run.py b/demo/streaming_filter/run.py new file mode 100644 index 0000000..b5754ff --- /dev/null +++ b/demo/streaming_filter/run.py @@ -0,0 +1,57 @@ +import gradio as gr +import numpy as np +import cv2 # type: ignore + + +def transform_cv2(frame, transform): + if transform == "cartoon": + # prepare color + img_color = cv2.pyrDown(cv2.pyrDown(frame)) + for _ in range(6): + img_color = cv2.bilateralFilter(img_color, 9, 9, 7) + img_color = cv2.pyrUp(cv2.pyrUp(img_color)) + + # prepare edges + img_edges = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) + img_edges = cv2.adaptiveThreshold( + cv2.medianBlur(img_edges, 7), + 255, + cv2.ADAPTIVE_THRESH_MEAN_C, + cv2.THRESH_BINARY, + 9, + 2, + ) + img_edges = cv2.cvtColor(img_edges, cv2.COLOR_GRAY2RGB) + # combine color and edges + img = cv2.bitwise_and(img_color, img_edges) + return img + elif transform == "edges": + # perform edge detection + img = cv2.cvtColor(cv2.Canny(frame, 100, 200), cv2.COLOR_GRAY2BGR) + return img + else: + return np.flipud(frame) + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + transform = gr.Dropdown( + choices=["cartoon", "edges", "flip"], + value="flip", + label="Transformation", + ) + input_img = gr.Image(sources=["webcam"], type="numpy") + with gr.Column(): + output_img = gr.Image(streaming=True) + dep = input_img.stream( + transform_cv2, + [input_img, transform], + [output_img], + time_limit=30, + stream_every=0.1, + concurrency_limit=30, + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/streaming_filter_unified/requirements.txt b/demo/streaming_filter_unified/requirements.txt new file mode 100644 index 0000000..b96544b --- /dev/null +++ b/demo/streaming_filter_unified/requirements.txt @@ -0,0 +1,2 @@ +opencv-python +numpy diff --git a/demo/streaming_filter_unified/run.py b/demo/streaming_filter_unified/run.py new file mode 100644 index 0000000..98cef32 --- /dev/null +++ b/demo/streaming_filter_unified/run.py @@ -0,0 +1,48 @@ +import gradio as gr +import numpy as np +import cv2 # type: ignore + +def transform_cv2(frame, transform): + if transform == "cartoon": + # prepare color + img_color = cv2.pyrDown(cv2.pyrDown(frame)) + for _ in range(6): + img_color = cv2.bilateralFilter(img_color, 9, 9, 7) + img_color = cv2.pyrUp(cv2.pyrUp(img_color)) + + # prepare edges + img_edges = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) + img_edges = cv2.adaptiveThreshold( + cv2.medianBlur(img_edges, 7), + 255, + cv2.ADAPTIVE_THRESH_MEAN_C, + cv2.THRESH_BINARY, + 9, + 2, + ) + img_edges = cv2.cvtColor(img_edges, cv2.COLOR_GRAY2RGB) + # combine color and edges + img = cv2.bitwise_and(img_color, img_edges) + return img + elif transform == "edges": + # perform edge detection + img = cv2.cvtColor(cv2.Canny(frame, 100, 200), cv2.COLOR_GRAY2BGR) + return img + else: + return np.flipud(frame) + + +css=""".my-group {max-width: 500px !important; max-height: 500px !important;} + .my-column {display: flex !important; justify-content: center !important; align-items: center !important};""" + +with gr.Blocks() as demo: + with gr.Column(elem_classes=["my-column"]): + with gr.Group(elem_classes=["my-group"]): + transform = gr.Dropdown(choices=["cartoon", "edges", "flip"], + value="flip", label="Transformation") + input_img = gr.Image(sources=["webcam"], type="numpy", streaming=True) + input_img.stream(transform_cv2, [input_img, transform], [input_img], time_limit=30, stream_every=0.1) + + +if __name__ == "__main__": + demo.launch(css=css) diff --git a/demo/streaming_simple/run.py b/demo/streaming_simple/run.py new file mode 100644 index 0000000..a08f066 --- /dev/null +++ b/demo/streaming_simple/run.py @@ -0,0 +1,13 @@ +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() diff --git a/demo/streaming_wav2vec/requirements.txt b/demo/streaming_wav2vec/requirements.txt new file mode 100644 index 0000000..4f492dd --- /dev/null +++ b/demo/streaming_wav2vec/requirements.txt @@ -0,0 +1,2 @@ +torch +transformers diff --git a/demo/streaming_wav2vec/run.py b/demo/streaming_wav2vec/run.py new file mode 100644 index 0000000..033b0e8 --- /dev/null +++ b/demo/streaming_wav2vec/run.py @@ -0,0 +1,28 @@ +from transformers import pipeline +import gradio as gr +import time + +p = pipeline("automatic-speech-recognition") + +def transcribe(audio, state=""): + time.sleep(2) + text = p(audio)["text"] # type: ignore + state += text + " " # type: ignore + return state, state + +demo = gr.Interface( + fn=transcribe, + inputs=[ + gr.Audio(sources=["microphone"], type="filepath", streaming=True), + "state" + ], + outputs=[ + "textbox", + "state" + ], + live=True, + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/stt_or_tts/run.py b/demo/stt_or_tts/run.py new file mode 100644 index 0000000..cc11675 --- /dev/null +++ b/demo/stt_or_tts/run.py @@ -0,0 +1,27 @@ +import gradio as gr + +tts_examples = [ + "I love learning machine learning", + "How do you do?", +] + +tts_demo = gr.load( + "huggingface/facebook/fastspeech2-en-ljspeech", + title=None, + examples=tts_examples, + description="Give me something to say!", + cache_examples=False +) + +stt_demo = gr.load( + "huggingface/facebook/wav2vec2-base-960h", + title=None, + inputs=gr.Microphone(type="filepath"), + description="Let me try to guess what you're saying!", + cache_examples=False +) + +demo = gr.TabbedInterface([tts_demo, stt_demo], ["Text-to-speech", "Speech-to-text"]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/sub_block_render/frog.jpg b/demo/sub_block_render/frog.jpg new file mode 100644 index 0000000..031b404 Binary files /dev/null and b/demo/sub_block_render/frog.jpg differ diff --git a/demo/sub_block_render/requirements.txt b/demo/sub_block_render/requirements.txt new file mode 100644 index 0000000..7e2fba5 --- /dev/null +++ b/demo/sub_block_render/requirements.txt @@ -0,0 +1 @@ +Pillow diff --git a/demo/sub_block_render/run.py b/demo/sub_block_render/run.py new file mode 100644 index 0000000..136828d --- /dev/null +++ b/demo/sub_block_render/run.py @@ -0,0 +1,77 @@ +import gradio as gr +import os +from pathlib import Path + +from PIL import Image + +root = Path(os.path.dirname(__file__)) + +def infer( + text, + guidance_scale, +): + + img = ( + Image.open(root / "cheetah.jpg") + if text == "Cheetah" + else Image.open(root / "frog.jpg") + ) + img = img.resize((224, 224)) + + return ([img, img, img, img], "image") + +block = gr.Blocks() + +examples = [ + ["A serious capybara at work, wearing a suit", 7], + ["A Squirtle fine dining with a view to the London Eye", 7], + ["A tamale food cart in front of a Japanese Castle", 7], + ["a graffiti of a robot serving meals to people", 7], + ["a beautiful cabin in Attersee, Austria, 3d animation style", 7], +] + +with block as demo: + with gr.Row(elem_id="prompt-container", equal_height=True): + text = gr.Textbox( + label="Enter your prompt", + show_label=False, + max_lines=1, + placeholder="Enter your prompt", + elem_id="prompt-text-input", + ) + + gallery = gr.Gallery( + label="Generated images", show_label=False, elem_id="gallery", rows=2, columns=2 + ) + out_txt = gr.Textbox( + label="Prompt", + placeholder="Enter a prompt to generate an image", + lines=3, + elem_id="prompt-text-input", + ) + + guidance_scale = gr.Slider( + label="Guidance Scale", minimum=0, maximum=50, value=7.5, step=0.1 + ) + + ex = gr.Examples( + examples=examples, + fn=infer, + inputs=[text, guidance_scale], + outputs=[gallery, out_txt], + cache_examples=True, + ) + + text.submit( + infer, + inputs=[text, guidance_scale], + outputs=[gallery, out_txt], + concurrency_id="infer", + concurrency_limit=8, + ) + +with gr.Blocks() as demo: + block.render() + +if __name__ == "__main__": + demo.queue(max_size=10, api_open=False).launch() diff --git a/demo/super_html/run.py b/demo/super_html/run.py new file mode 100644 index 0000000..f21b9f8 --- /dev/null +++ b/demo/super_html/run.py @@ -0,0 +1,456 @@ +import os +import gradio as gr + + +with gr.Blocks() as demo: + gr.Markdown(""" + # Simple HTML usecase + This is the classic `gr.HTML` usecase where we just want to render some static HTML. + """) + simple_html = gr.HTML("

Hello, World!

") + + gr.Markdown(""" + # Templated HTML usecase + 'value' can now be anything, and it can be used inside the `html_template` using `${value}` syntax. + Note that when used as output or input, `value` is just this specific value rather than the entire HTML. + """) + with gr.Row(): + name1 = gr.Textbox(label="Name") + templated_html = gr.HTML( + "", + html_template="

Hello, {{value}}! ${value.length} letters

", + elem_id="templated", + ) + name1.change(lambda x: x, inputs=name1, outputs=templated_html) + + gr.Markdown(""" + # Additional Props + You are not limited to using `${value}` in the templates, you can add any number of custom tags to the template, and pass them to the component as keyword arguments. These props can be updated via python event listeners as well. + """) + with gr.Row(): + templated_html_props = gr.HTML( + "John", + html_template=""" +

Hello, ${value}!

+ """, + fontSize=30, + elem_id="props", + ) + slider = gr.Slider(10, 100, value=30, label="Font Size") + slider.change( + lambda x: gr.HTML(fontSize=x), inputs=slider, outputs=templated_html_props + ) + + gr.Markdown(""" + # CSS Templating + We can also template CSS, which is automatically scoped to the component. + """) + with gr.Row(): + name2 = gr.Textbox(label="Person") + color = gr.ColorPicker(label="Text Color", value="#00ff00") + bold = gr.Checkbox(label="Bold Text", value=True) + templated_html_css = gr.HTML( + ["J", "o", "h", "n"], + html_template=""" +

Hello, ${value.join('')}!

+
    + {{#each value}} +
  • {{this}}
  • + {{/each}} +
+ """, + css_template=""" + h1, li { + color: ${color}; + font-weight: ${bold ? 'bold' : 'normal'}; + } + """, + color="green", + bold=True, + elem_id="css", + ) + with gr.Row(): + btn = gr.Button("Update HTML") + btn_blue = gr.Button("Make HTML Blue") + + def update_templated_html_css(name, color, bold): + return gr.HTML(value=list(name), color=color, bold=bold) + + btn.click( + update_templated_html_css, + inputs=[name2, color, bold], + outputs=templated_html_css, + ) + btn_blue.click(lambda: gr.HTML(color="blue"), outputs=templated_html_css) + + gr.Markdown(""" + # JS Prop Updates + We can now trigger events from gr.HTML using event listeners in `js_on_load`. This script has access to `element` which refers to the parent element, and `trigger(event_name)` or `trigger(event_name, event_data)`, which can be used to dispatch events. + """) + + button_set = gr.HTML( + html_template=""" + + + + """, + css_template=""" + button { + padding: 10px; + background-color: red; + } + """, + js_on_load=""" + const buttons = element.querySelectorAll('button'); + buttons.forEach(button => { + button.addEventListener('click', () => { + trigger('click', {clicked: button.innerText}); + }); + }); + """, + elem_id="button_set", + ) + clicked_box = gr.Textbox(label="Clicked") + + def on_button_click(evt: gr.EventData): + return evt.clicked + + button_set.click(on_button_click, outputs=clicked_box) + + gr.Markdown(""" + # JS Prop Changes + You can also update `value` or any other prop of the component from JS using `props`, e.g., `props.value = "new value"` will update the `value` prop and re-render the HTML template. + """) + form = gr.HTML( + html_template=""" + +

${value.length} letters

+ + + """, + js_on_load=""" + const input = element.querySelector('input'); + const submit_button = element.querySelector('button.submit'); + const clear_button = element.querySelector('button.clear'); + input.addEventListener('input', () => { + props.valid = input.value.length > 5; + props.value = input.value; + }); + submit_button.addEventListener('click', () => { + trigger('submit'); + }); + clear_button.addEventListener('click', () => { + props.value = ""; + props.valid = false; + trigger('clear'); + }); + """, + valid=False, + elem_id="form", + ) + output_box = gr.Textbox(label="Output Box") + form.submit(lambda x: x, form, outputs=output_box) + output_box.submit(lambda x: x, output_box, outputs=form) + + gr.Markdown(""" + # Watch API + Use `watch` inside `js_on_load` to run a callback after the template re-renders whenever specific props are changed from the backend (Python event listener). The callback is NOT triggered by frontend (JavaScript) changes to props. The callback takes no arguments — read current values from `props` directly. + """) + watch_html = gr.HTML( + value=0, + html_template=""" +
+
value: ${value}
+
+ """, + js_on_load=""" + watch('value', () => { + if (props.value >= 3) { + trigger('submit'); + } + }); + """, + elem_id="watch_demo", + ) + watch_output = gr.Textbox(label="Watch Output") + watch_inc_btn = gr.Button("Increment") + watch_inc_btn.click(lambda x: (x or 0) + 1, watch_html, outputs=watch_html) + watch_html.submit(lambda x: x, watch_html, outputs=watch_output) + + gr.Markdown(""" + # Extending gr.HTML for new Components + You can create your own Components by extending the gr.HTML class. + """) + + class ListComponent(gr.HTML): + def __init__(self, container=True, label="List", ordered=False, **kwargs): + self.ordered = ordered + super().__init__( + html_template=""" +

${label}

+ ${ordered ? `
    ` : `
      `} + ${value.map(item => `
    • ${item}
    • `).join('')} + ${ordered ? `
` : ``} + """, + container=container, + label=label, + ordered=ordered, + **kwargs, + ) + + l1 = ListComponent( + label="Fruits", value=["Apple", "Banana", "Cherry"], elem_id="fruits" + ) + l2 = ListComponent( + label="Vegetables", + value=["Carrot", "Broccoli", "Spinach"], + elem_id="vegetables", + ) + + make_ordered_btn = gr.Button("Make Ordered") + make_unordered_btn = gr.Button("Make Unordered") + + make_ordered_btn.click( + lambda: [ListComponent(ordered=True), ListComponent(ordered=True)], + outputs=[l1, l2], + ) + make_unordered_btn.click( + lambda: [ListComponent(ordered=False), ListComponent(ordered=False)], + outputs=[l1, l2], + ) + + failed_template = gr.HTML( + value=None, + html_template=""" + ${Zalue} + """, + ) + + gr.Markdown(""" + # File Upload via gr.HTML + The `upload` async function is available in `js_on_load`. It takes a JavaScript `File` object, + uploads it to the Gradio server, and returns the server-side file path as a string. + """) + + upload_html = gr.HTML( + html_template=""" +
+ + +

No file uploaded yet.

+
+ """, + js_on_load=""" + const input = element.querySelector('#html-file-input'); + const btn = element.querySelector('#html-upload-btn'); + const status = element.querySelector('#html-upload-status'); + + btn.addEventListener('click', async () => { + const file = input.files[0]; + if (!file) { + status.textContent = 'Please select a file first.'; + return; + } + status.textContent = 'Uploading...'; + try { + const { path, url } = await upload(file); + status.textContent = 'Uploaded: ' + path; + trigger('upload', { path: path, url: url, name: file.name }); + } catch (e) { + status.textContent = 'Upload failed: ' + e.message; + } + }); + """, + elem_id="upload_html" + ) + upload_result = gr.Textbox(label="Upload Result", elem_id="upload_result") + + def on_html_upload(evt: gr.EventData): + return evt.path + + upload_html.upload(on_html_upload, outputs=upload_result) + + class TodoList(gr.HTML): + def __init__( + self, + value: list[str] | None = None, + completed: list[int] | None = None, + **kwargs, + ): + self.completed = completed or [] + super().__init__( + html_template=""" +

Todo List

+
    + ${value.map((item, index) => ` +
  • + + ${item} +
  • + `).join('')} +
+ """, + js_on_load=""" + const checkboxes = element.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach(checkbox => { + checkbox.addEventListener('change', () => { + const index = parseInt(checkbox.getAttribute('data-index')); + let completed = props.completed || []; + if (checkbox.checked) { + if (!completed.includes(index)) { + completed.push(index); + } + } else { + completed = completed.filter(i => i !== index); + } + props.completed = [...completed]; + console.log(JSON.stringify(props.completed)) + }); + }); + """, + completed=self.completed, + value=value, + **kwargs, + ) + + todo_list = TodoList( + value=["Buy groceries", "Walk the dog", "Read a book"], + completed=[1], + elem_id="todo", + ) + + gr.Markdown(""" + # HTML Children + Use `@children` in `html_template` to render child components inside the HTML wrapper. + """) + with gr.HTML( + html_template=""" +

${title}

+ @children + + """, + css_template=""" + border: 2px solid gray; + border-radius: 8px; + padding: 16px; + """, + js_on_load=""" + element.querySelector('.send').addEventListener('click', () => { + trigger('submit'); + }); + """, + title="Contact Form", + elem_id="children_form", + ) as children_form: + children_name = gr.Textbox(label="Your Name") + children_email = gr.Textbox(label="Your Email") + + children_output = gr.Textbox(label="Children Output") + children_form.submit( + lambda name, email: f"Name: {name}, Email: {email}", + inputs=[children_name, children_email], + outputs=children_output, + ) + + gr.Markdown(""" + # Server Functions + You can call Python functions from `js_on_load` using the `server` object. Pass a list of functions via `server_functions` and they become available as async methods on the `server` object in your JavaScript code. + """) + + def list_directory(path): + try: + items = sorted(os.listdir(path)) + return [ + {"name": item, "is_dir": os.path.isdir(os.path.join(path, item))} + for item in items[:20] + ] + except (FileNotFoundError, PermissionError): + return [] + + server_fn_html = gr.HTML( + value=os.path.dirname(__file__), + html_template=""" +
+

Directory: ${value}

+
+ +
+ """, + js_on_load=""" + const loadBtn = element.querySelector('#server-fn-load'); + const tree = element.querySelector('#server-fn-tree'); + loadBtn.addEventListener('click', async () => { + tree.innerHTML = 'Loading...'; + const items = await server.list_directory(props.value); + tree.innerHTML = ''; + items.forEach(item => { + const el = document.createElement('div'); + el.textContent = (item.is_dir ? '📁 ' : '📄 ') + item.name; + el.className = 'server-fn-item'; + tree.appendChild(el); + }); + }); + """, + css_template=""" + #server-fn-tree { padding: 8px; min-height: 20px; } + .server-fn-item { padding: 2px 8px; } + #server-fn-load { padding: 6px 12px; margin-top: 8px; } + """, + server_functions=[list_directory], + elem_id="server_fns", + ) + + gr.Markdown(""" + # Custom Events + You can trigger custom events (not in the standard event list) from `js_on_load` using `trigger('custom_event_name', data)`. As long as the event name appears in quotes in `js_on_load`, you can attach a Python listener using `component.custom_event_name(fn, ...)`. + """) + + keyboard = gr.HTML( + html_template="

Press any key...

", + js_on_load=""" + document.addEventListener('keydown', (e) => { + trigger('keypress', {key: e.key}); + }); + """, + elem_id="custom_event", + ) + + key_output = gr.Textbox(label="Key Pressed", elem_id="key_output") + + def get_key(evt_data: gr.EventData): + return evt_data.key + + keyboard.keypress(get_key, None, key_output) + + gr.Markdown(""" + # Head / Third-Party Scripts + The `head` parameter lets you load third-party JS/CSS libraries directly on the component. + Scripts are deduplicated by `src`, so multiple components sharing the same library only load it once. + """) + + head_html = gr.HTML( + value=[30, 70, 45, 90, 60], + html_template=""" + + """, + js_on_load=""" + const canvas = element.querySelector('#head-chart'); + new Chart(canvas, { + type: 'bar', + data: { + labels: props.value.map((_, i) => 'Item ' + (i + 1)), + datasets: [{ + label: 'Values', + data: props.value, + backgroundColor: 'rgba(99, 132, 255, 0.5)', + }] + }, + options: { responsive: false } + }); + """, + head='', + elem_id="head_demo", + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/tab_render_children/run.py b/demo/tab_render_children/run.py new file mode 100644 index 0000000..cad746a --- /dev/null +++ b/demo/tab_render_children/run.py @@ -0,0 +1,16 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown("## Tab render_children parameter") + with gr.Tabs(): + with gr.Tab("Tab 1") as tab1: + gr.Markdown("This tab is visible by default") + with gr.Tab("Tab 2", render_children=True) as tab2: + tb = gr.Textbox(label="Will be rendered but hidden", elem_id="invisible-but-rendered") + tb2 = gr.Textbox(label="Will not be rendered", elem_id="invisible-and-not-rendered", visible=False) + tb3 = gr.Textbox(label="Will be rendered but hidden with visible='hidden'", elem_id="visibility-hidden", visible="hidden") + btn = gr.Button("Make textbox interactive", variant="primary") + btn.click(lambda: gr.update(interactive=True), None, tb) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/tabbed_interface_lite/run.py b/demo/tabbed_interface_lite/run.py new file mode 100644 index 0000000..dedcf5a --- /dev/null +++ b/demo/tabbed_interface_lite/run.py @@ -0,0 +1,10 @@ +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"]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/tabs/run.py b/demo/tabs/run.py new file mode 100644 index 0000000..4b22953 --- /dev/null +++ b/demo/tabs/run.py @@ -0,0 +1,71 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Tabs() as outer_tabs: + with gr.Tab("Set 1"): + with gr.Tabs(selected="a3") as tabs_1: + tabset_1 = [] + textset_1 = [] + for i in range(10): + with gr.Tab(f"Tab {i+1}", id=f"a{i+1}") as tab: + gr.Markdown(f"Text {i+1}!") + textbox = gr.Textbox(label=f"Input {i+1}") + tabset_1.append(tab) + textset_1.append(textbox) + with gr.Tab("Set 2"): + tabset_2 = [] + textset_2 = [] + for i in range(10): + with gr.Tab(f"Tab {i+11}") as tab: + gr.Markdown(f"Text {i+11}!") + textbox = gr.Textbox(label=f"Input {i+11}") + tabset_2.append(tab) + textset_2.append(textbox) + + for text1, text2 in zip(textset_1, textset_2): + text1.submit(lambda x: x, text1, text2) + + with gr.Tab("Locked Tab", id="locked", interactive=False) as locked_tab: + gr.Markdown("This tab was unlocked!") + + selected = gr.Textbox(label="Selected Tab") + outer_selected = gr.Textbox(label="Outer Container Tab") + select_count = gr.Number(label="Tab Select Count", value=0) + with gr.Row(): + hide_odd_btn = gr.Button("Hide Odd Tabs") + show_all_btn = gr.Button("Show All Tabs") + make_even_uninteractive_btn = gr.Button("Make Even Tabs Uninteractive") + make_all_interactive_btn = gr.Button("Make All Tabs Interactive") + unlock_btn = gr.Button("Unlock Tab") + + select_tab_num = gr.Number(label="Select Tab #", value=1) + + hide_odd_btn.click(lambda: [gr.Tab(visible=i % 2 == 1) for i, _ in enumerate(tabset_1 + tabset_2)], outputs=(tabset_1 + tabset_2)) + show_all_btn.click(lambda: [gr.Tab(visible=True) for tab in tabset_1 + tabset_2], outputs=(tabset_1 + tabset_2)) + make_even_uninteractive_btn.click(lambda: [gr.Tab(interactive=i % 2 == 0) for i, _ in enumerate(tabset_1 + tabset_2)], outputs=(tabset_1 + tabset_2)) + make_all_interactive_btn.click(lambda: [gr.Tab(interactive=True) for tab in tabset_1 + tabset_2], outputs=(tabset_1 + tabset_2)) + unlock_btn.click(lambda: gr.Tab(interactive=True), outputs=locked_tab) + select_tab_num.submit(lambda x: gr.Tabs(selected=f"a{x}"), inputs=select_tab_num, outputs=tabs_1) + + def get_selected_index(evt: gr.SelectData): + return evt.value + gr.on([tab.select for tab in tabset_1 + tabset_2], get_selected_index, outputs=selected) + + # Count how many times the per-tab `select` fires. Uses a server-side + # accumulator (not a read-modify-write of the component) so concurrent + # dispatches under `trigger_mode="multiple"` can't clobber each other, and + # `trigger_mode="multiple"` so a double-dispatch regression isn't masked by + # the default "once" mode. + tab_select_count = {"n": 0} + def count_tab_select(): + tab_select_count["n"] += 1 + return tab_select_count["n"] + gr.on([tab.select for tab in tabset_1 + tabset_2], count_tab_select, outputs=select_count, trigger_mode="multiple") + + # The `select` event on a Tabs container should fire when its active tab changes. + def get_outer_selected(evt: gr.SelectData): + return evt.value + outer_tabs.select(get_outer_selected, None, outer_selected) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/tabs_visibility/run.py b/demo/tabs_visibility/run.py new file mode 100644 index 0000000..4b6c026 --- /dev/null +++ b/demo/tabs_visibility/run.py @@ -0,0 +1,36 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Tab("abc"): + gr.Textbox(label="abc") + with gr.Tab("def", visible=False) as t: + gr.Textbox(label="def") + with gr.Tab("ghi"): + gr.Textbox(label="ghi") + with gr.Tab("jkl", visible=False) as t2: + gr.Textbox(label="jkl") + with gr.Tab("mno"): + gr.Textbox(label="mno") + with gr.Tab("pqr", visible=False) as t3: + gr.Textbox(label="pqr") + with gr.Tab("stu"): + gr.Textbox(label="stu") + with gr.Tab("vwx", visible=False) as t4: + gr.Textbox(label="vwx") + with gr.Tab("yz"): + gr.Textbox(label="yz") + b = gr.Button("Make visible") + + b.click( + lambda: [ + gr.Tab(visible=True), + gr.Tab(visible=True), + gr.Tab(visible=True), + gr.Tab(visible=True), + ], + inputs=None, + outputs=[t, t2, t3, t4], + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/tax_calculator/DESCRIPTION.md b/demo/tax_calculator/DESCRIPTION.md new file mode 100644 index 0000000..9ed8163 --- /dev/null +++ b/demo/tax_calculator/DESCRIPTION.md @@ -0,0 +1 @@ +Calculate taxes using Textbox, Radio, and Dataframe components \ No newline at end of file diff --git a/demo/tax_calculator/run.py b/demo/tax_calculator/run.py new file mode 100644 index 0000000..0add680 --- /dev/null +++ b/demo/tax_calculator/run.py @@ -0,0 +1,40 @@ +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"], # type: ignore + 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() diff --git a/demo/test_chatinterface_examples/eager_caching_examples_testcase.py b/demo/test_chatinterface_examples/eager_caching_examples_testcase.py new file mode 100644 index 0000000..57f3b77 --- /dev/null +++ b/demo/test_chatinterface_examples/eager_caching_examples_testcase.py @@ -0,0 +1,26 @@ +import gradio as gr + +def generate( + message: str, + chat_history: list[dict], +): + + output = "" + for character in message: + output += character + yield output + + +demo = gr.ChatInterface( + fn=generate, + examples=[ + ["Hey"], + ["Can you explain briefly to me what is the Python programming language?"], + ], + cache_examples=True, + cache_mode="eager", +) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/test_chatinterface_examples/lazy_caching_examples_testcase.py b/demo/test_chatinterface_examples/lazy_caching_examples_testcase.py new file mode 100644 index 0000000..fc2b8c1 --- /dev/null +++ b/demo/test_chatinterface_examples/lazy_caching_examples_testcase.py @@ -0,0 +1,26 @@ +import gradio as gr + +def generate( + message: str, + chat_history: list[dict], +): + + output = "" + for character in message: + output += character + yield output + + +demo = gr.ChatInterface( + fn=generate, + examples=[ + ["Hey"], + ["Can you explain briefly to me what is the Python programming language?"], + ], + cache_examples=True, + cache_mode="lazy", +) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/test_chatinterface_examples/multimodal_messages_examples_testcase.py b/demo/test_chatinterface_examples/multimodal_messages_examples_testcase.py new file mode 100644 index 0000000..6f82345 --- /dev/null +++ b/demo/test_chatinterface_examples/multimodal_messages_examples_testcase.py @@ -0,0 +1,26 @@ +import gradio as gr + +def generate( + message: dict, + chat_history: list[dict], +): + + output = "" + for character in message['text']: + output += character + yield output + + +demo = gr.ChatInterface( + fn=generate, + examples=[ + [{"text": "Hey"}], + [{"text": "Can you explain briefly to me what is the Python programming language?"}], + ], + cache_examples=False, + multimodal=True, +) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/test_chatinterface_examples/run.py b/demo/test_chatinterface_examples/run.py new file mode 100644 index 0000000..d6c25af --- /dev/null +++ b/demo/test_chatinterface_examples/run.py @@ -0,0 +1,25 @@ +import gradio as gr + +def generate( + message: str, + chat_history: list[dict], +): + + output = "" + for character in message: + output += character + yield output + + +demo = gr.ChatInterface( + fn=generate, + examples=[ + ["Hey"], + ["Can you explain briefly to me what is the Python programming language?"], + ], + cache_examples=False, +) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/test_chatinterface_multimodal_examples/cached_testcase.py b/demo/test_chatinterface_multimodal_examples/cached_testcase.py new file mode 100644 index 0000000..509c590 --- /dev/null +++ b/demo/test_chatinterface_multimodal_examples/cached_testcase.py @@ -0,0 +1,18 @@ +import gradio as gr + +image = gr.get_image("avatar.png") +audio = gr.get_audio("cantina.wav") + +def echo(message, history): + return f"You wrote: {message['text']} and uploaded {len(message['files'])} files." + +demo = gr.ChatInterface( + fn=echo, + examples=[{"text": "hello"}, {"text": "hola", "files": [image]}, {"text": "merhaba", "files": [image, audio]}], # type: ignore + title="Echo Bot", + multimodal=True, + api_name="chat", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/test_chatinterface_multimodal_examples/run.py b/demo/test_chatinterface_multimodal_examples/run.py new file mode 100644 index 0000000..509c590 --- /dev/null +++ b/demo/test_chatinterface_multimodal_examples/run.py @@ -0,0 +1,18 @@ +import gradio as gr + +image = gr.get_image("avatar.png") +audio = gr.get_audio("cantina.wav") + +def echo(message, history): + return f"You wrote: {message['text']} and uploaded {len(message['files'])} files." + +demo = gr.ChatInterface( + fn=echo, + examples=[{"text": "hello"}, {"text": "hola", "files": [image]}, {"text": "merhaba", "files": [image, audio]}], # type: ignore + title="Echo Bot", + multimodal=True, + api_name="chat", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/test_chatinterface_streaming_echo/multimodal_group_messages_testcase.py b/demo/test_chatinterface_streaming_echo/multimodal_group_messages_testcase.py new file mode 100644 index 0000000..9a56962 --- /dev/null +++ b/demo/test_chatinterface_streaming_echo/multimodal_group_messages_testcase.py @@ -0,0 +1,28 @@ +import gradio as gr + +runs = 0 + +def reset_runs(): + global runs + runs = 0 + +def slow_echo(message, history): + global runs # i didn't want to add state or anything to this demo + runs = runs + 1 + for i in range(len(message['text'])): + yield f"Run {runs} - You typed: " + message['text'][: i + 1] + if "files" in message and message["files"]: + yield f"Run {runs} - You typed: " + message['text'] + f" And you sent {len(message['files'])} files" + +chat = gr.ChatInterface(slow_echo, multimodal=True) + +with gr.Blocks() as demo: + chat.render() + # We reset the global variable to minimize flakes + # this works because CI runs only one test at at time + # need to use gr.State if we want to parallelize this test + # currently chatinterface does not support that + demo.unload(reset_runs) + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/test_chatinterface_streaming_echo/multimodal_messages_testcase.py b/demo/test_chatinterface_streaming_echo/multimodal_messages_testcase.py new file mode 100644 index 0000000..4dd7bd2 --- /dev/null +++ b/demo/test_chatinterface_streaming_echo/multimodal_messages_testcase.py @@ -0,0 +1,26 @@ +import gradio as gr + +runs = 0 + +def reset_runs(): + global runs + runs = 0 + +def slow_echo(message, history): + global runs # i didn't want to add state or anything to this demo + runs = runs + 1 + for i in range(len(message['text'])): + yield f"Run {runs} - You typed: " + message['text'][: i + 1] + +chat = gr.ChatInterface(slow_echo, multimodal=True, api_name="chat") + +with gr.Blocks() as demo: + chat.render() + # We reset the global variable to minimize flakes + # this works because CI runs only one test at at time + # need to use gr.State if we want to parallelize this test + # currently chatinterface does not support that + demo.unload(reset_runs) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/test_chatinterface_streaming_echo/multimodal_non_stream_testcase.py b/demo/test_chatinterface_streaming_echo/multimodal_non_stream_testcase.py new file mode 100644 index 0000000..8b93341 --- /dev/null +++ b/demo/test_chatinterface_streaming_echo/multimodal_non_stream_testcase.py @@ -0,0 +1,25 @@ +import gradio as gr + +runs = 0 + +def reset_runs(): + global runs + runs = 0 + +def slow_echo(message, history): + global runs # i didn't want to add state or anything to this demo + runs = runs + 1 + return f"Run {runs} - You typed: " + message['text'] + +chat = gr.ChatInterface(slow_echo, multimodal=True, api_name="chat") + +with gr.Blocks() as demo: + chat.render() + # We reset the global variable to minimize flakes + # this works because CI runs only one test at at time + # need to use gr.State if we want to parallelize this test + # currently chatinterface does not support that + demo.unload(reset_runs) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/test_chatinterface_streaming_echo/run.py b/demo/test_chatinterface_streaming_echo/run.py new file mode 100644 index 0000000..a69d35e --- /dev/null +++ b/demo/test_chatinterface_streaming_echo/run.py @@ -0,0 +1,28 @@ +import gradio as gr +import time + +runs = 0 + +def reset_runs(): + global runs + runs = 0 + +def slow_echo(message, history): + global runs # i didn't want to add state or anything to this demo + runs = runs + 1 + for i in range(len(message)): + time.sleep(0.02) + yield f"Run {runs} - You typed: " + message[: i + 1] + +chat = gr.ChatInterface(slow_echo, fill_height=True, editable=True, api_name="chat") + +with gr.Blocks() as demo: + chat.render() + # We reset the global variable to minimize flakes + # this works because CI runs only one test at at time + # need to use gr.State if we want to parallelize this test + # currently chatinterface does not support that + demo.unload(reset_runs) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/text_analysis/DESCRIPTION.md b/demo/text_analysis/DESCRIPTION.md new file mode 100644 index 0000000..dddeb43 --- /dev/null +++ b/demo/text_analysis/DESCRIPTION.md @@ -0,0 +1 @@ +This simple demo takes advantage of Gradio's HighlightedText, JSON and HTML outputs to create a clear NER segmentation. \ No newline at end of file diff --git a/demo/text_analysis/requirements.txt b/demo/text_analysis/requirements.txt new file mode 100644 index 0000000..33ea94c --- /dev/null +++ b/demo/text_analysis/requirements.txt @@ -0,0 +1 @@ +spacy \ No newline at end of file diff --git a/demo/text_analysis/run.py b/demo/text_analysis/run.py new file mode 100644 index 0000000..2cf76d3 --- /dev/null +++ b/demo/text_analysis/run.py @@ -0,0 +1,39 @@ +import gradio as gr +import os +os.system('python -m spacy download en_core_web_sm') +import spacy # type: ignore +from spacy import displacy # type: ignore + +nlp = spacy.load("en_core_web_sm") + +def text_analysis(text): + doc = nlp(text) + html = displacy.render(doc, style="dep", page=True) + html = ( + "
" + + html + + "
" + ) + pos_count = { + "char_count": len(text), + "token_count": 0, + } + pos_tokens = [] + + for token in doc: + pos_tokens.extend([(token.text, token.pos_), (" ", None)]) + + return pos_tokens, pos_count, html + +demo = gr.Interface( + text_analysis, + gr.Textbox(placeholder="Enter sentence here..."), + ["highlight", "json", "html"], + examples=[ + ["What a beautiful morning for a walk!"], + ["It was the best of times, it was the worst of times."], + ], + api_name="predict", +) + +demo.launch() diff --git a/demo/text_analysis/screenshot.png b/demo/text_analysis/screenshot.png new file mode 100644 index 0000000..95d4756 Binary files /dev/null and b/demo/text_analysis/screenshot.png differ diff --git a/demo/text_analysis/setup.sh b/demo/text_analysis/setup.sh new file mode 100644 index 0000000..ad8bb8e --- /dev/null +++ b/demo/text_analysis/setup.sh @@ -0,0 +1 @@ +python -m spacy download en_core_web_sm \ No newline at end of file diff --git a/demo/text_generation/DESCRIPTION.md b/demo/text_generation/DESCRIPTION.md new file mode 100644 index 0000000..5b9716d --- /dev/null +++ b/demo/text_generation/DESCRIPTION.md @@ -0,0 +1 @@ +This text generation demo takes in input text and returns generated text. It uses the Transformers library to set up the model and has two examples. \ No newline at end of file diff --git a/demo/text_generation/requirements.txt b/demo/text_generation/requirements.txt new file mode 100644 index 0000000..05ec8d9 --- /dev/null +++ b/demo/text_generation/requirements.txt @@ -0,0 +1,3 @@ +git+https://github.com/huggingface/transformers +gradio +torch \ No newline at end of file diff --git a/demo/text_generation/run.py b/demo/text_generation/run.py new file mode 100644 index 0000000..1c49784 --- /dev/null +++ b/demo/text_generation/run.py @@ -0,0 +1,22 @@ +import gradio as gr +from transformers import pipeline + +generator = pipeline('text-generation', model='gpt2') + +def generate(text): + result = generator(text, max_length=30, num_return_sequences=1) + return result[0]["generated_text"] # type: ignore + +examples = [ + ["The Moon's orbit around Earth has"], + ["The smooth Borealis basin in the Northern Hemisphere covers 40%"], +] + +demo = gr.Interface( + fn=generate, + inputs=gr.Textbox(lines=5, label="Input Text"), + outputs=gr.Textbox(label="Generated Text"), + examples=examples +) + +demo.launch() diff --git a/demo/textbox_component/run.py b/demo/textbox_component/run.py new file mode 100644 index 0000000..86b8316 --- /dev/null +++ b/demo/textbox_component/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Textbox() + +demo.launch() diff --git a/demo/textbox_custom_buttons/run.py b/demo/textbox_custom_buttons/run.py new file mode 100644 index 0000000..79153e2 --- /dev/null +++ b/demo/textbox_custom_buttons/run.py @@ -0,0 +1,56 @@ +import gradio as gr + +def export_data(text): + print("Exporting data:", text) + return "Data exported to server!" + +def refresh_data(): + import random + return f"Refreshed content: {random.randint(1000, 9999)}" + +with gr.Blocks() as demo: + gr.Markdown(""" + # Textbox with Custom Buttons Demo + + This demo showcases custom buttons in a Textbox component that can trigger either (or both): + - **Python functions** + - **JS functions** (with and without input parameters) + + You can use emojis, text, or icons for the buttons. + """) + + gr.Markdown("### Textbox with Custom Buttons") + refresh_btn = gr.Button("Refresh") + alert_btn = gr.Button("⚠️ Alert") + clear_btn = gr.Button("🗑️") + + textbox = gr.Textbox( + value="Sample text content that can be exported, refreshed, or transformed.", + buttons=["copy", refresh_btn, alert_btn, clear_btn], + label="Sample Text", + lines=5 + ) + + output = gr.Textbox(label="Output (Python Function Result)") + + + refresh_btn.click(refresh_data, outputs=textbox) + + alert_btn.click( + None, + inputs=textbox, + outputs=[], + js="(text) => { alert('This is a JavaScript alert!\\n\\nTextbox content: ' + text); return []; }" + ) + + + clear_btn.click( + None, + inputs=[], + outputs=textbox, + js="() => ''" + ) + +if __name__ == "__main__": + demo.launch() + diff --git a/demo/theme_builder/custom_css.css b/demo/theme_builder/custom_css.css new file mode 100644 index 0000000..b09990e --- /dev/null +++ b/demo/theme_builder/custom_css.css @@ -0,0 +1,21 @@ +.gradio-container { + overflow: visible !important; + max-width: none !important; +} +#controls { + max-height: 100vh; + flex-wrap: unset; + overflow-y: scroll; + position: sticky; + top: 0; +} +#controls::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; +} + +#controls::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0, 0, 0, .5); + box-shadow: 0 0 1px rgba(255, 255, 255, .5); +} \ No newline at end of file diff --git a/demo/theme_builder/run.py b/demo/theme_builder/run.py new file mode 100644 index 0000000..05f3199 --- /dev/null +++ b/demo/theme_builder/run.py @@ -0,0 +1,7 @@ +import gradio as gr +from gradio.themes.builder_app import demo + +if __name__ == "__main__": + from gradio.utils import get_space + path = "custom_css.css" if get_space() else "demo/custom_css/custom_css.css" + demo.launch(css_paths=["demo/custom_css/custom_css.css"], theme=gr.themes.Base(), head="") diff --git a/demo/theme_extended_step_1/run.py b/demo/theme_extended_step_1/run.py new file mode 100644 index 0000000..254a486 --- /dev/null +++ b/demo/theme_extended_step_1/run.py @@ -0,0 +1,19 @@ +import gradio as gr +import time + +with gr.Blocks() as demo: + textbox = gr.Textbox(label="Name") + slider = gr.Slider(label="Count", minimum=0, maximum=100, step=1) + with gr.Row(): + button = gr.Button("Submit", variant="primary") + clear = gr.Button("Clear") + output = gr.Textbox(label="Output") + + def repeat(name, count): + time.sleep(3) + return name * count + + button.click(repeat, [textbox, slider], output) + +if __name__ == "__main__": + demo.launch(theme=gr.themes.Default(primary_hue="red", secondary_hue="pink")) diff --git a/demo/theme_extended_step_2/run.py b/demo/theme_extended_step_2/run.py new file mode 100644 index 0000000..d984540 --- /dev/null +++ b/demo/theme_extended_step_2/run.py @@ -0,0 +1,19 @@ +import gradio as gr +import time + +with gr.Blocks() as demo: + textbox = gr.Textbox(label="Name") + slider = gr.Slider(label="Count", minimum=0, maximum=100, step=1) + with gr.Row(): + button = gr.Button("Submit", variant="primary") + clear = gr.Button("Clear") + output = gr.Textbox(label="Output") + + def repeat(name, count): + time.sleep(3) + return name * count + + button.click(repeat, [textbox, slider], output) + +if __name__ == "__main__": + demo.launch(theme=gr.themes.Default(spacing_size="sm", radius_size="none")) diff --git a/demo/theme_extended_step_3/run.py b/demo/theme_extended_step_3/run.py new file mode 100644 index 0000000..32af46e --- /dev/null +++ b/demo/theme_extended_step_3/run.py @@ -0,0 +1,23 @@ +import gradio as gr +import time + +with gr.Blocks() as demo: + textbox = gr.Textbox(label="Name") + slider = gr.Slider(label="Count", minimum=0, maximum=100, step=1) + with gr.Row(): + button = gr.Button("Submit", variant="primary") + clear = gr.Button("Clear") + output = gr.Textbox(label="Output") + + def repeat(name, count): + time.sleep(3) + return name * count + + button.click(repeat, [textbox, slider], output) + +if __name__ == "__main__": + demo.launch( + theme=gr.themes.Default( + font=[gr.themes.GoogleFont("Inconsolata"), "Arial", "sans-serif"] + ) +) diff --git a/demo/theme_extended_step_4/run.py b/demo/theme_extended_step_4/run.py new file mode 100644 index 0000000..1b8eb2f --- /dev/null +++ b/demo/theme_extended_step_4/run.py @@ -0,0 +1,24 @@ +import gradio as gr +import time + +theme = gr.themes.Default(primary_hue="blue").set( + loader_color="#FF0000", + slider_color="#FF0000", +) + +with gr.Blocks() as demo: + textbox = gr.Textbox(label="Name") + slider = gr.Slider(label="Count", minimum=0, maximum=100, step=1) + with gr.Row(): + button = gr.Button("Submit", variant="primary") + clear = gr.Button("Clear") + output = gr.Textbox(label="Output") + + def repeat(name, count): + time.sleep(3) + return name * count + + button.click(repeat, [textbox, slider], output) + +if __name__ == "__main__": + demo.launch(theme=theme) diff --git a/demo/theme_new_step_1/run.py b/demo/theme_new_step_1/run.py new file mode 100644 index 0000000..cd5ff21 --- /dev/null +++ b/demo/theme_new_step_1/run.py @@ -0,0 +1,25 @@ +import gradio as gr +from gradio.themes.base import Base +import time + +class Seafoam(Base): + pass + +seafoam = Seafoam() + +with gr.Blocks() as demo: + textbox = gr.Textbox(label="Name") + slider = gr.Slider(label="Count", minimum=0, maximum=100, step=1) + with gr.Row(): + button = gr.Button("Submit", variant="primary") + clear = gr.Button("Clear") + output = gr.Textbox(label="Output") + + def repeat(name, count): + time.sleep(3) + return name * count + + button.click(repeat, [textbox, slider], output) + +if __name__ == "__main__": + demo.launch(theme=seafoam) diff --git a/demo/theme_new_step_2/run.py b/demo/theme_new_step_2/run.py new file mode 100644 index 0000000..0b82a9c --- /dev/null +++ b/demo/theme_new_step_2/run.py @@ -0,0 +1,61 @@ +from __future__ import annotations +from typing import Iterable +import gradio as gr +from gradio.themes.base import Base +from gradio.themes.utils import colors, fonts, sizes +import time + +class Seafoam(Base): + def __init__( + self, + *, + primary_hue: colors.Color | str = colors.emerald, + secondary_hue: colors.Color | str = colors.blue, + neutral_hue: colors.Color | str = colors.gray, + spacing_size: sizes.Size | str = sizes.spacing_md, + radius_size: sizes.Size | str = sizes.radius_md, + text_size: sizes.Size | str = sizes.text_lg, + font: fonts.Font + | str + | Iterable[fonts.Font | str] = ( + fonts.GoogleFont("Quicksand"), + "ui-sans-serif", + "sans-serif", + ), + font_mono: fonts.Font + | str + | Iterable[fonts.Font | str] = ( + fonts.GoogleFont("IBM Plex Mono"), + "ui-monospace", + "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, + ) + +seafoam = Seafoam() + +with gr.Blocks() as demo: + textbox = gr.Textbox(label="Name") + slider = gr.Slider(label="Count", minimum=0, maximum=100, step=1) + with gr.Row(): + button = gr.Button("Submit", variant="primary") + clear = gr.Button("Clear") + output = gr.Textbox(label="Output") + + def repeat(name, count): + time.sleep(3) + return name * count + + button.click(repeat, [textbox, slider], output) + +if __name__ == "__main__": + demo.launch(theme=seafoam) diff --git a/demo/theme_new_step_3/run.py b/demo/theme_new_step_3/run.py new file mode 100644 index 0000000..fe376b9 --- /dev/null +++ b/demo/theme_new_step_3/run.py @@ -0,0 +1,76 @@ +from __future__ import annotations +from typing import Iterable +import gradio as gr +from gradio.themes.base import Base +from gradio.themes.utils import colors, fonts, sizes +import time + +class Seafoam(Base): + def __init__( + self, + *, + primary_hue: colors.Color | str = colors.emerald, + secondary_hue: colors.Color | str = colors.blue, + neutral_hue: colors.Color | str = colors.blue, + spacing_size: sizes.Size | str = sizes.spacing_md, + radius_size: sizes.Size | str = sizes.radius_md, + text_size: sizes.Size | str = sizes.text_lg, + font: fonts.Font + | str + | Iterable[fonts.Font | str] = ( + fonts.GoogleFont("Quicksand"), + "ui-sans-serif", + "sans-serif", + ), + font_mono: fonts.Font + | str + | Iterable[fonts.Font | str] = ( + fonts.GoogleFont("IBM Plex Mono"), + "ui-monospace", + "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, + ) + super().set( + body_background_fill="repeating-linear-gradient(45deg, *primary_200, *primary_200 10px, *primary_50 10px, *primary_50 20px)", + body_background_fill_dark="repeating-linear-gradient(45deg, *primary_800, *primary_800 10px, *primary_900 10px, *primary_900 20px)", + button_primary_background_fill="linear-gradient(90deg, *primary_300, *secondary_400)", + button_primary_background_fill_hover="linear-gradient(90deg, *primary_200, *secondary_300)", + button_primary_text_color="white", + button_primary_background_fill_dark="linear-gradient(90deg, *primary_600, *secondary_800)", + slider_color="*secondary_300", + slider_color_dark="*secondary_600", + block_title_text_weight="600", + block_border_width="3px", + block_shadow="*shadow_drop_lg", + button_primary_shadow="*shadow_drop_lg", + button_large_padding="32px", + ) + +seafoam = Seafoam() + +with gr.Blocks() as demo: + textbox = gr.Textbox(label="Name") + slider = gr.Slider(label="Count", minimum=0, maximum=100, step=1) + with gr.Row(): + button = gr.Button("Submit", variant="primary") + clear = gr.Button("Clear") + output = gr.Textbox(label="Output") + + def repeat(name, count): + time.sleep(3) + return name * count + + button.click(repeat, [textbox, slider], output) + +if __name__ == "__main__": + demo.launch(theme=seafoam) diff --git a/demo/theme_soft/run.py b/demo/theme_soft/run.py new file mode 100644 index 0000000..63b21ca --- /dev/null +++ b/demo/theme_soft/run.py @@ -0,0 +1,19 @@ +import gradio as gr +import time + +with gr.Blocks() as demo: + textbox = gr.Textbox(label="Name") + slider = gr.Slider(label="Count", minimum=0, maximum=100, step=1) + with gr.Row(): + button = gr.Button("Submit", variant="primary") + clear = gr.Button("Clear") + output = gr.Textbox(label="Output") + + def repeat(name, count): + time.sleep(3) + return name * count + + button.click(repeat, [textbox, slider], output) + +if __name__ == "__main__": + demo.launch(theme=gr.themes.Soft()) diff --git a/demo/tictactoe/run.py b/demo/tictactoe/run.py new file mode 100644 index 0000000..ddfaea5 --- /dev/null +++ b/demo/tictactoe/run.py @@ -0,0 +1,17 @@ +import gradio as gr + +with gr.Blocks() as demo: + turn = gr.Textbox("X", interactive=False, label="Turn") + board = gr.Dataframe(value=[["", "", ""]] * 3, interactive=False, type="array") + + def place(board: list[list[int]], turn, evt: gr.SelectData): # type: ignore + if evt.value: + return board, turn + board[evt.index[0]][evt.index[1]] = turn + turn = "O" if turn == "X" else "X" + return board, turn + + board.select(place, [board, turn], [board, turn], show_progress="hidden") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/timer/run.py b/demo/timer/run.py new file mode 100644 index 0000000..37f7608 --- /dev/null +++ b/demo/timer/run.py @@ -0,0 +1,31 @@ +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) + gr.Number(lambda: round(time.time()), label="Time 2", every=1) + + with gr.Row(): + timestamp_3 = gr.Number() + start_btn = gr.Button("Start") + stop_btn = gr.Button("Stop") + + time_3 = start_btn.click(lambda: round(time.time()), None, timestamp_3, every=1) + stop_btn.click(fn=None, cancels=time_3) + + with gr.Row(): + min = gr.Number(1, label="Min") + max = gr.Number(10, label="Max") + timer2 = gr.Timer(1) + number = gr.Number(lambda a, b: random.randint(a, b), inputs=[min, max], every=timer2, label="Random Number") + with gr.Row(): + gr.Button("Start").click(lambda: gr.Timer(active=True), None, timer2) + gr.Button("Stop").click(lambda: gr.Timer(active=False), None, timer2) + gr.Button("Go Fast").click(lambda: 0.2, None, timer2) + gr.Button("Go Slow").click(lambda: 2, None, timer2) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/timer_simple/run.py b/demo/timer_simple/run.py new file mode 100644 index 0000000..9c93557 --- /dev/null +++ b/demo/timer_simple/run.py @@ -0,0 +1,17 @@ +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() diff --git a/demo/timeseries-forecasting-with-prophet/DESCRIPTION.md b/demo/timeseries-forecasting-with-prophet/DESCRIPTION.md new file mode 100644 index 0000000..2914bc9 --- /dev/null +++ b/demo/timeseries-forecasting-with-prophet/DESCRIPTION.md @@ -0,0 +1 @@ +A simple dashboard showing pypi stats for python libraries. Updates on load, and has no buttons! \ No newline at end of file diff --git a/demo/timeseries-forecasting-with-prophet/requirements.txt b/demo/timeseries-forecasting-with-prophet/requirements.txt new file mode 100644 index 0000000..22bb657 --- /dev/null +++ b/demo/timeseries-forecasting-with-prophet/requirements.txt @@ -0,0 +1,5 @@ +holidays==0.24 +prophet==1.1.2 +pandas +pypistats +plotly \ No newline at end of file diff --git a/demo/timeseries-forecasting-with-prophet/run.py b/demo/timeseries-forecasting-with-prophet/run.py new file mode 100644 index 0000000..b5eeedf --- /dev/null +++ b/demo/timeseries-forecasting-with-prophet/run.py @@ -0,0 +1,41 @@ +import gradio as gr +import pypistats # type: ignore +from datetime import date +from dateutil.relativedelta import relativedelta +import pandas as pd +from prophet import Prophet # type: ignore +pd.options.plotting.backend = "plotly" + +def get_forecast(lib, time): + + data = pypistats.overall(lib, total=True, format="pandas") + data = data.groupby("category").get_group("with_mirrors").sort_values("date") + start_date = date.today() - relativedelta(months=int(time.split(" ")[0])) + df = data[(data['date'] > str(start_date))] + + df1 = df[['date','downloads']] + df1.columns = ['ds','y'] + + m = Prophet() + m.fit(df1) + future = m.make_future_dataframe(periods=90) + forecast = m.predict(future) + fig1 = m.plot(forecast) + return fig1 + +with gr.Blocks() as demo: + gr.Markdown( + """ + **Pypi Download Stats 📈 with Prophet Forecasting**: see live download stats for popular open-source libraries 🤗 along with a 3 month forecast using Prophet. The [ source code for this Gradio demo is here](https://huggingface.co/spaces/gradio/timeseries-forecasting-with-prophet/blob/main/app.py). + """) + with gr.Row(): + lib = gr.Dropdown(["pandas", "scikit-learn", "torch", "prophet"], label="Library", value="pandas") + time = gr.Dropdown(["3 months", "6 months", "9 months", "12 months"], label="Downloads over the last...", value="12 months") + + plt = gr.Plot() + + lib.change(get_forecast, [lib, time], plt, queue=False) + time.change(get_forecast, [lib, time], plt, queue=False) + demo.load(get_forecast, [lib, time], plt, queue=False) + +demo.launch() diff --git a/demo/titanic_survival/requirements.txt b/demo/titanic_survival/requirements.txt new file mode 100644 index 0000000..2bfe97f --- /dev/null +++ b/demo/titanic_survival/requirements.txt @@ -0,0 +1,3 @@ +scikit-learn +numpy +pandas \ No newline at end of file diff --git a/demo/titanic_survival/run.py b/demo/titanic_survival/run.py new file mode 100644 index 0000000..0a6d5ef --- /dev/null +++ b/demo/titanic_survival/run.py @@ -0,0 +1,106 @@ +import pandas as pd +from sklearn.ensemble import RandomForestClassifier # type: ignore +from sklearn.model_selection import train_test_split # type: ignore + +import gradio as gr +# get_file() returns the file path to sample data files included with Gradio +from gradio.media import get_file + +data = pd.read_csv(get_file("titanic.csv")) + +def encode_age(df): + df.Age = df.Age.fillna(-0.5) + bins = (-1, 0, 5, 12, 18, 25, 35, 60, 120) + categories = pd.cut(df.Age, bins, labels=False) + df.Age = categories + return df + +def encode_fare(df): + df.Fare = df.Fare.fillna(-0.5) + bins = (-1, 0, 8, 15, 31, 1000) + categories = pd.cut(df.Fare, bins, labels=False) + df.Fare = categories + return df + +def encode_df(df): + df = encode_age(df) + df = encode_fare(df) + sex_mapping = {"male": 0, "female": 1} + df = df.replace({"Sex": sex_mapping}) + embark_mapping = {"S": 1, "C": 2, "Q": 3} + df = df.replace({"Embarked": embark_mapping}) + df.Embarked = df.Embarked.fillna(0) + df["Company"] = 0 + df.loc[(df["SibSp"] > 0), "Company"] = 1 + df.loc[(df["Parch"] > 0), "Company"] = 2 + df.loc[(df["SibSp"] > 0) & (df["Parch"] > 0), "Company"] = 3 + df = df[ + [ + "PassengerId", + "Pclass", + "Sex", + "Age", + "Fare", + "Embarked", + "Company", + "Survived", + ] + ] + return df + +train = encode_df(data) + +X_all = train.drop(["Survived", "PassengerId"], axis=1) +y_all = train["Survived"] + +num_test = 0.20 +X_train, X_test, y_train, y_test = train_test_split( + X_all, y_all, test_size=num_test, random_state=23 +) + +clf = RandomForestClassifier() +clf.fit(X_train, y_train) +predictions = clf.predict(X_test) + +def predict_survival(passenger_class, is_male, age, company, fare, embark_point): + if passenger_class is None or embark_point is None: + return None + df = pd.DataFrame.from_dict( + { + "Pclass": [passenger_class + 1], + "Sex": [0 if is_male else 1], + "Age": [age], + "Fare": [fare], + "Embarked": [embark_point + 1], + "Company": [ + (1 if "Sibling" in company else 0) + (2 if "Child" in company else 0) + ] + } + ) + df = encode_age(df) + df = encode_fare(df) + pred = clf.predict_proba(df)[0] + return {"Perishes": float(pred[0]), "Survives": float(pred[1])} + +demo = gr.Interface( + predict_survival, + [ + gr.Dropdown(["first", "second", "third"], type="index"), + "checkbox", + gr.Slider(0, 80, value=25), + gr.CheckboxGroup(["Sibling", "Child"], label="Travelling with (select all)"), + gr.Number(value=20), + gr.Radio(["S", "C", "Q"], type="index"), + ], + "label", + examples=[ + ["first", True, 30, [], 50, "S"], + ["second", False, 40, ["Sibling", "Child"], 10, "Q"], + ["third", True, 30, ["Child"], 20, "S"], + ], + live=True, + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/titanic_survival/screenshot.png b/demo/titanic_survival/screenshot.png new file mode 100644 index 0000000..20f698d Binary files /dev/null and b/demo/titanic_survival/screenshot.png differ diff --git a/demo/todo_list/run.py b/demo/todo_list/run.py new file mode 100644 index 0000000..6edd226 --- /dev/null +++ b/demo/todo_list/run.py @@ -0,0 +1,38 @@ +import gradio as gr + +with gr.Blocks() as demo: + + tasks = gr.State([]) + new_task = gr.Textbox(label="Task Name", autofocus=True, max_lines=1) + + def add_task(tasks, new_task_name): + return tasks + [{"name": new_task_name, "complete": False}], "" + + new_task.submit(add_task, [tasks, new_task], [tasks, new_task]) + + @gr.render(inputs=tasks) + def render_todos(task_list): + complete = [task for task in task_list if task["complete"]] + incomplete = [task for task in task_list if not task["complete"]] + gr.Markdown(f"### Incomplete Tasks ({len(incomplete)})") + for task in incomplete: + with gr.Row(): + gr.Textbox(task['name'], show_label=False, container=False) + done_btn = gr.Button("Done", scale=0) + def mark_done(task=task): + task["complete"] = True + return task_list + done_btn.click(mark_done, None, [tasks]) + + delete_btn = gr.Button("Delete", scale=0, variant="stop") + def delete(task=task): + task_list.remove(task) + return task_list + delete_btn.click(delete, None, [tasks]) + + gr.Markdown(f"### Complete Tasks ({len(complete)})") + for task in complete: + gr.Textbox(task['name'], show_label=False, container=False) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/todo_list_js/run.py b/demo/todo_list_js/run.py new file mode 100644 index 0000000..4d58a15 --- /dev/null +++ b/demo/todo_list_js/run.py @@ -0,0 +1,32 @@ +""" +This is a simple todo list app that allows you to edit tasks and mark tasks as complete. +All actions are performed on the client side. +""" +import gradio as gr + +tasks = ["Get a job", "Marry rich", "", "", "", ""] +textboxes = [] +buttons = [] +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(scale=3): + gr.Markdown("# A Simple Interactive Todo List") + with gr.Column(scale=2): + with gr.Row(): + freeze_button = gr.Button("Freeze tasks", variant="stop") + edit_button = gr.Button("Edit tasks") + for i in range(6): + with gr.Row() as r: + t = gr.Textbox(tasks[i], placeholder="Enter a task", show_label=False, container=False, scale=7, interactive=True) + b = gr.Button("✔️", interactive=bool(tasks[i]), variant="primary" if tasks[i] else "secondary") + textboxes.append(t) + buttons.append(b) + t.change(lambda : gr.Button(interactive=True, variant="primary"), None, b, js=True) + b.click(lambda : gr.Row(visible=False), None, r, js=True) + freeze_button.click(lambda : [gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False), gr.Textbox(interactive=False)], None, textboxes, js=True) + edit_button.click(lambda : [gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True), gr.Textbox(interactive=True)], None, textboxes, js=True) + freeze_button.click(lambda : [gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False)], None, buttons, js=True) + edit_button.click(lambda : [gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=True)], None, buttons, js=True) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/translation/DESCRIPTION.md b/demo/translation/DESCRIPTION.md new file mode 100644 index 0000000..38e29c7 --- /dev/null +++ b/demo/translation/DESCRIPTION.md @@ -0,0 +1 @@ +This translation demo takes in the text, source and target languages, and returns the translation. It uses the Transformers library to set up the model and has a title, description, and example. \ No newline at end of file diff --git a/demo/translation/requirements.txt b/demo/translation/requirements.txt new file mode 100644 index 0000000..05ec8d9 --- /dev/null +++ b/demo/translation/requirements.txt @@ -0,0 +1,3 @@ +git+https://github.com/huggingface/transformers +gradio +torch \ No newline at end of file diff --git a/demo/translation/run.py b/demo/translation/run.py new file mode 100644 index 0000000..fe3b37a --- /dev/null +++ b/demo/translation/run.py @@ -0,0 +1,35 @@ +# type: ignore +import gradio as gr +from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, pipeline +import torch + +# this model was loaded from https://hf.co/models +model = AutoModelForSeq2SeqLM.from_pretrained("facebook/nllb-200-distilled-600M") +tokenizer = AutoTokenizer.from_pretrained("facebook/nllb-200-distilled-600M") +device = 0 if torch.cuda.is_available() else -1 +LANGS = ["ace_Arab", "eng_Latn", "fra_Latn", "spa_Latn"] + +def translate(text, src_lang, tgt_lang): + """ + Translate the text from source lang to target lang + """ + translation_pipeline = pipeline("translation", model=model, tokenizer=tokenizer, src_lang=src_lang, tgt_lang=tgt_lang, max_length=400, device=device) + result = translation_pipeline(text) + return result[0]['translation_text'] # type: ignore + +demo = gr.Interface( + fn=translate, + inputs=[ + gr.components.Textbox(label="Text"), + gr.components.Dropdown(label="Source Language", choices=LANGS), + gr.components.Dropdown(label="Target Language", choices=LANGS), + ], + outputs=["text"], + examples=[["Building a translation demo with Gradio is so easy!", "eng_Latn", "spa_Latn"]], + cache_examples=False, + title="Translation Demo", + description="This demo is a simplified version of the original [NLLB-Translator](https://huggingface.co/spaces/Narrativaai/NLLB-Translator) space", + api_name="predict" +) + +demo.launch() diff --git a/demo/unified_demo_text_generation/requirements.txt b/demo/unified_demo_text_generation/requirements.txt new file mode 100644 index 0000000..9216a18 --- /dev/null +++ b/demo/unified_demo_text_generation/requirements.txt @@ -0,0 +1,2 @@ +torch +transformers \ No newline at end of file diff --git a/demo/unified_demo_text_generation/run.py b/demo/unified_demo_text_generation/run.py new file mode 100644 index 0000000..c164727 --- /dev/null +++ b/demo/unified_demo_text_generation/run.py @@ -0,0 +1,15 @@ +import gradio as gr +from transformers import pipeline + +generator = pipeline('text-generation', model = 'gpt2') + +def generate_text(text_prompt): + response = generator(text_prompt, max_length = 30, num_return_sequences=5) + return response[0]['generated_text'] # type: ignore + +textbox = gr.Textbox() + +demo = gr.Interface(generate_text, textbox, textbox, api_name="predict") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/unispeech-speaker-verification/requirements.txt b/demo/unispeech-speaker-verification/requirements.txt new file mode 100644 index 0000000..baca898 --- /dev/null +++ b/demo/unispeech-speaker-verification/requirements.txt @@ -0,0 +1,2 @@ +git+https://github.com/huggingface/transformers +torchaudio diff --git a/demo/unispeech-speaker-verification/run.py b/demo/unispeech-speaker-verification/run.py new file mode 100644 index 0000000..8b757fd --- /dev/null +++ b/demo/unispeech-speaker-verification/run.py @@ -0,0 +1,117 @@ +# type: ignore +import gradio as gr +import torch +from torchaudio.sox_effects import apply_effects_file # type: ignore +from transformers import AutoFeatureExtractor, AutoModelForAudioXVector + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +STYLE = """ + +""" +OUTPUT_OK = ( + STYLE + + """ +
+

The speakers are

+

{:.1f}%

+

similar

+

Welcome, human!

+
(You must get at least 85% to be considered the same person)
+
+""" +) +OUTPUT_FAIL = ( + STYLE + + """ +
+

The speakers are

+

{:.1f}%

+

similar

+

You shall not pass!

+
(You must get at least 85% to be considered the same person)
+
+""" +) + +EFFECTS = [ + ["remix", "-"], + ["channels", "1"], + ["rate", "16000"], + ["gain", "-1.0"], + ["silence", "1", "0.1", "0.1%", "-1", "0.1", "0.1%"], + ["trim", "0", "10"], +] + +THRESHOLD = 0.85 + +model_name = "microsoft/unispeech-sat-base-plus-sv" +feature_extractor = AutoFeatureExtractor.from_pretrained(model_name) +model = AutoModelForAudioXVector.from_pretrained(model_name).to(device) +cosine_sim = torch.nn.CosineSimilarity(dim=-1) + +def similarity_fn(path1, path2): + if not (path1 and path2): + return 'ERROR: Please record audio for *both* speakers!' + wav1, _ = apply_effects_file(path1, EFFECTS) + wav2, _ = apply_effects_file(path2, EFFECTS) + print(wav1.shape, wav2.shape) + + input1 = feature_extractor(wav1.squeeze(0), return_tensors="pt", sampling_rate=16000).input_values.to(device) + input2 = feature_extractor(wav2.squeeze(0), return_tensors="pt", sampling_rate=16000).input_values.to(device) + + with torch.no_grad(): + emb1 = model(input1).embeddings + emb2 = model(input2).embeddings + emb1 = torch.nn.functional.normalize(emb1, dim=-1).cpu() + emb2 = torch.nn.functional.normalize(emb2, dim=-1).cpu() + similarity = cosine_sim(emb1, emb2).numpy()[0] + + if similarity >= THRESHOLD: + output = OUTPUT_OK.format(similarity * 100) + else: + output = OUTPUT_FAIL.format(similarity * 100) + + return output + +inputs = [ + gr.Audio(sources=["microphone"], type="filepath", label="Speaker #1"), + gr.Audio(sources=["microphone"], type="filepath", label="Speaker #2"), +] +output = gr.HTML(label="") + +description = ( + "This demo will compare two speech samples and determine if they are from the same speaker. " + "Try it with your own voice!" +) +article = ( + "

" + "🎙️ Learn more about UniSpeech-SAT | " + "📚 UniSpeech-SAT paper | " + "📚 X-Vector paper" + "

" +) +examples = [ + ["samples/cate_blanch.mp3", "samples/cate_blanch_2.mp3"], + ["samples/cate_blanch.mp3", "samples/cate_blanch_3.mp3"], + ["samples/cate_blanch_2.mp3", "samples/cate_blanch_3.mp3"], + ["samples/heath_ledger.mp3", "samples/heath_ledger_2.mp3"], + ["samples/cate_blanch.mp3", "samples/kirsten_dunst.wav"], +] + +demo = gr.Interface( + fn=similarity_fn, + inputs=inputs, + outputs=output, + title="Voice Authentication with UniSpeech-SAT + X-Vectors", + description=description, + article=article, + flagging_mode="never", + live=False, + examples=examples, + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() + diff --git a/demo/unispeech-speaker-verification/samples/cate_blanch_3.mp3 b/demo/unispeech-speaker-verification/samples/cate_blanch_3.mp3 new file mode 100644 index 0000000..fa0a9f7 Binary files /dev/null and b/demo/unispeech-speaker-verification/samples/cate_blanch_3.mp3 differ diff --git a/demo/unispeech-speaker-verification/samples/heath_ledger_2.mp3 b/demo/unispeech-speaker-verification/samples/heath_ledger_2.mp3 new file mode 100644 index 0000000..d74f1ec Binary files /dev/null and b/demo/unispeech-speaker-verification/samples/heath_ledger_2.mp3 differ diff --git a/demo/unispeech-speaker-verification/samples/kirsten_dunst.wav b/demo/unispeech-speaker-verification/samples/kirsten_dunst.wav new file mode 100644 index 0000000..7cf10c9 Binary files /dev/null and b/demo/unispeech-speaker-verification/samples/kirsten_dunst.wav differ diff --git a/demo/unload_event_test/run.py b/demo/unload_event_test/run.py new file mode 100644 index 0000000..16565bb --- /dev/null +++ b/demo/unload_event_test/run.py @@ -0,0 +1,32 @@ +"""This demo is only meant to test the unload event. +It will write to a file when the unload event is triggered. +May not work as expected if multiple people are using it. +""" +import gradio as gr +from pathlib import Path + +log_file = (Path(__file__).parent / "output_log.txt").resolve() + +def test_fn(x): + with open(log_file, "a") as f: + f.write(f"incremented {x}\n") + return x + 1, x + 1 + +def delete_fn(v): + with log_file.open("a") as f: + f.write(f"deleted {v}\n") + +def unload_fn(): + with log_file.open("a") as f: + f.write("unloading\n") + +with gr.Blocks() as demo: + n1 = gr.Number(value=0, label="Number") + state = gr.State(value=0, delete_callback=delete_fn) + button = gr.Button("Increment") + button.click(test_fn, [state], [n1, state], api_name="increment") + demo.unload(unload_fn) + demo.load(lambda: log_file.write_text("")) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/upload_and_download/run.py b/demo/upload_and_download/run.py new file mode 100644 index 0000000..48725bb --- /dev/null +++ b/demo/upload_and_download/run.py @@ -0,0 +1,21 @@ +from pathlib import Path +import gradio as gr + +def upload_file(filepath): + name = Path(filepath).name + return [gr.UploadButton(visible=False), gr.DownloadButton(label=f"Download {name}", value=filepath, visible=True)] + +def download_file(): + return [gr.UploadButton(visible=True), gr.DownloadButton(visible=False)] + +with gr.Blocks() as demo: + gr.Markdown("First upload a file and and then you'll be able download it (but only once!)") + with gr.Row(): + u = gr.UploadButton("Upload a file", file_count="single") + d = gr.DownloadButton("Download the file", visible=False) + + u.upload(upload_file, u, [u, d]) + d.click(download_file, None, [u, d]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/upload_button/DESCRIPTION.md b/demo/upload_button/DESCRIPTION.md new file mode 100644 index 0000000..a070ed1 --- /dev/null +++ b/demo/upload_button/DESCRIPTION.md @@ -0,0 +1 @@ +A simple demo showcasing the upload button used with its `upload` event trigger. \ No newline at end of file diff --git a/demo/upload_button/run.py b/demo/upload_button/run.py new file mode 100644 index 0000000..6e40d2a --- /dev/null +++ b/demo/upload_button/run.py @@ -0,0 +1,12 @@ +import gradio as gr + +def upload_file(files): + file_paths = [file.name for file in files] + return file_paths + +with gr.Blocks() as demo: + file_output = gr.File() + upload_button = gr.UploadButton("Click to Upload a File", file_types=["image", "video"], file_count="multiple") + upload_button.upload(upload_file, upload_button, file_output) + +demo.launch() diff --git a/demo/upload_button_component_events/run.py b/demo/upload_button_component_events/run.py new file mode 100644 index 0000000..8c87bc8 --- /dev/null +++ b/demo/upload_button_component_events/run.py @@ -0,0 +1,25 @@ +import gradio as gr + +with gr.Blocks() as demo: + + with gr.Row(): + with gr.Column(): + upload_btn = gr.UploadButton(label="Upload Single File", file_count="single") + with gr.Column(): + output_file_1 = gr.File(label="Upload Single File Output", file_count="single") + num_load_btn_1 = gr.Number(label="# Load Upload Single File", value=0) + output_click_1 = gr.Number(label="# Click Upload Single File Output", value=0) + upload_btn.upload(lambda s,n: (s, n + 1), [upload_btn, num_load_btn_1], [output_file_1, num_load_btn_1]) + upload_btn.click(lambda n: (n + 1), output_click_1, [output_click_1]) + with gr.Row(): + with gr.Column(): + upload_btn_multiple = gr.UploadButton(label="Upload Multiple Files", file_count="multiple") + with gr.Column(): + output_file_2 = gr.File(label="Upload Multiple Files Output", file_count="multiple") + num_load_btn_2 = gr.Number(label="# Load Upload Multiple Files", value=0) + output_click_2 = gr.Number(label="# Click Upload Multiple Files Output", value=0) + upload_btn_multiple.upload(lambda s,n: (s, n + 1), [upload_btn_multiple, num_load_btn_2], [output_file_2, num_load_btn_2]) + upload_btn_multiple.click(lambda n: (n + 1), output_click_2, [output_click_2]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/upload_file_limit_test/run.py b/demo/upload_file_limit_test/run.py new file mode 100644 index 0000000..9c88fa6 --- /dev/null +++ b/demo/upload_file_limit_test/run.py @@ -0,0 +1,25 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown(""" + # ⬆️📁 max_file_size test + The demo has a max file size of 15kb. The error modal should pop up when a file larger than that is uploaded. + """) + with gr.Row(): + with gr.Column(): + gr.Image(label="Image", interactive=True) + gr.Gallery(label="Gallery", interactive=True) + gr.File(label="Single File", interactive=True, file_count="single") + with gr.Column(): + gr.Model3D(label="Model 3D", interactive=True,) + gr.MultimodalTextbox(label="Multimodal Textbox", interactive=True) + gr.UploadButton(label="Upload Button", interactive=True) + with gr.Column(): + gr.Video(label="Video", interactive=True) + gr.Audio(label="Audio", interactive=True) + gr.File(label="Multiple Files", interactive=True, file_count="multiple") + +if __name__ == "__main__": + # The upload limit is set in playwright_setup.js + # since the e2e tests use mount_gradio_app + demo.launch(max_file_size="15kb") diff --git a/demo/uploadbutton_component/run.py b/demo/uploadbutton_component/run.py new file mode 100644 index 0000000..c668bc0 --- /dev/null +++ b/demo/uploadbutton_component/run.py @@ -0,0 +1,12 @@ +import gradio as gr + +def upload_file(files): + file_paths = [file.name for file in files] + return file_paths + +with gr.Blocks() as demo: + file_output = gr.File() + upload_button = gr.UploadButton("Click to Upload an Image or Video File", file_types=["image", "video"], file_count="multiple") + upload_button.upload(upload_file, upload_button, file_output) + +demo.launch() diff --git a/demo/validator_chatinterface/run.py b/demo/validator_chatinterface/run.py new file mode 100644 index 0000000..78c7bd8 --- /dev/null +++ b/demo/validator_chatinterface/run.py @@ -0,0 +1,20 @@ +import gradio as gr +import time + + +def validate_input(x): + print("VALIDATE", x) + return gr.validate(x != "error", "Can't be error") + + +def do_chat(message, history): + for i in range(len(message)): + time.sleep(0.05) + yield "You typed: " + message[: i + 1] + + +demo = gr.ChatInterface(fn=do_chat, validator=validate_input, show_progress="full") + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/validator_interface/run.py b/demo/validator_interface/run.py new file mode 100644 index 0000000..29f6e55 --- /dev/null +++ b/demo/validator_interface/run.py @@ -0,0 +1,90 @@ +import gradio as gr + + +def validate_input(age, location): + is_age_valid = True + is_location_valid = True + if not age or age < 3: + is_age_valid = False + if "london" in location.lower(): + is_location_valid = False + + return [ + gr.validate(is_age_valid, "Age must be at least 3"), + gr.validate(is_location_valid, "Location must not be in London"), + ] + + +def process_text(age, location): + result = f"Processed: {age} -- {location.upper()}" + return result + + +def validate_image(image): + # we don't want to error when a user is clearing the image + if not image: + return None + is_portrait = image.width < image.height + + return gr.validate(is_portrait, "Image must be in portrait mode") + + +def process_image(image): + if not image: + return "No image uploaded" + return "HELLO IMAGE!!!" + + +def raise_error(): + raise ValueError("test error") + + +with gr.Blocks() as demo: + with gr.Tab("Text"): + gr.Markdown("# Validator Parameter Test Demo") + + with gr.Row(): + with gr.Column(): + age = gr.Number( + label="Enter age", + placeholder="Enter age", + ) + location = gr.Textbox( + max_lines=3, + label="Enter location", + placeholder="Enter location", + ) + + validate_btn = gr.Button("Process with Validation", variant="primary") + + output_with_validation = gr.Textbox( + label="Output (with validation)", interactive=False + ) + + validate_btn.click( + fn=process_text, + validator=validate_input, + inputs=[age, location], + outputs=output_with_validation, + ) + with gr.Tab("Image"): + im = gr.Image(label="Enter image", placeholder="Enter image", type="pil") + t = gr.Textbox(label="Enter text", placeholder="Enter text") + im.change( + fn=process_image, + validator=validate_image, + inputs=im, + outputs=t, + ) + with gr.Tab("Validation Error"): + error_btn = gr.Button("Raise Validation Error", variant="primary") + error_btn.click( + validator=raise_error, + fn=raise_error, + inputs=[], + outputs=[], + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/validator_simple/run.py b/demo/validator_simple/run.py new file mode 100644 index 0000000..628b262 --- /dev/null +++ b/demo/validator_simple/run.py @@ -0,0 +1,45 @@ +import gradio as gr + + +def validate_input(age, location): + return [ + gr.validate(not age or age > 3, "Age must be at least 3"), + gr.validate("london" not in location.lower(), "Location must not be in London"), + ] + + +def process_text(age, location): + return f"Processed: {age} -- {location.upper()}" + + +with gr.Blocks() as demo: + gr.Markdown("# Validator Parameter Test Demo") + + with gr.Row(): + with gr.Column(): + age = gr.Number( + label="Enter age", + placeholder="Enter age", + ) + location = gr.Textbox( + max_lines=3, + label="Enter location", + placeholder="Enter location", + ) + + validate_btn = gr.Button("Process with Validation", variant="primary") + + output_with_validation = gr.Textbox( + label="Output (with validation)", interactive=False + ) + + validate_btn.click( + fn=process_text, + validator=validate_input, + inputs=[age, location], + outputs=output_with_validation, + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/validator_test/run.py b/demo/validator_test/run.py new file mode 100644 index 0000000..29f6e55 --- /dev/null +++ b/demo/validator_test/run.py @@ -0,0 +1,90 @@ +import gradio as gr + + +def validate_input(age, location): + is_age_valid = True + is_location_valid = True + if not age or age < 3: + is_age_valid = False + if "london" in location.lower(): + is_location_valid = False + + return [ + gr.validate(is_age_valid, "Age must be at least 3"), + gr.validate(is_location_valid, "Location must not be in London"), + ] + + +def process_text(age, location): + result = f"Processed: {age} -- {location.upper()}" + return result + + +def validate_image(image): + # we don't want to error when a user is clearing the image + if not image: + return None + is_portrait = image.width < image.height + + return gr.validate(is_portrait, "Image must be in portrait mode") + + +def process_image(image): + if not image: + return "No image uploaded" + return "HELLO IMAGE!!!" + + +def raise_error(): + raise ValueError("test error") + + +with gr.Blocks() as demo: + with gr.Tab("Text"): + gr.Markdown("# Validator Parameter Test Demo") + + with gr.Row(): + with gr.Column(): + age = gr.Number( + label="Enter age", + placeholder="Enter age", + ) + location = gr.Textbox( + max_lines=3, + label="Enter location", + placeholder="Enter location", + ) + + validate_btn = gr.Button("Process with Validation", variant="primary") + + output_with_validation = gr.Textbox( + label="Output (with validation)", interactive=False + ) + + validate_btn.click( + fn=process_text, + validator=validate_input, + inputs=[age, location], + outputs=output_with_validation, + ) + with gr.Tab("Image"): + im = gr.Image(label="Enter image", placeholder="Enter image", type="pil") + t = gr.Textbox(label="Enter text", placeholder="Enter text") + im.change( + fn=process_image, + validator=validate_image, + inputs=im, + outputs=t, + ) + with gr.Tab("Validation Error"): + error_btn = gr.Button("Raise Validation Error", variant="primary") + error_btn.click( + validator=raise_error, + fn=raise_error, + inputs=[], + outputs=[], + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/variable_outputs/run.py b/demo/variable_outputs/run.py new file mode 100644 index 0000000..c893faf --- /dev/null +++ b/demo/variable_outputs/run.py @@ -0,0 +1,19 @@ +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() diff --git a/demo/video_component/run.py b/demo/video_component/run.py new file mode 100644 index 0000000..984f671 --- /dev/null +++ b/demo/video_component/run.py @@ -0,0 +1,19 @@ +import gradio as gr +# get_video() returns the file path to sample videos included with Gradio +from gradio.media import get_video, MEDIA_PATHS + +demo = gr.Interface( + fn=lambda x: x, + inputs=gr.Video(), + outputs=gr.Video(), + examples=[ + [get_video("world.mp4")], + [get_video("a.mp4")], + [get_video("b.mp4")], + ], + api_name="predict", + cache_examples=True +) + +if __name__ == "__main__": + demo.launch(allowed_paths=[str(p) for p in MEDIA_PATHS]) diff --git a/demo/video_component_events/run.py b/demo/video_component_events/run.py new file mode 100644 index 0000000..24f8bfb --- /dev/null +++ b/demo/video_component_events/run.py @@ -0,0 +1,22 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + input_video = gr.Video(label="Input Video") + with gr.Column(): + output_video = gr.Video(label="Output Video") + with gr.Column(): + num_change = gr.Number(label="# Change Events", value=0) + num_input = gr.Number(label="# Input Events", value=0) + num_load = gr.Number(label="# Upload Events", value=0) + num_play = gr.Number(label="# Play Events", value=0) + num_pause = gr.Number(label="# Pause Events", value=0) + input_video.upload(lambda s, n: (s, n + 1), [input_video, num_load], [output_video, num_load]) + input_video.input(lambda n: n + 1, num_input, num_input) + input_video.play(lambda n: n + 1, num_play, num_play) + input_video.pause(lambda n: n + 1, num_pause, num_pause) + input_video.change(lambda n: n + 1, num_change, num_change) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/video_identity/run.py b/demo/video_identity/run.py new file mode 100644 index 0000000..247dd88 --- /dev/null +++ b/demo/video_identity/run.py @@ -0,0 +1,18 @@ +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",) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/video_identity/screenshot.png b/demo/video_identity/screenshot.png new file mode 100644 index 0000000..443948a Binary files /dev/null and b/demo/video_identity/screenshot.png differ diff --git a/demo/video_identity_2/run.py b/demo/video_identity_2/run.py new file mode 100644 index 0000000..37c3c03 --- /dev/null +++ b/demo/video_identity_2/run.py @@ -0,0 +1,13 @@ +import gradio as gr + +def video_identity(video): + return video + +demo = gr.Interface(video_identity, + gr.Video(), + "playable_video", + api_name="predict" + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/video_subtitle/run.py b/demo/video_subtitle/run.py new file mode 100644 index 0000000..ae2c4d2 --- /dev/null +++ b/demo/video_subtitle/run.py @@ -0,0 +1,25 @@ +import gradio as gr +from gradio.media import get_video, get_file + +def video_demo(video, subtitle=None): + if subtitle is None: + return video + return gr.Video(label="Out", value=video, subtitles=subtitle.name) + +demo = gr.Interface( + fn=video_demo, + inputs=[ + gr.Video(label="In", interactive=True), + gr.File(label="Subtitle", file_types=[".srt", ".vtt", ".json"]), + ], + outputs=gr.Video(label="Out"), + examples=[ + [get_video("a.mp4"), get_file("s1.srt")], + [get_video("b.mp4"), get_file("s2.vtt")], + [get_video("a.mp4"), get_file("s3.json")], + ], + api_name="predict", +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/video_watermark/files/w2.png b/demo/video_watermark/files/w2.png new file mode 100644 index 0000000..855b404 Binary files /dev/null and b/demo/video_watermark/files/w2.png differ diff --git a/demo/video_watermark/run.py b/demo/video_watermark/run.py new file mode 100644 index 0000000..2509d59 --- /dev/null +++ b/demo/video_watermark/run.py @@ -0,0 +1,17 @@ +import gradio as gr + + +a = gr.get_video("a.mp4") +b = gr.get_video("b.mp4") +w1 = gr.get_image("logo.png") +w2 = gr.get_image("bus.png") + +def generate_video(original_video, watermark, position): + return gr.Video(original_video, watermark=gr.WatermarkOptions(watermark=watermark, position=position)) + + +demo = gr.Interface(generate_video, [gr.Video(), gr.File(), gr.Dropdown(["top-left", "top-right", "bottom-left", "bottom-right"], label="Position")], gr.Video(), + examples=[[a, w1, "top-left"], [b, w2, "bottom-right"]], api_name="predict") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/visibility_test/run.py b/demo/visibility_test/run.py new file mode 100644 index 0000000..db25f89 --- /dev/null +++ b/demo/visibility_test/run.py @@ -0,0 +1,111 @@ +import gradio as gr + + +def toggle_visibility(choice, input_text): + """Toggle visibility based on choice and return current input value.""" + updates = {} + + if choice == "Visible": + updates["textbox"] = gr.update(visible=True) + updates["button"] = gr.update(visible=True) + elif choice == "Hidden (in DOM)": + updates["textbox"] = gr.update(visible="hidden") + updates["button"] = gr.update(visible="hidden") + else: # "Not Visible (removed)" + updates["textbox"] = gr.update(visible=False) + updates["button"] = gr.update(visible=False) + + return updates["textbox"], updates["button"], f"Current value: {input_text}" + + +def get_value(input_text): + """Get the current value from the textbox.""" + return f"Retrieved value: {input_text}" + + +def increment_counter(counter): + """Increment counter to test event handling.""" + return counter + 1 + + +def reveal_hidden_column(): + """Generator that toggles two columns over multiple yields (issue #13494): + the column that starts hidden must end up visible.""" + yield gr.update(visible=False), gr.update(visible=False) + yield gr.update(visible=False), gr.update(visible=True) + + +def hide_revealed_column(): + yield gr.update(visible=True), gr.update(visible=True) + yield gr.update(visible=True), gr.update(visible=False) + + +with gr.Blocks() as demo: + gr.Markdown("# Visibility Test Demo") + gr.Markdown( + "Test the three visibility states: visible=True, visible='hidden', visible=False" + ) + + with gr.Row(): + visibility_radio = gr.Radio( + ["Visible", "Hidden (in DOM)", "Not Visible (removed)"], + label="Choose visibility state", + value="Visible", + elem_id="visibility-radio", + ) + + with gr.Row(): + with gr.Column(): + textbox = gr.Textbox( + label="Test Input", + value="Initial text", + elem_id="test-textbox", + visible=True, + ) + button = gr.Button("Get Value", elem_id="test-button", visible=True) + + # Hidden counter for testing events on hidden elements + counter = gr.Number(value=0, visible="hidden", elem_id="counter") + + increment_btn = gr.Button( + "Increment Counter", + elem_id="increment-button", + ) + counter_result = gr.Textbox( + label="Counter Result", elem_id="counter-result" + ) + + with gr.Column(): + status = gr.Textbox(label="Status", elem_id="status-output") + output = gr.Textbox(label="Output", elem_id="output-textbox") + + # Wire up the events + visibility_radio.change( + toggle_visibility, + inputs=[visibility_radio, textbox], + outputs=[textbox, button, status], + ) + + button.click(get_value, inputs=textbox, outputs=output) + counter.change( + lambda x: f"Counter Result: {x}", inputs=counter, outputs=counter_result + ) + increment_btn.click(increment_counter, inputs=counter, outputs=counter) + + # Regression coverage for #13494: a multi-yield generator that updates two + # columns at once (one hidden, one revealed) must leave the revealed column + # visible. The reveal/hide buttons sit outside both columns so they persist. + with gr.Row(): + reveal_btn = gr.Button("Reveal Hidden Column", elem_id="yield-reveal") + hide_btn = gr.Button("Hide Revealed Column", elem_id="yield-hide") + with gr.Column(elem_id="yield-col-x") as yield_col_x: + gr.Markdown("Column X") + with gr.Column(visible=False, elem_id="yield-col-y") as yield_col_y: + gr.Textbox(label="Revealed", elem_id="yield-target") + + reveal_btn.click(reveal_hidden_column, outputs=[yield_col_x, yield_col_y]) + hide_btn.click(hide_revealed_column, outputs=[yield_col_x, yield_col_y]) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/walkthrough/run.py b/demo/walkthrough/run.py new file mode 100644 index 0000000..445a337 --- /dev/null +++ b/demo/walkthrough/run.py @@ -0,0 +1,18 @@ +import gradio as gr + +with gr.Blocks() as demo: + with gr.Walkthrough(selected=0) as walkthrough: + with gr.Step("Image", id=0): + image = gr.Image() + btn = gr.Button("go to prompt") + btn.click(lambda: gr.Walkthrough(selected=1), outputs=walkthrough) + with gr.Step("Prompt", id=1): + prompt = gr.Textbox() + btn = gr.Button("generate") + btn.click(lambda: gr.Walkthrough(selected=2), outputs=walkthrough) + with gr.Step("Result", id=2): + gr.Image(label="result", interactive=False) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/walkthrough_many/run.py b/demo/walkthrough_many/run.py new file mode 100644 index 0000000..f4cb662 --- /dev/null +++ b/demo/walkthrough_many/run.py @@ -0,0 +1,60 @@ +import gradio as gr + + +def process_step(step_num): + return f"You are on step {step_num}" + + +with gr.Blocks() as demo: + gr.Markdown("# Stepper - Many Steps") + + with gr.Walkthrough(selected=0) as walkthrough: + with gr.Step("Introduction", id=0): + gr.Markdown("This is the introduction step.") + output1 = gr.Textbox(label="Step 1 Output") + next1 = gr.Button("Next Step") + next1.click(lambda: gr.Walkthrough(selected=1), outputs=walkthrough) + + with gr.Step("Basic Information", id=1): + gr.Markdown("Enter your basic information.") + output2 = gr.Textbox(label="Step 2 Output") + next2 = gr.Button("Next Step") + next2.click(lambda: gr.Walkthrough(selected=2), outputs=walkthrough) + + with gr.Step("Preferences", id=2): + gr.Markdown("Set your preferences.") + output3 = gr.Textbox(label="Step 3 Output") + next3 = gr.Button("Next Step") + next3.click(lambda: gr.Walkthrough(selected=3), outputs=walkthrough) + + with gr.Step("Advanced Settings", id=3): + gr.Markdown("Configure advanced settings.") + output4 = gr.Textbox(label="Step 4 Output") + next4 = gr.Button("Next Step") + next4.click(lambda: gr.Walkthrough(selected=4), outputs=walkthrough) + + with gr.Step("Review", id=4): + gr.Markdown("Review your choices.") + output5 = gr.Textbox(label="Step 5 Output") + next5 = gr.Button("Next Step") + next5.click(lambda: gr.Walkthrough(selected=5), outputs=walkthrough) + + with gr.Step("Confirmation", id=5): + gr.Markdown("Confirm and submit.") + output6 = gr.Textbox(label="Step 6 Output") + next6 = gr.Button("Next Step") + next6.click(lambda: gr.Walkthrough(selected=6), outputs=walkthrough) + + with gr.Step("Additional Options", id=6): + gr.Markdown("Additional options if needed.") + output7 = gr.Textbox(label="Step 7 Output") + next7 = gr.Button("Next Step") + next7.click(lambda: gr.Walkthrough(selected=7), outputs=walkthrough) + + with gr.Step("Final Step", id=7): + gr.Markdown("This is the final step!") + output8 = gr.Textbox(label="Step 8 Output") + gr.Button("Complete") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/web_component_embed/run.py b/demo/web_component_embed/run.py new file mode 100644 index 0000000..ca30a40 --- /dev/null +++ b/demo/web_component_embed/run.py @@ -0,0 +1,11 @@ +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown("# Web component embed target") + name = gr.Textbox(label="Name") + greet = gr.Button("Greet") + out = gr.Textbox(label="Greeting") + greet.click(lambda n: f"Hello, {n or 'world'}!", name, out) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/webcam/run.py b/demo/webcam/run.py new file mode 100644 index 0000000..5ff45f5 --- /dev/null +++ b/demo/webcam/run.py @@ -0,0 +1,15 @@ + +import gradio as gr + +def snap(image, video): + return [image, video] + +demo = gr.Interface( + snap, + [gr.Image(sources=["webcam"]), gr.Video(sources=["webcam"])], + ["image", "video"], + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/webcam/screenshot.png b/demo/webcam/screenshot.png new file mode 100644 index 0000000..33d74a9 Binary files /dev/null and b/demo/webcam/screenshot.png differ diff --git a/demo/webcam_constraints/requirements.txt b/demo/webcam_constraints/requirements.txt new file mode 100644 index 0000000..1db7aea --- /dev/null +++ b/demo/webcam_constraints/requirements.txt @@ -0,0 +1 @@ +opencv-python \ No newline at end of file diff --git a/demo/webcam_constraints/run.py b/demo/webcam_constraints/run.py new file mode 100644 index 0000000..f0989da --- /dev/null +++ b/demo/webcam_constraints/run.py @@ -0,0 +1,43 @@ +import gradio as gr +import cv2 # type: ignore + +def get_video_shape(video): + cap = cv2.VideoCapture(video) + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + cap.release() + return {"width": width, "height": height} + +def image_mod(image): + width, height = image.size + return {"width": width, "height": height} + + +video = gr.Interface( + fn=get_video_shape, + inputs=gr.Video(webcam_options=gr.WebcamOptions(constraints={"video": {"width": 800, "height": 600}}), sources="webcam"), + outputs=gr.JSON(), + api_name="predict" +) + +image = gr.Interface( + image_mod, + gr.Image(type="pil", webcam_options=gr.WebcamOptions(constraints={"video": {"width": 800, "height": 600}}), sources="webcam"), + gr.Json(), + api_name="predict") + +with gr.Blocks() as demo: + gr.Markdown("""# Webcam Constraints + The webcam constraints are set to 800x600 with the following syntax: + ```python + gr.Video(webcam_options=gr.WebcamOptions(constraints={"video": {"width": 800, "height": 600}}), sources="webcam") + ``` + """) + with gr.Tabs(): + with gr.Tab("Video"): + video.render() + with gr.Tab("Image"): + image.render() + +if __name__ == "__main__": + demo.launch() \ No newline at end of file diff --git a/demo/white_noise_vid_not_playable/requirements.txt b/demo/white_noise_vid_not_playable/requirements.txt new file mode 100644 index 0000000..1db7aea --- /dev/null +++ b/demo/white_noise_vid_not_playable/requirements.txt @@ -0,0 +1 @@ +opencv-python \ No newline at end of file diff --git a/demo/white_noise_vid_not_playable/run.py b/demo/white_noise_vid_not_playable/run.py new file mode 100644 index 0000000..7e7484a --- /dev/null +++ b/demo/white_noise_vid_not_playable/run.py @@ -0,0 +1,20 @@ +import cv2 # type: ignore +import gradio as gr +import numpy as np + +def gif_maker(): + img_array = [] + height, width = 50, 50 + for _ in range(30): + img_array.append(np.random.randint(0, 255, size=(height, width, 3)).astype(np.uint8)) + output_file = "test.mp4" + out = cv2.VideoWriter(output_file, cv2.VideoWriter_fourcc(*'mp4v'), 15, (height, width)) # type: ignore + for i in range(len(img_array)): + out.write(img_array[i]) + out.release() + return output_file, output_file + +demo = gr.Interface(gif_maker, inputs=None, outputs=[gr.Video(buttons=["download"]), gr.File()], api_name="predict") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/workflow/run.py b/demo/workflow/run.py new file mode 100644 index 0000000..96b2ae8 --- /dev/null +++ b/demo/workflow/run.py @@ -0,0 +1,55 @@ +import os +import random + +import gradio as gr + + +def describe_product(image) -> str: + if image is None: + return "a premium product" + img_path = image.get("path") if isinstance(image, dict) else str(image) + img_url = image.get("url", "") if isinstance(image, dict) else str(image) + try: + from gradio_client import Client, handle_file + source = img_path if (img_path and os.path.exists(img_path)) else img_url + if not source: + return "a premium product" + client = Client("vikhyatk/moondream2", verbose=False) + result = client.predict( + handle_file(source), + "Describe this product briefly and concisely. What is it?", + api_name="/answer_question", + ) + return str(result).strip() if result else "a premium product" + except Exception: + return "a premium product" + + +def craft_marketing_prompt(caption: str) -> str: + if not caption: + caption = "a premium product" + caption = caption.strip().rstrip(".") + styles = [ + ( + f"Professional product advertisement photograph of {caption}, " + "studio lighting, clean white background, commercial photography, " + "ultra-sharp, 8K quality" + ), + ( + f"Cinematic product shot of {caption}, dramatic lighting, " + "aspirational lifestyle context, premium brand aesthetic, " + "shot on Hasselblad, magazine cover quality" + ), + ( + f"Bold marketing campaign visual of {caption}, vibrant colors, " + "dynamic composition, modern editorial style, " + "award-winning commercial photography" + ), + ] + return random.choice(styles) + + +demo = gr.Workflow(bind=[describe_product, craft_marketing_prompt]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/workflow/workflow.json b/demo/workflow/workflow.json new file mode 100644 index 0000000..d989787 --- /dev/null +++ b/demo/workflow/workflow.json @@ -0,0 +1 @@ +{"schema_version":"2","name":"Marketing Image Creator","runtime":{"default":"client"},"references":[{"label":"Text","inputs":[{"id":"in","label":"Text","type":"text"}],"outputs":[{"id":"out","label":"Text","type":"text"}],"width":220,"height":163,"asset_type":"text","role":"reference","id":"1539a1e9-4906-4008-b345-e6d05bb69b22","x":740,"y":80,"data":{"in":null}},{"label":"Image","inputs":[{"id":"in","label":"Image","type":"image"}],"outputs":[{"id":"out","label":"Image","type":"image"}],"width":220,"height":124,"asset_type":"image","role":"reference","id":"13e92b22-7173-416c-81c6-1ca097a2a299","x":80,"y":80,"data":{}}],"operators":[{"id":"n_flux","label":"Generate Image","inputs":[{"id":"in","label":"Prompt","type":"text","required":true}],"outputs":[{"id":"out","label":"Image","type":"image"}],"data":{"out":97500097},"x":1040,"y":80,"width":220,"height":124,"role":"operator","kind":"space","source":"hf://spaces/multimodalart/FLUX.1-merged","space_id":"multimodalart/FLUX.1-merged","runtime":"client","endpoints":[{"name":"/infer","inputs":[{"id":"in_0","label":"Prompt","type":"text","required":true},{"id":"in_1","label":"Seed","type":"number","required":false},{"id":"in_2","label":"Randomize seed","type":"boolean","required":false},{"id":"in_3","label":"Width","type":"number","required":false},{"id":"in_4","label":"Height","type":"number","required":false},{"id":"in_5","label":"Guidance Scale","type":"number","required":false},{"id":"in_6","label":"Number of inference steps","type":"number","required":false}],"outputs":[{"id":"out_0","label":"Result","type":"image","output_index":0},{"id":"out_1","label":"Seed","type":"number","output_index":1}]}]},{"label":"Image To Prompt","inputs":[{"id":"in_0","label":"Input Image","type":"image","required":true}],"outputs":[{"id":"out_0","label":"Output Prompt","type":"text","output_index":0}],"width":280,"height":124,"kind":"space","space_id":"ovi054/image-to-prompt","endpoint":"/predict","endpoints":[{"name":"/predict","inputs":[{"id":"in_0","label":"Input Image","type":"image","required":true}],"outputs":[{"id":"out_0","label":"Output Prompt","type":"text","output_index":0}]}],"pipeline_tag":"Image Captioning","role":"operator","id":"897592dc-17b5-473b-9e89-4ebdae4073d6","x":380,"y":80,"data":{}}],"subjects":[{"id":"n_output","label":"Marketing Image","inputs":[{"id":"in","label":"Image","type":"image"}],"outputs":[{"id":"out","label":"Image","type":"image"}],"data":{"in":null},"x":1340,"y":80,"width":220,"height":107,"role":"subject","asset_type":"image"}],"edges":[{"id":"e4","from_node_id":"n_flux","from_port_id":"out","to_node_id":"n_output","to_port_id":"in","type":"image"},{"from_node_id":"1539a1e9-4906-4008-b345-e6d05bb69b22","from_port_id":"out","to_node_id":"n_flux","to_port_id":"in","type":"text","id":"b44e83fe-23b1-4dcc-83d3-55b3481721bc"},{"from_node_id":"13e92b22-7173-416c-81c6-1ca097a2a299","from_port_id":"out","to_node_id":"897592dc-17b5-473b-9e89-4ebdae4073d6","to_port_id":"in_0","type":"image","id":"22796709-ff7d-4250-92c0-2d541f9e6a99"},{"from_node_id":"897592dc-17b5-473b-9e89-4ebdae4073d6","from_port_id":"out_0","to_node_id":"1539a1e9-4906-4008-b345-e6d05bb69b22","to_port_id":"in","type":"text","id":"96dab823-0f2c-4356-98c4-aa82feca5e0a"}],"view":{"default":"canvas"}} \ No newline at end of file diff --git a/demo/workflow_api/run.py b/demo/workflow_api/run.py new file mode 100644 index 0000000..e588613 --- /dev/null +++ b/demo/workflow_api/run.py @@ -0,0 +1,19 @@ +import gradio as gr + + +def shout(text: str) -> str: + return (text or "").upper() + + +def reverse(text: str) -> str: + return (text or "")[::-1] + + +# Two outputs ("Loud" and "Reversed") fed by a shared "Text" input. Because they +# form a single connected subgraph, they're exposed as ONE API endpoint that +# returns both outputs as a tuple — see the "View API" panel in the app, or +# connect with the gradio_client. +demo = gr.Workflow(graph="workflow.json", bind={"shout": shout, "reverse": reverse}) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/workflow_api/workflow.json b/demo/workflow_api/workflow.json new file mode 100644 index 0000000..cc95a67 --- /dev/null +++ b/demo/workflow_api/workflow.json @@ -0,0 +1,48 @@ +{ + "schema_version": "2", + "name": "Text Tools", + "runtime": { "default": "client" }, + "references": [ + { + "id": "ref_text", "label": "Text", "role": "reference", "asset_type": "text", + "inputs": [{ "id": "in", "label": "Text", "type": "text" }], + "outputs": [{ "id": "out", "label": "Text", "type": "text" }], + "x": 80, "y": 120, "width": 220, "height": 120, "data": {} + } + ], + "operators": [ + { + "id": "op_loud", "label": "shout", "role": "operator", "kind": "fn", "fn": "shout", + "inputs": [{ "id": "in_text", "label": "text", "type": "text", "required": true }], + "outputs": [{ "id": "out_0", "label": "output", "type": "text", "output_index": 0 }], + "x": 380, "y": 40, "width": 220, "height": 120, "data": {} + }, + { + "id": "op_rev", "label": "reverse", "role": "operator", "kind": "fn", "fn": "reverse", + "inputs": [{ "id": "in_text", "label": "text", "type": "text", "required": true }], + "outputs": [{ "id": "out_0", "label": "output", "type": "text", "output_index": 0 }], + "x": 380, "y": 240, "width": 220, "height": 120, "data": {} + } + ], + "subjects": [ + { + "id": "sub_loud", "label": "Loud", "role": "subject", "asset_type": "text", + "inputs": [{ "id": "in", "label": "Text", "type": "text" }], + "outputs": [{ "id": "out", "label": "Text", "type": "text" }], + "x": 700, "y": 40, "width": 220, "height": 107, "data": {} + }, + { + "id": "sub_rev", "label": "Reversed", "role": "subject", "asset_type": "text", + "inputs": [{ "id": "in", "label": "Text", "type": "text" }], + "outputs": [{ "id": "out", "label": "Text", "type": "text" }], + "x": 700, "y": 240, "width": 220, "height": 107, "data": {} + } + ], + "edges": [ + { "id": "e1", "from_node_id": "ref_text", "from_port_id": "out", "to_node_id": "op_loud", "to_port_id": "in_text", "type": "text" }, + { "id": "e2", "from_node_id": "ref_text", "from_port_id": "out", "to_node_id": "op_rev", "to_port_id": "in_text", "type": "text" }, + { "id": "e3", "from_node_id": "op_loud", "from_port_id": "out_0", "to_node_id": "sub_loud", "to_port_id": "in", "type": "text" }, + { "id": "e4", "from_node_id": "op_rev", "from_port_id": "out_0", "to_node_id": "sub_rev", "to_port_id": "in", "type": "text" } + ], + "view": { "default": "canvas" } +} diff --git a/demo/xgboost-income-prediction-with-explainability/DESCRIPTION.md b/demo/xgboost-income-prediction-with-explainability/DESCRIPTION.md new file mode 100644 index 0000000..cd69ef8 --- /dev/null +++ b/demo/xgboost-income-prediction-with-explainability/DESCRIPTION.md @@ -0,0 +1 @@ +This demo takes in 12 inputs from the user in dropdowns and sliders and predicts income. It also has a separate button for explaining the prediction. \ No newline at end of file diff --git a/demo/xgboost-income-prediction-with-explainability/requirements.txt b/demo/xgboost-income-prediction-with-explainability/requirements.txt new file mode 100644 index 0000000..049ecfd --- /dev/null +++ b/demo/xgboost-income-prediction-with-explainability/requirements.txt @@ -0,0 +1,6 @@ +numpy==1.23.2 +matplotlib +shap +xgboost==1.7.6 +pandas +datasets \ No newline at end of file diff --git a/demo/xgboost-income-prediction-with-explainability/run.py b/demo/xgboost-income-prediction-with-explainability/run.py new file mode 100644 index 0000000..658b05b --- /dev/null +++ b/demo/xgboost-income-prediction-with-explainability/run.py @@ -0,0 +1,161 @@ +# type: ignore +import gradio as gr +import random +import matplotlib.pyplot as plt +import pandas as pd +import shap +import xgboost as xgb +from datasets import load_dataset + +dataset = load_dataset("scikit-learn/adult-census-income") +X_train = dataset["train"].to_pandas() +_ = X_train.pop("fnlwgt") +_ = X_train.pop("race") +y_train = X_train.pop("income") +y_train = (y_train == ">50K").astype(int) +categorical_columns = [ + "workclass", + "education", + "marital.status", + "occupation", + "relationship", + "sex", + "native.country", +] +X_train = X_train.astype({col: "category" for col in categorical_columns}) +data = xgb.DMatrix(X_train, label=y_train, enable_categorical=True) +model = xgb.train(params={"objective": "binary:logistic"}, dtrain=data) +explainer = shap.TreeExplainer(model) + +def predict(*args): + df = pd.DataFrame([args], columns=X_train.columns) + df = df.astype({col: "category" for col in categorical_columns}) + pos_pred = model.predict(xgb.DMatrix(df, enable_categorical=True)) + return {">50K": float(pos_pred[0]), "<=50K": 1 - float(pos_pred[0])} + +def interpret(*args): + df = pd.DataFrame([args], columns=X_train.columns) + df = df.astype({col: "category" for col in categorical_columns}) + shap_values = explainer.shap_values(xgb.DMatrix(df, enable_categorical=True)) + scores_desc = list(zip(shap_values[0], X_train.columns)) + scores_desc = sorted(scores_desc) + fig_m = plt.figure(tight_layout=True) + plt.barh([s[1] for s in scores_desc], [s[0] for s in scores_desc]) + plt.title("Feature Shap Values") + plt.ylabel("Shap Value") + plt.xlabel("Feature") + plt.tight_layout() + return fig_m + +unique_class = sorted(X_train["workclass"].unique()) +unique_education = sorted(X_train["education"].unique()) +unique_marital_status = sorted(X_train["marital.status"].unique()) +unique_relationship = sorted(X_train["relationship"].unique()) +unique_occupation = sorted(X_train["occupation"].unique()) +unique_sex = sorted(X_train["sex"].unique()) +unique_country = sorted(X_train["native.country"].unique()) + +with gr.Blocks() as demo: + gr.Markdown(""" + **Income Classification with XGBoost 💰**: This demo uses an XGBoost classifier predicts income based on demographic factors, along with Shapley value-based *explanations*. The [source code for this Gradio demo is here](https://huggingface.co/spaces/gradio/xgboost-income-prediction-with-explainability/blob/main/app.py). + """) + with gr.Row(): + with gr.Column(): + age = gr.Slider(label="Age", minimum=17, maximum=90, step=1, randomize=True) + work_class = gr.Dropdown( + label="Workclass", + choices=unique_class, + value=lambda: random.choice(unique_class), + ) + education = gr.Dropdown( + label="Education Level", + choices=unique_education, + value=lambda: random.choice(unique_education), + ) + years = gr.Slider( + label="Years of schooling", + minimum=1, + maximum=16, + step=1, + randomize=True, + ) + marital_status = gr.Dropdown( + label="Marital Status", + choices=unique_marital_status, + value=lambda: random.choice(unique_marital_status), + ) + occupation = gr.Dropdown( + label="Occupation", + choices=unique_occupation, + value=lambda: random.choice(unique_occupation), + ) + relationship = gr.Dropdown( + label="Relationship Status", + choices=unique_relationship, + value=lambda: random.choice(unique_relationship), + ) + sex = gr.Dropdown( + label="Sex", choices=unique_sex, value=lambda: random.choice(unique_sex) + ) + capital_gain = gr.Slider( + label="Capital Gain", + minimum=0, + maximum=100000, + step=500, + randomize=True, + ) + capital_loss = gr.Slider( + label="Capital Loss", minimum=0, maximum=10000, step=500, randomize=True + ) + hours_per_week = gr.Slider( + label="Hours Per Week Worked", minimum=1, maximum=99, step=1 + ) + country = gr.Dropdown( + label="Native Country", + choices=unique_country, + value=lambda: random.choice(unique_country), + ) + with gr.Column(): + label = gr.Label() + plot = gr.Plot() + with gr.Row(): + predict_btn = gr.Button(value="Predict") + interpret_btn = gr.Button(value="Explain") + predict_btn.click( + predict, + inputs=[ + age, + work_class, + education, + years, + marital_status, + occupation, + relationship, + sex, + capital_gain, + capital_loss, + hours_per_week, + country, + ], + outputs=[label], + ) + interpret_btn.click( + interpret, + inputs=[ + age, + work_class, + education, + years, + marital_status, + occupation, + relationship, + sex, + capital_gain, + capital_loss, + hours_per_week, + country, + ], + outputs=[plot], + ) + +demo.launch() diff --git a/demo/yolov10_webcam_stream/inference.py b/demo/yolov10_webcam_stream/inference.py new file mode 100644 index 0000000..70fbcfb --- /dev/null +++ b/demo/yolov10_webcam_stream/inference.py @@ -0,0 +1,148 @@ +import time +import cv2 # type: ignore +import numpy as np +import onnxruntime # type: ignore + +from utils import draw_detections # type: ignore + + +class YOLOv10: + def __init__(self, path): + # Initialize model + self.initialize_model(path) + + def __call__(self, image): + return self.detect_objects(image) + + def initialize_model(self, path): + self.session = onnxruntime.InferenceSession( + path, providers=onnxruntime.get_available_providers() + ) + # Get model info + self.get_input_details() + self.get_output_details() + + def detect_objects(self, image, conf_threshold=0.3): + input_tensor = self.prepare_input(image) + + # Perform inference on the image + new_image = self.inference(image, input_tensor, conf_threshold) + + return new_image + + def prepare_input(self, image): + self.img_height, self.img_width = image.shape[:2] + + input_img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + + # Resize input image + input_img = cv2.resize(input_img, (self.input_width, self.input_height)) + + # Scale input pixel values to 0 to 1 + input_img = input_img / 255.0 + input_img = input_img.transpose(2, 0, 1) + input_tensor = input_img[np.newaxis, :, :, :].astype(np.float32) + + return input_tensor + + def inference(self, image, input_tensor, conf_threshold=0.3): + start = time.perf_counter() + outputs = self.session.run( + self.output_names, {self.input_names[0]: input_tensor} + ) + + print(f"Inference time: {(time.perf_counter() - start)*1000:.2f} ms") + ( + boxes, + scores, + class_ids, + ) = self.process_output(outputs, conf_threshold) + return self.draw_detections(image, boxes, scores, class_ids) + + def process_output(self, output, conf_threshold=0.3): + predictions = np.squeeze(output[0]) + + # Filter out object confidence scores below threshold + scores = predictions[:, 4] + predictions = predictions[scores > conf_threshold, :] + scores = scores[scores > conf_threshold] + + if len(scores) == 0: + return [], [], [] + + # Get the class with the highest confidence + class_ids = predictions[:, 5].astype(int) + + # Get bounding boxes for each object + boxes = self.extract_boxes(predictions) + + return boxes, scores, class_ids + + def extract_boxes(self, predictions): + # Extract boxes from predictions + boxes = predictions[:, :4] + + # Scale boxes to original image dimensions + boxes = self.rescale_boxes(boxes) + + # Convert boxes to xyxy format + # boxes = xywh2xyxy(boxes) + + return boxes + + def rescale_boxes(self, boxes): + # Rescale boxes to original image dimensions + input_shape = np.array( + [self.input_width, self.input_height, self.input_width, self.input_height] + ) + boxes = np.divide(boxes, input_shape, dtype=np.float32) + boxes *= np.array( + [self.img_width, self.img_height, self.img_width, self.img_height] + ) + return boxes + + def draw_detections( + self, image, boxes, scores, class_ids, draw_scores=True, mask_alpha=0.4 + ): + return draw_detections(image, boxes, scores, class_ids, mask_alpha) + + def get_input_details(self): + model_inputs = self.session.get_inputs() + self.input_names = [model_inputs[i].name for i in range(len(model_inputs))] + + self.input_shape = model_inputs[0].shape + self.input_height = self.input_shape[2] + self.input_width = self.input_shape[3] + + def get_output_details(self): + model_outputs = self.session.get_outputs() + self.output_names = [model_outputs[i].name for i in range(len(model_outputs))] + + +if __name__ == "__main__": + import requests + import tempfile + from huggingface_hub import hf_hub_download + + model_file = hf_hub_download( + repo_id="onnx-community/yolov10s", filename="onnx/model.onnx" + ) + + yolov8_detector = YOLOv10(model_file) + + with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f: + f.write( + requests.get( + "https://live.staticflickr.com/13/19041780_d6fd803de0_3k.jpg" + ).content + ) + f.seek(0) + img = cv2.imread(f.name) + + # # Detect Objects + combined_image = yolov8_detector.detect_objects(img) + + # Draw detections + cv2.namedWindow("Output", cv2.WINDOW_NORMAL) + cv2.imshow("Output", combined_image) + cv2.waitKey(0) diff --git a/demo/yolov10_webcam_stream/requirements.txt b/demo/yolov10_webcam_stream/requirements.txt new file mode 100644 index 0000000..9766bb4 --- /dev/null +++ b/demo/yolov10_webcam_stream/requirements.txt @@ -0,0 +1,6 @@ +safetensors==0.4.3 +opencv-python +twilio +gradio>=5.0,<6.0 +gradio-webrtc==0.0.1 +onnxruntime-gpu \ No newline at end of file diff --git a/demo/yolov10_webcam_stream/run.py b/demo/yolov10_webcam_stream/run.py new file mode 100644 index 0000000..515986a --- /dev/null +++ b/demo/yolov10_webcam_stream/run.py @@ -0,0 +1,72 @@ +import gradio as gr +import cv2 # type: ignore +from huggingface_hub import hf_hub_download +from gradio_webrtc import WebRTC # type: ignore +from twilio.rest import Client # type: ignore +import os +from inference import YOLOv10 # type: ignore + +model_file = hf_hub_download( + repo_id="onnx-community/yolov10n", filename="onnx/model.onnx" +) + +model = YOLOv10(model_file) + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +if account_sid and auth_token: + client = Client(account_sid, auth_token) + + token = client.tokens.create() + + rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", + } +else: + rtc_configuration = None + + +def detection(image, conf_threshold=0.3): + image = cv2.resize(image, (model.input_width, model.input_height)) + new_image = model.detect_objects(image, conf_threshold) + return cv2.resize(new_image, (500, 500)) + + +css = """.my-group {max-width: 600px !important; max-height: 600 !important;} + .my-column {display: flex !important; justify-content: center !important; align-items: center !important};""" + + +with gr.Blocks() as demo: + gr.HTML( + """ +

+ YOLOv10 Webcam Stream (Powered by WebRTC ⚡️) +

+ """ + ) + gr.HTML( + """ +

+ arXiv | github +

+ """ + ) + with gr.Column(elem_classes=["my-column"]): + with gr.Group(elem_classes=["my-group"]): + image = WebRTC(label="Stream", rtc_configuration=rtc_configuration) + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + + image.stream( + fn=detection, inputs=[image, conf_threshold], outputs=[image], time_limit=10 + ) + +if __name__ == "__main__": + demo.launch(css=css) diff --git a/demo/yolov10_webcam_stream/utils.py b/demo/yolov10_webcam_stream/utils.py new file mode 100644 index 0000000..a2b282c --- /dev/null +++ b/demo/yolov10_webcam_stream/utils.py @@ -0,0 +1,237 @@ +import numpy as np +import cv2 # type: ignore + +class_names = [ + "person", + "bicycle", + "car", + "motorcycle", + "airplane", + "bus", + "train", + "truck", + "boat", + "traffic light", + "fire hydrant", + "stop sign", + "parking meter", + "bench", + "bird", + "cat", + "dog", + "horse", + "sheep", + "cow", + "elephant", + "bear", + "zebra", + "giraffe", + "backpack", + "umbrella", + "handbag", + "tie", + "suitcase", + "frisbee", + "skis", + "snowboard", + "sports ball", + "kite", + "baseball bat", + "baseball glove", + "skateboard", + "surfboard", + "tennis racket", + "bottle", + "wine glass", + "cup", + "fork", + "knife", + "spoon", + "bowl", + "banana", + "apple", + "sandwich", + "orange", + "broccoli", + "carrot", + "hot dog", + "pizza", + "donut", + "cake", + "chair", + "couch", + "potted plant", + "bed", + "dining table", + "toilet", + "tv", + "laptop", + "mouse", + "remote", + "keyboard", + "cell phone", + "microwave", + "oven", + "toaster", + "sink", + "refrigerator", + "book", + "clock", + "vase", + "scissors", + "teddy bear", + "hair drier", + "toothbrush", +] + +# Create a list of colors for each class where each color is a tuple of 3 integer values +rng = np.random.default_rng(3) +colors = rng.uniform(0, 255, size=(len(class_names), 3)) + + +def nms(boxes, scores, iou_threshold): + # Sort by score + sorted_indices = np.argsort(scores)[::-1] + + keep_boxes = [] + while sorted_indices.size > 0: + # Pick the last box + box_id = sorted_indices[0] + keep_boxes.append(box_id) + + # Compute IoU of the picked box with the rest + ious = compute_iou(boxes[box_id, :], boxes[sorted_indices[1:], :]) + + # Remove boxes with IoU over the threshold + keep_indices = np.where(ious < iou_threshold)[0] + + # print(keep_indices.shape, sorted_indices.shape) + sorted_indices = sorted_indices[keep_indices + 1] + + return keep_boxes + + +def multiclass_nms(boxes, scores, class_ids, iou_threshold): + unique_class_ids = np.unique(class_ids) + + keep_boxes = [] + for class_id in unique_class_ids: + class_indices = np.where(class_ids == class_id)[0] + class_boxes = boxes[class_indices, :] + class_scores = scores[class_indices] + + class_keep_boxes = nms(class_boxes, class_scores, iou_threshold) + keep_boxes.extend(class_indices[class_keep_boxes]) + + return keep_boxes + + +def compute_iou(box, boxes): + # Compute xmin, ymin, xmax, ymax for both boxes + xmin = np.maximum(box[0], boxes[:, 0]) + ymin = np.maximum(box[1], boxes[:, 1]) + xmax = np.minimum(box[2], boxes[:, 2]) + ymax = np.minimum(box[3], boxes[:, 3]) + + # Compute intersection area + intersection_area = np.maximum(0, xmax - xmin) * np.maximum(0, ymax - ymin) + + # Compute union area + box_area = (box[2] - box[0]) * (box[3] - box[1]) + boxes_area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) + union_area = box_area + boxes_area - intersection_area + + # Compute IoU + iou = intersection_area / union_area + + return iou + + +def xywh2xyxy(x): + # Convert bounding box (x, y, w, h) to bounding box (x1, y1, x2, y2) + y = np.copy(x) + y[..., 0] = x[..., 0] - x[..., 2] / 2 + y[..., 1] = x[..., 1] - x[..., 3] / 2 + y[..., 2] = x[..., 0] + x[..., 2] / 2 + y[..., 3] = x[..., 1] + x[..., 3] / 2 + return y + + +def draw_detections(image, boxes, scores, class_ids, mask_alpha=0.3): + det_img = image.copy() + + img_height, img_width = image.shape[:2] + font_size = min([img_height, img_width]) * 0.0006 + text_thickness = int(min([img_height, img_width]) * 0.001) + + # det_img = draw_masks(det_img, boxes, class_ids, mask_alpha) + + # Draw bounding boxes and labels of detections + for class_id, box, score in zip(class_ids, boxes, scores): + color = colors[class_id] + + draw_box(det_img, box, color) # type: ignore + + label = class_names[class_id] + caption = f"{label} {int(score * 100)}%" + draw_text(det_img, caption, box, color, font_size, text_thickness) # type: ignore + + return det_img + + +def draw_box( + image: np.ndarray, + box: np.ndarray, + color: tuple[int, int, int] = (0, 0, 255), + thickness: int = 2, +) -> np.ndarray: + x1, y1, x2, y2 = box.astype(int) + return cv2.rectangle(image, (x1, y1), (x2, y2), color, thickness) + + +def draw_text( + image: np.ndarray, + text: str, + box: np.ndarray, + color: tuple[int, int, int] = (0, 0, 255), + font_size: float = 0.001, + text_thickness: int = 2, +) -> np.ndarray: + x1, y1, x2, y2 = box.astype(int) + (tw, th), _ = cv2.getTextSize( + text=text, + fontFace=cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_size, + thickness=text_thickness, + ) + th = int(th * 1.2) + + cv2.rectangle(image, (x1, y1), (x1 + tw, y1 - th), color, -1) + + return cv2.putText( + image, + text, + (x1, y1), + cv2.FONT_HERSHEY_SIMPLEX, + font_size, + (255, 255, 255), + text_thickness, + cv2.LINE_AA, + ) + + +def draw_masks( + image: np.ndarray, boxes: np.ndarray, classes: np.ndarray, mask_alpha: float = 0.3 +) -> np.ndarray: + mask_img = image.copy() + + # Draw bounding boxes and labels of detections + for box, class_id in zip(boxes, classes): + color = colors[class_id] + + x1, y1, x2, y2 = box.astype(int) + + # Draw fill rectangle in mask image + cv2.rectangle(mask_img, (x1, y1), (x2, y2), color, -1) # type: ignore + + return cv2.addWeighted(mask_img, mask_alpha, image, 1 - mask_alpha, 0) diff --git a/demo/zip_files/.gitignore b/demo/zip_files/.gitignore new file mode 100644 index 0000000..8e9b8f2 --- /dev/null +++ b/demo/zip_files/.gitignore @@ -0,0 +1 @@ +tmp.zip \ No newline at end of file diff --git a/demo/zip_files/run.py b/demo/zip_files/run.py new file mode 100644 index 0000000..f180f11 --- /dev/null +++ b/demo/zip_files/run.py @@ -0,0 +1,23 @@ +from zipfile import ZipFile + +import gradio as gr + +def zip_files(files): + with ZipFile("tmp.zip", "w") as zip_obj: + for file in files: + zip_obj.write(file.name, file.name.split("/")[-1]) + return "tmp.zip" + +demo = gr.Interface( + zip_files, + gr.File(file_count="multiple", file_types=["text", ".json", ".csv"]), + "file", + examples=[[[gr.get_file("titanic.csv"), + gr.get_file("titanic.csv"), + gr.get_file("titanic.csv")]]], + cache_examples=True, + api_name="predict" +) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/zip_files/screenshot.png b/demo/zip_files/screenshot.png new file mode 100644 index 0000000..64591ff Binary files /dev/null and b/demo/zip_files/screenshot.png differ diff --git a/demo/zip_to_json/run.py b/demo/zip_to_json/run.py new file mode 100644 index 0000000..40f42b6 --- /dev/null +++ b/demo/zip_to_json/run.py @@ -0,0 +1,21 @@ +from zipfile import ZipFile + +import gradio as gr + +def zip_to_json(file_obj): + files = [] + with ZipFile(file_obj.name) as zfile: + for zinfo in zfile.infolist(): + files.append( + { + "name": zinfo.filename, + "file_size": zinfo.file_size, + "compressed_size": zinfo.compress_size, + } + ) + return files + +demo = gr.Interface(zip_to_json, "file", "json", api_name="predict") + +if __name__ == "__main__": + demo.launch() diff --git a/demo/zip_to_json/screenshot.png b/demo/zip_to_json/screenshot.png new file mode 100644 index 0000000..0620b78 Binary files /dev/null and b/demo/zip_to_json/screenshot.png differ diff --git a/ffmpeg-bin/ffmpeg b/ffmpeg-bin/ffmpeg new file mode 100755 index 0000000..67dcec7 Binary files /dev/null and b/ffmpeg-bin/ffmpeg differ diff --git a/ffmpeg-bin/ffprobe b/ffmpeg-bin/ffprobe new file mode 100755 index 0000000..4c2063a Binary files /dev/null and b/ffmpeg-bin/ffprobe differ diff --git a/globals.d.ts b/globals.d.ts new file mode 100644 index 0000000..c370c17 --- /dev/null +++ b/globals.d.ts @@ -0,0 +1,53 @@ +import { ApiData, ApiInfo } from "client/js/src/types"; +declare global { + interface Window { + __gradio_mode__: "app" | "website"; + __gradio_space__: string | null; + launchGradio: Function; + launchGradioFromSpaces: Function; + gradio_config: Config; + gradio_api_info: ApiInfo | { api: ApiInfo }; + scoped_css_attach: (link: HTMLLinkElement) => void; + __is_colab__: boolean; + parentIFrame?: { + scrollTo: (x: number, y: number) => void; + size: (height: number) => void; + autoResize: (auto: boolean) => void; + getPageInfo: ( + callback: (info: { scrollTop: number; offsetTop: number }) => void + ) => void; + }; + supports_zerogpu_headers?: boolean; + BUILD_MODE?: "dev" | "production"; + } +} + +export interface Config { + auth_required?: true; + auth_message: string; + components: any[]; + css: string | null; + dependencies: any[]; + dev_mode: boolean; + enable_queue: boolean; + layout: any; + mode: "blocks" | "interface"; + root: string; + theme: string; + title: string; + version: string; + space_id: string | null; + is_colab: boolean; + show_api: boolean; + stylesheets: string[]; + path: string; + js: string | null; + head: string | null; + analytics_enabled: boolean; + show_error: boolean; + is_space: boolean; + protocol: "ws" | "sse" | "sse_v1" | "sse_v2" | "sse_v2.1" | "sse_v3"; + theme_hash?: number; + username: string | null; + current_page: string; +} diff --git a/gradio/.dockerignore b/gradio/.dockerignore new file mode 100644 index 0000000..450a3af --- /dev/null +++ b/gradio/.dockerignore @@ -0,0 +1,2 @@ +templates/frontend +templates/frontend/**/* diff --git a/gradio/CHANGELOG.md b/gradio/CHANGELOG.md new file mode 100644 index 0000000..273d61f --- /dev/null +++ b/gradio/CHANGELOG.md @@ -0,0 +1,8128 @@ +# gradio + +## 6.20.0 + +### Features + +- [#13566](https://github.com/gradio-app/gradio/pull/13566) [`79b9171`](https://github.com/gradio-app/gradio/commit/79b91718b5c972c174ed042d8b04443bc2274076) - Gallery Download All. Thanks @dawoodkhan82! +- [#13544](https://github.com/gradio-app/gradio/pull/13544) [`784eb53`](https://github.com/gradio-app/gradio/commit/784eb536ab6596b0fa967419aaa93de39fa9418d) - workflow: show downstream output on subgraph run. Thanks @hannahblair! +- [#13582](https://github.com/gradio-app/gradio/pull/13582) [`59968e8`](https://github.com/gradio-app/gradio/commit/59968e8fdc86230eebb329ce0871dfaa9fa9c746) - workflow: update input change handling. Thanks @hannahblair! +- [#13543](https://github.com/gradio-app/gradio/pull/13543) [`0533483`](https://github.com/gradio-app/gradio/commit/0533483bccdee38f334a598f18297e8c02966343) - Migrate Image components to Svelte 5. Thanks @dawoodkhan82! +- [#13557](https://github.com/gradio-app/gradio/pull/13557) [`55241c8`](https://github.com/gradio-app/gradio/commit/55241c890fc5f0bbfddae4ce2ec14525d594e05e) - workflow: auto add node on port click. Thanks @hannahblair! +- [#13549](https://github.com/gradio-app/gradio/pull/13549) [`ec64242`](https://github.com/gradio-app/gradio/commit/ec642427856c9b7a3e90c35987d87bb98a6e9fbc) - workflow: validate model before invoking inference client. Thanks @hannahblair! +- [#13555](https://github.com/gradio-app/gradio/pull/13555) [`f7b872e`](https://github.com/gradio-app/gradio/commit/f7b872e52136d0f9308f0c7e0ebd49371148088a) - Replace deprecated Starlette 422 status constant in queue validator error path. Thanks @copilot-swe-agent! +- [#13574](https://github.com/gradio-app/gradio/pull/13574) [`75c5d1e`](https://github.com/gradio-app/gradio/commit/75c5d1eeec6f9cc7e84128812bc0c03b9682d7da) - workflow: inject token into fn functions. Thanks @hannahblair! + +### Fixes + +- [#13592](https://github.com/gradio-app/gradio/pull/13592) [`876d333`](https://github.com/gradio-app/gradio/commit/876d3334a9229e403193f7fd176af8c0720817cf) - Fix `JSONDecodeError` when loading cached examples with negative number outputs. Thanks @hysts! +- [#13596](https://github.com/gradio-app/gradio/pull/13596) [`1c5c538`](https://github.com/gradio-app/gradio/commit/1c5c53842df9c2750552d85c19a92e7e732cff3f) - Security: serve `/gradio_api/file=` via an SSRF-safe streaming proxy instead of an open redirect. Thanks @abidlabs! +- [#13595](https://github.com/gradio-app/gradio/pull/13595) [`4e72cd1`](https://github.com/gradio-app/gradio/commit/4e72cd1d6fd5cdb7d1267df4cfd1eec9a4b4214d) - workflow: forward `_token` to bound fn when no request session. Thanks @hannahblair! +- [#13540](https://github.com/gradio-app/gradio/pull/13540) [`98de45e`](https://github.com/gradio-app/gradio/commit/98de45e708305563f79614221aede2bf5d863922) - Fix gr.Plot collapsing to zero height for autosize Plotly figures. Thanks @hysts! +- [#13563](https://github.com/gradio-app/gradio/pull/13563) [`46d391b`](https://github.com/gradio-app/gradio/commit/46d391bd4385b6cfa31f10b35955c95c86c4cb85) - Fix embedded Gradio apps growing infinitely tall on HF Spaces when using `vh`/`%` heights or `fill_height`. Thanks @abidlabs! +- [#13580](https://github.com/gradio-app/gradio/pull/13580) [`63609f1`](https://github.com/gradio-app/gradio/commit/63609f10bb54523e5a996e584d1e9f635c02e80f) - Enforce `max_file_size` for multipart uploads to the `/component_server` route. Thanks @abidlabs! +- [#13571](https://github.com/gradio-app/gradio/pull/13571) [`c576675`](https://github.com/gradio-app/gradio/commit/c576675c1d0d9a917706bb830f7a738d9f96672f) - Fix `gr.ImageEditor` high CPU usage when idle by sleeping the render loop when nothing is changing. Thanks @abidlabs! +- [`dcc654d`](https://github.com/gradio-app/gradio/commit/dcc654d8467c9202a78c50b31e7d123e9804ebf4) - Fix ImageEditor transform tools and hidden cleanup. Thanks @dawoodkhan82! +- [#13588](https://github.com/gradio-app/gradio/pull/13588) [`2e80558`](https://github.com/gradio-app/gradio/commit/2e805588c8dc84a4a584898fd43b267c72209f78) - Fire `state.change()` for streaming (`.stream()`) events. Thanks @hysts! +- [#13597](https://github.com/gradio-app/gradio/pull/13597) [`7a595cb`](https://github.com/gradio-app/gradio/commit/7a595cb72d8a82151ee2231e86d20c91d0bb9062) - Fix ImageEditor brush texture resets. Thanks @dawoodkhan82! +- [#13581](https://github.com/gradio-app/gradio/pull/13581) [`461d82d`](https://github.com/gradio-app/gradio/commit/461d82df2689ec0c53e84b8722eaeae58fd2a6ae) - Use same-origin credentials in the JS client so cross-origin embeds work again. Thanks @hysts! +- [#13560](https://github.com/gradio-app/gradio/pull/13560) [`745f20c`](https://github.com/gradio-app/gradio/commit/745f20c9ec7d1e542c4b35f121b6f74e8e7c100c) - Fix `gr.Tabs.select()` event not firing when switching tabs. Thanks @abidlabs! +- [#13529](https://github.com/gradio-app/gradio/pull/13529) [`6438369`](https://github.com/gradio-app/gradio/commit/64383695167206d7775a364938a1005c662ea9c7) - Wait for in-flight head scripts before running js_on_load in gr.HTML. Thanks @hysts! +- [#13553](https://github.com/gradio-app/gradio/pull/13553) [`9a72d0c`](https://github.com/gradio-app/gradio/commit/9a72d0c76e62af365236a36c0137f7e0497d98cb) - Fix Column staying hidden after a multi-yield visibility update. Thanks @hysts! +- [#13561](https://github.com/gradio-app/gradio/pull/13561) [`882df35`](https://github.com/gradio-app/gradio/commit/882df35e9eaa7b2fb8767e23f03c66c46ab66d78) - Fix `TypeError: this.app.$destroy is not a function` when embedding a Gradio app with the `` web component. Thanks @abidlabs! + +## 6.19.0 + +### Features + +- [#13526](https://github.com/gradio-app/gradio/pull/13526) [`53cb4ca`](https://github.com/gradio-app/gradio/commit/53cb4cae1ec3521e9170d12867253516413ba37a) - Run `pnpm lint` and `pnpm ts:check` on CI. Thanks @abidlabs! +- [#13534](https://github.com/gradio-app/gradio/pull/13534) [`a11c728`](https://github.com/gradio-app/gradio/commit/a11c7282461ccef90f24a031d77a597ac9ae3d0e) - Re-translate i18n choices display names when the language is switched at runtime. Thanks @hysts! +- [#13524](https://github.com/gradio-app/gradio/pull/13524) [`d4d340d`](https://github.com/gradio-app/gradio/commit/d4d340d1bbc1dfdec72fa46431b43f6a808f084f) - Run `gr.Workflow` subgraphs via the Gradio API — each subgraph is exposed as a named endpoint (returning all of its outputs) reusing `/info`, `/call`, and `/api`, with a "View API" panel in the canvas. Thanks @abidlabs! + +### Fixes + +- [#13542](https://github.com/gradio-app/gradio/pull/13542) [`9d83502`](https://github.com/gradio-app/gradio/commit/9d83502215b56ed7747490afd0d5145b087a23e7) - Make dropdowns accessible to screen readers (combobox ARIA pattern). Thanks @hysts! +- [#13532](https://github.com/gradio-app/gradio/pull/13532) [`0a933b4`](https://github.com/gradio-app/gradio/commit/0a933b428f5b2158cb8c764f38abf0da2312d58a) - Fix `gr.SelectData` coordinates when the image in `gr.Image` does not fill its container. Thanks @hysts! +- [#13511](https://github.com/gradio-app/gradio/pull/13511) [`8eafb31`](https://github.com/gradio-app/gradio/commit/8eafb31ee17c134e65d1a006753b9a6ee4631f07) - refactor space and model discovery modal. Thanks @hannahblair! +- [#13533](https://github.com/gradio-app/gradio/pull/13533) [`e5ec1ca`](https://github.com/gradio-app/gradio/commit/e5ec1ca66c45e7e3a057b75ac2b8b86660b2827b) - Fix fullscreen button not working in ImageSlider, interactive Image, native plots, and AnnotatedImage. Thanks @hysts! +- [#13541](https://github.com/gradio-app/gradio/pull/13541) [`1d19f2b`](https://github.com/gradio-app/gradio/commit/1d19f2b21f6b12285089abc73468ac67b5b93e27) - Fix long unbroken text overflowing in the Markdown component. Thanks @hysts! + +## 6.18.0 + +### Features + +- [#13510](https://github.com/gradio-app/gradio/pull/13510) [`75c684e`](https://github.com/gradio-app/gradio/commit/75c684efb87624bee2fb63b08122564e6538509e) - Migrate Plot to Svelte 5. Thanks @dawoodkhan82! +- [#13501](https://github.com/gradio-app/gradio/pull/13501) [`e547392`](https://github.com/gradio-app/gradio/commit/e547392d794bc8ac0bcf60f6846229e95350f2c4) - workflow: update pipeline UX around pro accounts. Thanks @hannahblair! +- [#13517](https://github.com/gradio-app/gradio/pull/13517) [`100eaf2`](https://github.com/gradio-app/gradio/commit/100eaf2705861ccb71ea53748e2c6965a9c68bd0) - workflow: implement drag selection. Thanks @hannahblair! +- [#13509](https://github.com/gradio-app/gradio/pull/13509) [`dcd072c`](https://github.com/gradio-app/gradio/commit/dcd072cd57f09ffd2dc5f97ae6afc505894824a6) - Migrate Chatbot, Tabs, TabItem to Svelte 5. Thanks @dawoodkhan82! +- [#13502](https://github.com/gradio-app/gradio/pull/13502) [`429faeb`](https://github.com/gradio-app/gradio/commit/429faeb643fb1afc1722c0f63fafa11603f2c87f) - Ensure every component dispatches a `change` event when its value changes. Thanks @abidlabs! +- [#13516](https://github.com/gradio-app/gradio/pull/13516) [`8ec4cbc`](https://github.com/gradio-app/gradio/commit/8ec4cbc8de3bbc1d8ecb660ad8dd22ad35500e1c) - Translate i18n `choices` display names in choice-based components. Thanks @hysts! +- [#13513](https://github.com/gradio-app/gradio/pull/13513) [`df59064`](https://github.com/gradio-app/gradio/commit/df590646af4c2e518817201a2f1d3e334566669c) - Treat expired OAuth sessions as logged out users. Thanks @abidlabs! +- [#13507](https://github.com/gradio-app/gradio/pull/13507) [`a9550d8`](https://github.com/gradio-app/gradio/commit/a9550d863e2487eb0012d872e589d04dc60fb3c9) - Warn when a ` +``` + + Thanks [@hannahblair](https://github.com/hannahblair)! + +### Features + +- [#5268](https://github.com/gradio-app/gradio/pull/5268) [`f49028cf`](https://github.com/gradio-app/gradio/commit/f49028cfe3e21097001ddbda71c560b3d8b42e1c) - Move markdown & latex processing to the frontend for the gr.Markdown and gr.DataFrame components. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5215](https://github.com/gradio-app/gradio/pull/5215) [`fbdad78a`](https://github.com/gradio-app/gradio/commit/fbdad78af4c47454cbb570f88cc14bf4479bbceb) - Lazy load interactive or static variants of a component individually, rather than loading both variants regardless. This change will improve performance for many applications. Thanks [@pngwn](https://github.com/pngwn)! +- [#5216](https://github.com/gradio-app/gradio/pull/5216) [`4b58ea6d`](https://github.com/gradio-app/gradio/commit/4b58ea6d98e7a43b3f30d8a4cb6f379bc2eca6a8) - Update i18n tokens and locale files. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5283](https://github.com/gradio-app/gradio/pull/5283) [`a7460557`](https://github.com/gradio-app/gradio/commit/a74605572dd0d6bb41df6b38b120d656370dd67d) - Add height parameter and scrolling to `gr.Dataframe`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5232](https://github.com/gradio-app/gradio/pull/5232) [`c57d4c23`](https://github.com/gradio-app/gradio/commit/c57d4c232a97e03b4671f9e9edc3af456438fe89) - `gr.Radio` and `gr.CheckboxGroup` can now accept different names and values. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5219](https://github.com/gradio-app/gradio/pull/5219) [`e8fd4e4e`](https://github.com/gradio-app/gradio/commit/e8fd4e4ec68a6c974bc8c84b61f4a0ec50a85bc6) - Add `api_name` parameter to `gr.Interface`. Additionally, completely hide api page if show_api=False. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5280](https://github.com/gradio-app/gradio/pull/5280) [`a2f42e28`](https://github.com/gradio-app/gradio/commit/a2f42e28bd793bce4bed6d54164bb2a327a46fd5) - Allow updating the label of `gr.UpdateButton`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5112](https://github.com/gradio-app/gradio/pull/5112) [`1cefee7f`](https://github.com/gradio-app/gradio/commit/1cefee7fc05175aca23ba04b3a3fda7b97f49bf0) - chore(deps): update dependency marked to v7. Thanks [@renovate](https://github.com/apps/renovate)! +- [#5260](https://github.com/gradio-app/gradio/pull/5260) [`a773eaf7`](https://github.com/gradio-app/gradio/commit/a773eaf7504abb53b99885b3454dc1e027adbb42) - Stop passing inputs and preprocessing on iterators. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#4943](https://github.com/gradio-app/gradio/pull/4943) [`947d615d`](https://github.com/gradio-app/gradio/commit/947d615db6f76519d0e8bc0d1a0d7edf89df267b) - Sign in with Hugging Face (OAuth support). Thanks [@Wauplin](https://github.com/Wauplin)! +- [#5298](https://github.com/gradio-app/gradio/pull/5298) [`cf167cd1`](https://github.com/gradio-app/gradio/commit/cf167cd1dd4acd9aee225ff1cb6fac0e849806ba) - Create event listener table for components on docs. Thanks [@aliabd](https://github.com/aliabd)! +- [#5173](https://github.com/gradio-app/gradio/pull/5173) [`730f0c1d`](https://github.com/gradio-app/gradio/commit/730f0c1d54792eb11359e40c9f2326e8a6e39203) - Ensure gradio client works as expected for functions that return nothing. Thanks [@raymondtri](https://github.com/raymondtri)! +- [#5188](https://github.com/gradio-app/gradio/pull/5188) [`b22e1888`](https://github.com/gradio-app/gradio/commit/b22e1888fcf0843520525c1e4b7e1fe73fdeb948) - Fix the images in the theme builder to use permanent URI. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5221](https://github.com/gradio-app/gradio/pull/5221) [`f344592a`](https://github.com/gradio-app/gradio/commit/f344592aeb1658013235ded154107f72d86f24e7) - Allows setting a height to `gr.File` and improves the UI of the component. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5265](https://github.com/gradio-app/gradio/pull/5265) [`06982212`](https://github.com/gradio-app/gradio/commit/06982212dfbd613853133d5d0eebd75577967027) - Removes scrollbar from File preview when not needed. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5305](https://github.com/gradio-app/gradio/pull/5305) [`15075241`](https://github.com/gradio-app/gradio/commit/15075241fa7ad3f7fd9ae2a91e54faf8f19a46f9) - Rotate axes labels on LinePlot, BarPlot, and ScatterPlot. Thanks [@Faiga91](https://github.com/Faiga91)! +- [#5258](https://github.com/gradio-app/gradio/pull/5258) [`92282cea`](https://github.com/gradio-app/gradio/commit/92282cea6afdf7e9930ece1046d8a63be34b3cea) - Chatbot Avatar Images. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! +- [#5244](https://github.com/gradio-app/gradio/pull/5244) [`b3e50db9`](https://github.com/gradio-app/gradio/commit/b3e50db92f452f376aa2cc081326d40bb69d6dd7) - Remove aiohttp dependency. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5264](https://github.com/gradio-app/gradio/pull/5264) [`46a2b600`](https://github.com/gradio-app/gradio/commit/46a2b600a7ff030a9ea1560b882b3bf3ad266bbc) - ensure translations for audio work correctly. Thanks [@hannahblair](https://github.com/hannahblair)! + +### Fixes + +- [#5256](https://github.com/gradio-app/gradio/pull/5256) [`933db53e`](https://github.com/gradio-app/gradio/commit/933db53e93a1229fdf149556d61da5c4c7e1a331) - Better handling of empty dataframe in `gr.DataFrame`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5242](https://github.com/gradio-app/gradio/pull/5242) [`2b397791`](https://github.com/gradio-app/gradio/commit/2b397791fe2059e4beb72937ff0436f2d4d28b4b) - Fix message text overflow onto copy button in `gr.Chatbot`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5253](https://github.com/gradio-app/gradio/pull/5253) [`ddac7e4d`](https://github.com/gradio-app/gradio/commit/ddac7e4d0f55c3bdc6c3e9a9e24588b2563e4049) - Ensure File component uploads files to the server. Thanks [@pngwn](https://github.com/pngwn)! +- [#5179](https://github.com/gradio-app/gradio/pull/5179) [`6fb92b48`](https://github.com/gradio-app/gradio/commit/6fb92b48a916104db573602011a448b904d42e5e) - Fixes audio streaming issues. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#5295](https://github.com/gradio-app/gradio/pull/5295) [`7b8fa8aa`](https://github.com/gradio-app/gradio/commit/7b8fa8aa58f95f5046b9add64b40368bd3f1b700) - Allow caching examples with streamed output. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#5285](https://github.com/gradio-app/gradio/pull/5285) [`cdfd4217`](https://github.com/gradio-app/gradio/commit/cdfd42174a9c777eaee9c1209bf8e90d8c7791f2) - Tweaks to `icon` parameter in `gr.Button()`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5122](https://github.com/gradio-app/gradio/pull/5122) [`3b805346`](https://github.com/gradio-app/gradio/commit/3b8053469aca6c7a86a6731e641e4400fc34d7d3) - Allows code block in chatbot to scroll horizontally. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! +- [#5312](https://github.com/gradio-app/gradio/pull/5312) [`f769cb67`](https://github.com/gradio-app/gradio/commit/f769cb67149d8e209091508f06d87014acaed965) - only start listening for events after the components are mounted. Thanks [@pngwn](https://github.com/pngwn)! +- [#5254](https://github.com/gradio-app/gradio/pull/5254) [`c39f06e1`](https://github.com/gradio-app/gradio/commit/c39f06e16b9feea97984e4822df35a99c807461c) - Fix `.update()` for `gr.Radio()` and `gr.CheckboxGroup()`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5231](https://github.com/gradio-app/gradio/pull/5231) [`87f1c2b4`](https://github.com/gradio-app/gradio/commit/87f1c2b4ac7c685c43477215fa5b96b6cbeffa05) - Allow `gr.Interface.from_pipeline()` and `gr.load()` to work within `gr.Blocks()`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5238](https://github.com/gradio-app/gradio/pull/5238) [`de23e9f7`](https://github.com/gradio-app/gradio/commit/de23e9f7d67e685e791faf48a21f34121f6d094a) - Improve audio streaming. Thanks [@aliabid94](https://github.com/aliabid94)!/n - Proper audio streaming with WAV files. We now do the proper processing to stream out wav files as a single stream of audio without any cracks in the seams./n - Audio streaming with bytes. Stream any audio type by yielding out bytes, and it should work flawlessly. +- [#5313](https://github.com/gradio-app/gradio/pull/5313) [`54bcb724`](https://github.com/gradio-app/gradio/commit/54bcb72417b2781ad9d7500ea0f89aa9d80f7d8f) - Restores missing part of bottom border on file component. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5235](https://github.com/gradio-app/gradio/pull/5235) [`1ecf88ac`](https://github.com/gradio-app/gradio/commit/1ecf88ac5f20bc5a1c91792d1a68559575e6afd7) - fix #5229. Thanks [@breengles](https://github.com/breengles)! +- [#5276](https://github.com/gradio-app/gradio/pull/5276) [`502f1015`](https://github.com/gradio-app/gradio/commit/502f1015bf23b365bc32446dd2e549b0c5d0dc72) - Ensure `Blocks` translation copy renders correctly. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5296](https://github.com/gradio-app/gradio/pull/5296) [`a0f22626`](https://github.com/gradio-app/gradio/commit/a0f22626f2aff297754414bbc83d5c4cfe086ea0) - `make_waveform()` twitter video resolution fix. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! + +## 3.40.0 + +### Highlights + +#### Client.predict will now return the final output for streaming endpoints ([#5057](https://github.com/gradio-app/gradio/pull/5057) [`35856f8b`](https://github.com/gradio-app/gradio/commit/35856f8b54548cae7bd3b8d6a4de69e1748283b2)) + +### This is a breaking change (for gradio_client only)! + +Previously, `Client.predict` would only return the first output of an endpoint that streamed results. This was causing confusion for developers that wanted to call these streaming demos via the client. + +We realize that developers using the client don't know the internals of whether a demo streams or not, so we're changing the behavior of predict to match developer expectations. + +Using `Client.predict` will now return the final output of a streaming endpoint. This will make it even easier to use gradio apps via the client. + + Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +#### Gradio now supports streaming audio outputs + +Allows users to use generators to stream audio out, yielding consecutive chunks of audio. Requires `streaming=True` to be set on the output audio. + +```python +import gradio as gr +from pydub import AudioSegment + +def stream_audio(audio_file): + audio = AudioSegment.from_mp3(audio_file) + i = 0 + chunk_size = 3000 + + while chunk_size*i < len(audio): + chunk = audio[chunk_size*i:chunk_size*(i+1)] + i += 1 + if chunk: + file = f"/tmp/{i}.mp3" + chunk.export(file, format="mp3") + yield file + +demo = gr.Interface( + fn=stream_audio, + inputs=gr.Audio(type="filepath", label="Audio file to stream"), + outputs=gr.Audio(autoplay=True, streaming=True), +) + +demo.queue().launch() +``` + +From the backend, streamed outputs are served from the `/stream/` endpoint instead of the `/file/` endpoint. Currently just used to serve audio streaming output. The output JSON will have `is_stream`: `true`, instead of `is_file`: `true` in the file data object. Thanks [@aliabid94](https://github.com/aliabid94)! + +### Features + +- [#5081](https://github.com/gradio-app/gradio/pull/5081) [`d7f83823`](https://github.com/gradio-app/gradio/commit/d7f83823fbd7604456b0127d689a63eed759807d) - solve how can I config root_path dynamically? #4968. Thanks [@eastonsuo](https://github.com/eastonsuo)! +- [#5025](https://github.com/gradio-app/gradio/pull/5025) [`6693660a`](https://github.com/gradio-app/gradio/commit/6693660a790996f8f481feaf22a8c49130d52d89) - Add download button to selected images in `Gallery`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5133](https://github.com/gradio-app/gradio/pull/5133) [`61129052`](https://github.com/gradio-app/gradio/commit/61129052ed1391a75c825c891d57fa0ad6c09fc8) - Update dependency esbuild to ^0.19.0. Thanks [@renovate](https://github.com/apps/renovate)! +- [#5125](https://github.com/gradio-app/gradio/pull/5125) [`80be7a1c`](https://github.com/gradio-app/gradio/commit/80be7a1ca44c0adef1668367b2cf36b65e52e576) - chatbot conversation nodes can contain a copy button. Thanks [@fazpu](https://github.com/fazpu)! +- [#5048](https://github.com/gradio-app/gradio/pull/5048) [`0b74a159`](https://github.com/gradio-app/gradio/commit/0b74a1595b30df744e32a2c358c07acb7fd1cfe5) - Use `importlib` in favor of deprecated `pkg_resources`. Thanks [@jayceslesar](https://github.com/jayceslesar)! +- [#5045](https://github.com/gradio-app/gradio/pull/5045) [`3b9494f5`](https://github.com/gradio-app/gradio/commit/3b9494f5c57e6b52e6a040ce8d6b5141f780e84d) - Lite: Fix the analytics module to use asyncio to work in the Wasm env. Thanks [@whitphx](https://github.com/whitphx)! +- [#5046](https://github.com/gradio-app/gradio/pull/5046) [`5244c587`](https://github.com/gradio-app/gradio/commit/5244c5873c355cf3e2f0acb7d67fda3177ef8b0b) - Allow new lines in `HighlightedText` with `/n` and preserve whitespace. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5076](https://github.com/gradio-app/gradio/pull/5076) [`2745075a`](https://github.com/gradio-app/gradio/commit/2745075a26f80e0e16863d483401ff1b6c5ada7a) - Add deploy_discord to docs. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5116](https://github.com/gradio-app/gradio/pull/5116) [`0dc49b4c`](https://github.com/gradio-app/gradio/commit/0dc49b4c517706f572240f285313a881089ced79) - Add support for async functions and async generators to `gr.ChatInterface`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5047](https://github.com/gradio-app/gradio/pull/5047) [`883ac364`](https://github.com/gradio-app/gradio/commit/883ac364f69d92128774ac446ce49bdf8415fd7b) - Add `step` param to `Number`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5137](https://github.com/gradio-app/gradio/pull/5137) [`22aa5eba`](https://github.com/gradio-app/gradio/commit/22aa5eba3fee3f14473e4b0fac29cf72fe31ef04) - Use font size `--text-md` for `` in Chatbot messages. Thanks [@jaywonchung](https://github.com/jaywonchung)! +- [#5005](https://github.com/gradio-app/gradio/pull/5005) [`f5539c76`](https://github.com/gradio-app/gradio/commit/f5539c7618e31451420bd3228754774da14dc65f) - Enhancement: Add focus event to textbox and number component. Thanks [@JodyZ0203](https://github.com/JodyZ0203)! +- [#5104](https://github.com/gradio-app/gradio/pull/5104) [`34f6b22e`](https://github.com/gradio-app/gradio/commit/34f6b22efbfedfa569d452f3f99ed2e6593e3c21) - Strip leading and trailing spaces from username in login route. Thanks [@sweep-ai](https://github.com/apps/sweep-ai)! +- [#5149](https://github.com/gradio-app/gradio/pull/5149) [`144df459`](https://github.com/gradio-app/gradio/commit/144df459a3b7895e524defcfc4c03fbb8b083aca) - Add `show_edit_button` param to `gr.Audio`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5136](https://github.com/gradio-app/gradio/pull/5136) [`eaa1ce14`](https://github.com/gradio-app/gradio/commit/eaa1ce14ac41de1c23321e93f11f1b03a2f3c7f4) - Enhancing Tamil Translation: Language Refinement 🌟. Thanks [@sanjaiyan-dev](https://github.com/sanjaiyan-dev)! +- [#5035](https://github.com/gradio-app/gradio/pull/5035) [`8b4eb8ca`](https://github.com/gradio-app/gradio/commit/8b4eb8cac9ea07bde31b44e2006ca2b7b5f4de36) - JS Client: Fixes cannot read properties of null (reading 'is_file'). Thanks [@raymondtri](https://github.com/raymondtri)! +- [#5023](https://github.com/gradio-app/gradio/pull/5023) [`e6317d77`](https://github.com/gradio-app/gradio/commit/e6317d77f87d3dad638acca3dbc4a9228570e63c) - Update dependency extendable-media-recorder to v8. Thanks [@renovate](https://github.com/apps/renovate)! +- [#5085](https://github.com/gradio-app/gradio/pull/5085) [`13e47835`](https://github.com/gradio-app/gradio/commit/13e478353532c4af18cfa50772f8b6fb3c6c9818) - chore(deps): update dependency extendable-media-recorder to v8. Thanks [@renovate](https://github.com/apps/renovate)! +- [#5080](https://github.com/gradio-app/gradio/pull/5080) [`37caa2e0`](https://github.com/gradio-app/gradio/commit/37caa2e0fe95d6cab8beb174580fb557904f137f) - Add icon and link params to `gr.Button`. Thanks [@hannahblair](https://github.com/hannahblair)! + +### Fixes + +- [#5062](https://github.com/gradio-app/gradio/pull/5062) [`7d897165`](https://github.com/gradio-app/gradio/commit/7d89716519d0751072792c9bbda668ffeb597296) - `gr.Dropdown` now has correct behavior in static mode as well as when an option is selected. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5077](https://github.com/gradio-app/gradio/pull/5077) [`667875b2`](https://github.com/gradio-app/gradio/commit/667875b2441753e74d25bd9d3c8adedd8ede11cd) - Live audio streaming output +- [#5118](https://github.com/gradio-app/gradio/pull/5118) [`1b017e68`](https://github.com/gradio-app/gradio/commit/1b017e68f6a9623cc2ec085bd20e056229552028) - Add `interactive` args to `gr.ColorPicker`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5114](https://github.com/gradio-app/gradio/pull/5114) [`56d2609d`](https://github.com/gradio-app/gradio/commit/56d2609de93387a75dc82b1c06c1240c5b28c0b8) - Reset textbox value to empty string when value is None. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5075](https://github.com/gradio-app/gradio/pull/5075) [`67265a58`](https://github.com/gradio-app/gradio/commit/67265a58027ef1f9e4c0eb849a532f72eaebde48) - Allow supporting >1000 files in `gr.File()` and `gr.UploadButton()`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5135](https://github.com/gradio-app/gradio/pull/5135) [`80727bbe`](https://github.com/gradio-app/gradio/commit/80727bbe2c6d631022054edf01515017691b3bdd) - Fix dataset features and dataset preview for HuggingFaceDatasetSaver. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5039](https://github.com/gradio-app/gradio/pull/5039) [`620e4645`](https://github.com/gradio-app/gradio/commit/620e46452729d6d4877b3fab84a65daf2f2b7bc6) - `gr.Dropdown()` now supports values with arbitrary characters and doesn't clear value when re-focused. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5061](https://github.com/gradio-app/gradio/pull/5061) [`136adc9c`](https://github.com/gradio-app/gradio/commit/136adc9ccb23e5cb4d02d2e88f23f0b850041f98) - Ensure `gradio_client` is backwards compatible with `gradio==3.24.1`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5129](https://github.com/gradio-app/gradio/pull/5129) [`97d804c7`](https://github.com/gradio-app/gradio/commit/97d804c748be9acfe27b8369dd2d64d61f43c2e7) - [Spaces] ZeroGPU Queue fix. Thanks [@cbensimon](https://github.com/cbensimon)! +- [#5140](https://github.com/gradio-app/gradio/pull/5140) [`cd1353fa`](https://github.com/gradio-app/gradio/commit/cd1353fa3eb1b015f5860ca5d5a8e8d1aa4a831c) - Fixes the display of minutes in the video player. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5111](https://github.com/gradio-app/gradio/pull/5111) [`b84a35b7`](https://github.com/gradio-app/gradio/commit/b84a35b7b91eca947f787648ceb361b1d023427b) - Add icon and link to DuplicateButton. Thanks [@aliabd](https://github.com/aliabd)! +- [#5030](https://github.com/gradio-app/gradio/pull/5030) [`f6c491b0`](https://github.com/gradio-app/gradio/commit/f6c491b079d335af633dd854c68eb26f9e61c552) - highlightedtext throws an error basing on model. Thanks [@rajeunoia](https://github.com/rajeunoia)! + +## 3.39.0 + +### Highlights + +#### Create Discord Bots from Gradio Apps 🤖 ([#4960](https://github.com/gradio-app/gradio/pull/4960) [`46e4ef67`](https://github.com/gradio-app/gradio/commit/46e4ef67d287dd68a91473b73172b29cbad064bc)) + +We're excited to announce that Gradio can now automatically create a discord bot from any `gr.ChatInterface` app. + +It's as easy as importing `gradio_client`, connecting to the app, and calling `deploy_discord`! + +_🦙 Turning Llama 2 70b into a discord bot 🦙_ + +```python +import gradio_client as grc +grc.Client("ysharma/Explore_llamav2_with_TGI").deploy_discord(to_id="llama2-70b-discord-bot") +``` + + + +#### Getting started with template spaces + +To help get you started, we have created an organization on Hugging Face called [gradio-discord-bots](https://huggingface.co/gradio-discord-bots) with template spaces you can use to turn state of the art LLMs powered by Gradio to discord bots. + +Currently we have template spaces for: + +- [Llama-2-70b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/Llama-2-70b-chat-hf) powered by a FREE Hugging Face Inference Endpoint! +- [Llama-2-13b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/Llama-2-13b-chat-hf) powered by Hugging Face Inference Endpoints. +- [Llama-2-13b-chat-hf](https://huggingface.co/spaces/gradio-discord-bots/llama-2-13b-chat-transformers) powered by Hugging Face transformers. +- [falcon-7b-instruct](https://huggingface.co/spaces/gradio-discord-bots/falcon-7b-instruct) powered by Hugging Face Inference Endpoints. +- [gpt-3.5-turbo](https://huggingface.co/spaces/gradio-discord-bots/gpt-35-turbo), powered by openai. Requires an OpenAI key. + +But once again, you can deploy ANY `gr.ChatInterface` app exposed on the internet! So don't hesitate to try it on your own Chatbots. + +❗️ Additional Note ❗️: Technically, any gradio app that exposes an api route that takes in a single string and outputs a single string can be deployed to discord. But `gr.ChatInterface` apps naturally lend themselves to discord's chat functionality so we suggest you start with those. + +Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Features + +- [#4995](https://github.com/gradio-app/gradio/pull/4995) [`3f8c210b`](https://github.com/gradio-app/gradio/commit/3f8c210b01ef1ceaaf8ee73be4bf246b5b745bbf) - Implement left and right click in `Gallery` component and show implicit images in `Gallery` grid. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#4993](https://github.com/gradio-app/gradio/pull/4993) [`dc07a9f9`](https://github.com/gradio-app/gradio/commit/dc07a9f947de44b419d8384987a02dcf94977851) - Bringing back the "Add download button for audio" PR by [@leuryr](https://github.com/leuryr). Thanks [@abidlabs](https://github.com/abidlabs)! +- [#4979](https://github.com/gradio-app/gradio/pull/4979) [`44ac8ad0`](https://github.com/gradio-app/gradio/commit/44ac8ad08d82ea12c503dde5c78f999eb0452de2) - Allow setting sketch color default. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#4985](https://github.com/gradio-app/gradio/pull/4985) [`b74f8453`](https://github.com/gradio-app/gradio/commit/b74f8453034328f0e42da8e41785f5eb039b45d7) - Adds `additional_inputs` to `gr.ChatInterface`. Thanks [@abidlabs](https://github.com/abidlabs)! + +### Fixes + +- [#4997](https://github.com/gradio-app/gradio/pull/4997) [`41c83070`](https://github.com/gradio-app/gradio/commit/41c83070b01632084e7d29123048a96c1e261407) - Add CSS resets and specifiers to play nice with HF blog. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 3.38 + +### New Features: + +- Provide a parameter `animate` (`False` by default) in `gr.make_waveform()` which animates the overlayed waveform by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4918](https://github.com/gradio-app/gradio/pull/4918) +- Add `show_download_button` param to allow the download button in static Image components to be hidden by [@hannahblair](https://github.com/hannahblair) in [PR 4959](https://github.com/gradio-app/gradio/pull/4959) +- Added autofocus argument to Textbox by [@aliabid94](https://github.com/aliabid94) in [PR 4978](https://github.com/gradio-app/gradio/pull/4978) +- The `gr.ChatInterface` UI now converts the "Submit" button to a "Stop" button in ChatInterface while streaming, which can be used to pause generation. By [@abidlabs](https://github.com/abidlabs) in [PR 4971](https://github.com/gradio-app/gradio/pull/4971). +- Add a `border_color_accent_subdued` theme variable to add a subdued border color to accented items. This is used by chatbot user messages. Set the value of this variable in `Default` theme to `*primary_200`. By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4989](https://github.com/gradio-app/gradio/pull/4989) +- Add default sketch color argument `brush_color`. Also, masks drawn on images are now slightly translucent (and mask color can also be set via brush_color). By [@aliabid94](https://github.com/aliabid94) in [PR 4979](https://github.com/gradio-app/gradio/pull/4979) + +### Bug Fixes: + +- Fixes `cancels` for generators so that if a generator is canceled before it is complete, subsequent runs of the event do not continue from the previous iteration, but rather start from the beginning. By [@abidlabs](https://github.com/abidlabs) in [PR 4969](https://github.com/gradio-app/gradio/pull/4969). +- Use `gr.State` in `gr.ChatInterface` to reduce latency by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4976](https://github.com/gradio-app/gradio/pull/4976) +- Fix bug with `gr.Interface` where component labels inferred from handler parameters were including special args like `gr.Request` or `gr.EventData`. By [@cbensimon](https://github.com/cbensimon) in [PR 4956](https://github.com/gradio-app/gradio/pull/4956) + +### Breaking Changes: + +No changes to highlight. + +### Other Changes: + +- Apply pyright to the `components` directory by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4948](https://github.com/gradio-app/gradio/pull/4948) +- Improved look of ChatInterface by [@aliabid94](https://github.com/aliabid94) in [PR 4978](https://github.com/gradio-app/gradio/pull/4978) + +## 3.37 + +### New Features: + +Introducing a new `gr.ChatInterface` abstraction, which allows Gradio users to build fully functioning Chat interfaces very easily. The only required parameter is a chat function `fn`, which accepts a (string) user input `message` and a (list of lists) chat `history` and returns a (string) response. Here's a toy example: + +```py +import gradio as gr + +def echo(message, history): + return message + +demo = gr.ChatInterface(fn=echo, examples=["hello", "hola", "merhaba"], title="Echo Bot") +demo.launch() +``` + +Which produces: + +image + +And a corresponding easy-to-use API at `/chat`: + +image + +The `gr.ChatInterface` abstraction works nicely with various LLM libraries, such as `langchain`. See the [dedicated guide](https://gradio.app/guides/creating-a-chatbot-fast) for more examples using `gr.ChatInterface`. Collective team effort in [PR 4869](https://github.com/gradio-app/gradio/pull/4869) + +- Chatbot messages now show hyperlinks to download files uploaded to `gr.Chatbot()` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4848](https://github.com/gradio-app/gradio/pull/4848) +- Cached examples now work with generators and async generators by [@abidlabs](https://github.com/abidlabs) in [PR 4927](https://github.com/gradio-app/gradio/pull/4927) +- Add RTL support to `gr.Markdown`, `gr.Chatbot`, `gr.Textbox` (via the `rtl` boolean parameter) and text-alignment to `gr.Textbox`(via the string `text_align` parameter) by [@abidlabs](https://github.com/abidlabs) in [PR 4933](https://github.com/gradio-app/gradio/pull/4933) + +Examples of usage: + +```py +with gr.Blocks() as demo: + gr.Textbox(interactive=True, text_align="right") +demo.launch() +``` + +```py +with gr.Blocks() as demo: + gr.Markdown("سلام", rtl=True) +demo.launch() +``` + +- The `get_api_info` method of `Blocks` now supports layout output components [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4871](https://github.com/gradio-app/gradio/pull/4871) + +- Added the support for the new command `gradio environment`to make it easier for people to file bug reports if we shipped an easy command to list the OS, gradio version, and versions of gradio/gradio-client dependencies. bu [@varshneydevansh](https://github.com/varshneydevansh) in [PR 4915](https://github.com/gradio-app/gradio/pull/4915). + +### Bug Fixes: + +- The `.change()` event is fixed in `Video` and `Image` so that it only fires once by [@abidlabs](https://github.com/abidlabs) in [PR 4793](https://github.com/gradio-app/gradio/pull/4793) +- The `.change()` event is fixed in `Audio` so that fires when the component value is programmatically updated by [@abidlabs](https://github.com/abidlabs) in [PR 4793](https://github.com/gradio-app/gradio/pull/4793) + +* Add missing `display: flex` property to `Row` so that flex styling is applied to children by [@hannahblair] in [PR 4896](https://github.com/gradio-app/gradio/pull/4896) +* Fixed bug where `gr.Video` could not preprocess urls by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4904](https://github.com/gradio-app/gradio/pull/4904) +* Fixed copy button rendering in API page on Safari by [@aliabid94](https://github.com/aliabid94) in [PR 4924](https://github.com/gradio-app/gradio/pull/4924) +* Fixed `gr.Group` and `container=False`. `container` parameter only available for `Textbox`, `Number`, and `Dropdown`, the only elements where it makes sense. By [@aliabid94](https://github.com/aliabid94) in [PR 4916](https://github.com/gradio-app/gradio/pull/4916) +* Fixed broken image link in auto-generated `app.py` from `ThemeClass.push_to_hub` by [@deepkyu](https://github.com/deepkyu) in [PR 4944](https://github.com/gradio-app/gradio/pull/4944) + +### Other Changes: + +- Warning on mobile that if a user leaves the tab, websocket connection may break. On broken connection, tries to rejoin queue and displays error conveying connection broke. By [@aliabid94](https://github.com/aliabid94) in [PR 4742](https://github.com/gradio-app/gradio/pull/4742) +- Remove blocking network calls made before the local URL gets printed - these slow down the display of the local URL, especially when no internet is available. [@aliabid94](https://github.com/aliabid94) in [PR 4905](https://github.com/gradio-app/gradio/pull/4905). +- Pinned dependencies to major versions to reduce the likelihood of a broken `gradio` due to changes in downstream dependencies by [@abidlabs](https://github.com/abidlabs) in [PR 4885](https://github.com/gradio-app/gradio/pull/4885) +- Queue `max_size` defaults to parent Blocks `max_thread` when running on Spaces with ZeroGPU hardware. By [@cbensimon](https://github.com/cbensimon) in [PR 4937](https://github.com/gradio-app/gradio/pull/4937) + +### Breaking Changes: + +Motivated by the release of `pydantic==2.0`, which included breaking changes that broke a large number of Gradio apps, we've pinned many gradio dependencies. Note that pinned dependencies can cause downstream conflicts, so this may be a breaking change. That being said, we've kept the pins pretty loose, and we're expecting change to be better for the long-term stability of Gradio apps. + +## 3.36.1 + +### New Features: + +- Hotfix to support pydantic v1 and v2 by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4835](https://github.com/gradio-app/gradio/pull/4835) + +### Bug Fixes: + +- Fix bug where `gr.File` change event was not triggered when the value was changed by another event by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4811](https://github.com/gradio-app/gradio/pull/4811) + +### Other Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +## 3.36.0 + +### New Features: + +- The `gr.Video`, `gr.Audio`, `gr.Image`, `gr.Chatbot`, and `gr.Gallery` components now include a share icon when deployed on Spaces. This behavior can be modified by setting the `show_share_button` parameter in the component classes. by [@aliabid94](https://github.com/aliabid94) in [PR 4651](https://github.com/gradio-app/gradio/pull/4651) +- Allow the web component `space`, `src`, and `host` attributes to be updated dynamically by [@pngwn](https://github.com/pngwn) in [PR 4461](https://github.com/gradio-app/gradio/pull/4461) +- Suggestion for Spaces Duplication built into Gradio, by [@aliabid94](https://github.com/aliabid94) in [PR 4458](https://github.com/gradio-app/gradio/pull/4458) +- The `api_name` parameter now accepts `False` as a value, which means it does not show up in named or unnamed endpoints. By [@abidlabs](https://github.com/aliabid94) in [PR 4683](https://github.com/gradio-app/gradio/pull/4683) +- Added support for `pathlib.Path` in `gr.Video`, `gr.Gallery`, and `gr.Chatbot` by [sunilkumardash9](https://github.com/sunilkumardash9) in [PR 4581](https://github.com/gradio-app/gradio/pull/4581). + +### Bug Fixes: + +- Updated components with `info` attribute to update when `update()` is called on them. by [@jebarpg](https://github.com/jebarpg) in [PR 4715](https://github.com/gradio-app/gradio/pull/4715). +- Ensure the `Image` components undo button works mode is `mask` or `color-sketch` by [@amyorz](https://github.com/AmyOrz) in [PR 4692](https://github.com/gradio-app/gradio/pull/4692) +- Load the iframe resizer external asset asynchronously, by [@akx](https://github.com/akx) in [PR 4336](https://github.com/gradio-app/gradio/pull/4336) +- Restored missing imports in `gr.components` by [@abidlabs](https://github.com/abidlabs) in [PR 4566](https://github.com/gradio-app/gradio/pull/4566) +- Fix bug where `select` event was not triggered in `gr.Gallery` if `height` was set to be large with `allow_preview=False` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4551](https://github.com/gradio-app/gradio/pull/4551) +- Fix bug where setting `visible=False` in `gr.Group` event did not work by [@abidlabs](https://github.com/abidlabs) in [PR 4567](https://github.com/gradio-app/gradio/pull/4567) +- Fix `make_waveform` to work with paths that contain spaces [@akx](https://github.com/akx) in [PR 4570](https://github.com/gradio-app/gradio/pull/4570) & [PR 4578](https://github.com/gradio-app/gradio/pull/4578) +- Send captured data in `stop_recording` event for `gr.Audio` and `gr.Video` components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4554](https://github.com/gradio-app/gradio/pull/4554) +- Fix bug in `gr.Gallery` where `height` and `object_fit` parameters where being ignored by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4576](https://github.com/gradio-app/gradio/pull/4576) +- Fixes an HTML sanitization issue in DOMPurify where links in markdown were not opening in a new window by [@hannahblair] in [PR 4577](https://github.com/gradio-app/gradio/pull/4577) +- Fixed Dropdown height rendering in Columns by [@aliabid94](https://github.com/aliabid94) in [PR 4584](https://github.com/gradio-app/gradio/pull/4584) +- Fixed bug where `AnnotatedImage` css styling was causing the annotation masks to not be displayed correctly by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4628](https://github.com/gradio-app/gradio/pull/4628) +- Ensure that Gradio does not silently fail when running on a port that is occupied by [@abidlabs](https://github.com/abidlabs) in [PR 4624](https://github.com/gradio-app/gradio/pull/4624). +- Fix double upload bug that caused lag in file uploads by [@aliabid94](https://github.com/aliabid94) in [PR 4661](https://github.com/gradio-app/gradio/pull/4661) +- `Progress` component now appears even when no `iterable` is specified in `tqdm` constructor by [@itrushkin](https://github.com/itrushkin) in [PR 4475](https://github.com/gradio-app/gradio/pull/4475) +- Deprecation warnings now point at the user code using those deprecated features, instead of Gradio internals, by (https://github.com/akx) in [PR 4694](https://github.com/gradio-app/gradio/pull/4694) +- Adapt column widths in gr.Examples based on content by [@pngwn](https://github.com/pngwn) & [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4700](https://github.com/gradio-app/gradio/pull/4700) +- The `plot` parameter deprecation warnings should now only be emitted for `Image` components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4709](https://github.com/gradio-app/gradio/pull/4709) +- Removed uncessessary `type` deprecation warning by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4709](https://github.com/gradio-app/gradio/pull/4709) +- Ensure Audio autoplays works when `autoplay=True` and the video source is dynamically updated [@pngwn](https://github.com/pngwn) in [PR 4705](https://github.com/gradio-app/gradio/pull/4705) +- When an error modal is shown in spaces, ensure we scroll to the top so it can be seen by [@pngwn](https://github.com/pngwn) in [PR 4712](https://github.com/gradio-app/gradio/pull/4712) +- Update depedencies by [@pngwn](https://github.com/pngwn) in [PR 4675](https://github.com/gradio-app/gradio/pull/4675) +- Fixes `gr.Dropdown` being cutoff at the bottom by [@abidlabs](https://github.com/abidlabs) in [PR 4691](https://github.com/gradio-app/gradio/pull/4691). +- Scroll top when clicking "View API" in spaces by [@pngwn](https://github.com/pngwn) in [PR 4714](https://github.com/gradio-app/gradio/pull/4714) +- Fix bug where `show_label` was hiding the entire component for `gr.Label` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4713](https://github.com/gradio-app/gradio/pull/4713) +- Don't crash when uploaded image has broken EXIF data, by [@akx](https://github.com/akx) in [PR 4764](https://github.com/gradio-app/gradio/pull/4764) +- Place toast messages at the top of the screen by [@pngwn](https://github.com/pngwn) in [PR 4796](https://github.com/gradio-app/gradio/pull/4796) +- Fix regressed styling of Login page when auth is enabled by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4797](https://github.com/gradio-app/gradio/pull/4797) +- Prevent broken scrolling to output on Spaces by [@aliabid94](https://github.com/aliabid94) in [PR 4822](https://github.com/gradio-app/gradio/pull/4822) + +### Other Changes: + +- Add `.git-blame-ignore-revs` by [@akx](https://github.com/akx) in [PR 4586](https://github.com/gradio-app/gradio/pull/4586) +- Update frontend dependencies in [PR 4601](https://github.com/gradio-app/gradio/pull/4601) +- Use `typing.Literal` where possible in gradio library and client by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4608](https://github.com/gradio-app/gradio/pull/4608) +- Remove unnecessary mock json files for frontend E2E tests by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4625](https://github.com/gradio-app/gradio/pull/4625) +- Update dependencies by [@pngwn](https://github.com/pngwn) in [PR 4643](https://github.com/gradio-app/gradio/pull/4643) +- The theme builder now launches successfully, and the API docs are cleaned up. By [@abidlabs](https://github.com/aliabid94) in [PR 4683](https://github.com/gradio-app/gradio/pull/4683) +- Remove `cleared_value` from some components as its no longer used internally by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4685](https://github.com/gradio-app/gradio/pull/4685) +- Better errors when you define two Blocks and reference components in one Blocks from the events in the other Blocks [@abidlabs](https://github.com/abidlabs) in [PR 4738](https://github.com/gradio-app/gradio/pull/4738). +- Better message when share link is not created by [@abidlabs](https://github.com/abidlabs) in [PR 4773](https://github.com/gradio-app/gradio/pull/4773). +- Improve accessibility around selected images in gr.Gallery component by [@hannahblair](https://github.com/hannahblair) in [PR 4790](https://github.com/gradio-app/gradio/pull/4790) + +### Breaking Changes: + +[PR 4683](https://github.com/gradio-app/gradio/pull/4683) removes the explict named endpoint "load_examples" from gr.Interface that was introduced in [PR 4456](https://github.com/gradio-app/gradio/pull/4456). + +## 3.35.2 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fix chatbot streaming by [@aliabid94](https://github.com/aliabid94) in [PR 4537](https://github.com/gradio-app/gradio/pull/4537) +- Fix chatbot height and scrolling by [@aliabid94](https://github.com/aliabid94) in [PR 4540](https://github.com/gradio-app/gradio/pull/4540) + +### Other Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +## 3.35.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fix chatbot streaming by [@aliabid94](https://github.com/aliabid94) in [PR 4537](https://github.com/gradio-app/gradio/pull/4537) +- Fix error modal position and text size by [@pngwn](https://github.com/pngwn) in [PR 4538](https://github.com/gradio-app/gradio/pull/4538). + +### Other Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +## 3.35.0 + +### New Features: + +- A `gr.ClearButton` which allows users to easily clear the values of components by [@abidlabs](https://github.com/abidlabs) in [PR 4456](https://github.com/gradio-app/gradio/pull/4456) + +Example usage: + +```py +import gradio as gr + +with gr.Blocks() as demo: + chatbot = gr.Chatbot([("Hello", "How are you?")]) + with gr.Row(): + textbox = gr.Textbox(scale=3, interactive=True) + gr.ClearButton([textbox, chatbot], scale=1) + +demo.launch() +``` + +- Min and max value for gr.Number by [@artegoser](https://github.com/artegoser) and [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3991](https://github.com/gradio-app/gradio/pull/3991) +- Add `start_recording` and `stop_recording` events to `Video` and `Audio` components by [@pngwn](https://github.com/pngwn) in [PR 4422](https://github.com/gradio-app/gradio/pull/4422) +- Allow any function to generate an error message and allow multiple messages to appear at a time. Other error modal improvements such as auto dismiss after a time limit and a new layout on mobile [@pngwn](https://github.com/pngwn) in [PR 4459](https://github.com/gradio-app/gradio/pull/4459). +- Add `autoplay` kwarg to `Video` and `Audio` components by [@pngwn](https://github.com/pngwn) in [PR 4453](https://github.com/gradio-app/gradio/pull/4453) +- Add `allow_preview` parameter to `Gallery` to control whether a detailed preview is displayed on click by + [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4470](https://github.com/gradio-app/gradio/pull/4470) +- Add `latex_delimiters` parameter to `Chatbot` to control the delimiters used for LaTeX and to disable LaTeX in the `Chatbot` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4516](https://github.com/gradio-app/gradio/pull/4516) +- Can now issue `gr.Warning` and `gr.Info` modals. Simply put the code `gr.Warning("Your warning message")` or `gr.Info("Your info message")` as a standalone line in your function. By [@aliabid94](https://github.com/aliabid94) in [PR 4518](https://github.com/gradio-app/gradio/pull/4518). + +Example: + +```python +def start_process(name): + gr.Info("Starting process") + if name is None: + gr.Warning("Name is empty") + ... + if success == False: + raise gr.Error("Process failed") +``` + +### Bug Fixes: + +- Add support for PAUSED state in the JS client by [@abidlabs](https://github.com/abidlabs) in [PR 4438](https://github.com/gradio-app/gradio/pull/4438) +- Ensure Tabs only occupy the space required by [@pngwn](https://github.com/pngwn) in [PR 4419](https://github.com/gradio-app/gradio/pull/4419) +- Ensure components have the correct empty sizes to prevent empty containers from collapsing by [@pngwn](https://github.com/pngwn) in [PR 4447](https://github.com/gradio-app/gradio/pull/4447). +- Frontend code no longer crashes when there is a relative URL in an `` element, by [@akx](https://github.com/akx) in [PR 4449](https://github.com/gradio-app/gradio/pull/4449). +- Fix bug where setting `format='mp4'` on a video component would cause the function to error out if the uploaded video was not playable by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4467](https://github.com/gradio-app/gradio/pull/4467) +- Fix `_js` parameter to work even without backend function, by [@aliabid94](https://github.com/aliabid94) in [PR 4486](https://github.com/gradio-app/gradio/pull/4486). +- Fix new line issue with `gr.Chatbot()` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4491](https://github.com/gradio-app/gradio/pull/4491) +- Fixes issue with Clear button not working for `Label` component by [@abidlabs](https://github.com/abidlabs) in [PR 4456](https://github.com/gradio-app/gradio/pull/4456) +- Restores the ability to pass in a tuple (sample rate, audio array) to gr.Audio() by [@abidlabs](https://github.com/abidlabs) in [PR 4525](https://github.com/gradio-app/gradio/pull/4525) +- Ensure code is correctly formatted and copy button is always present in Chatbot by [@pngwn](https://github.com/pngwn) in [PR 4527](https://github.com/gradio-app/gradio/pull/4527) +- `show_label` will not automatically be set to `True` in `gr.BarPlot.update` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4531](https://github.com/gradio-app/gradio/pull/4531) +- `gr.BarPlot` group text now respects darkmode by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4531](https://github.com/gradio-app/gradio/pull/4531) +- Fix dispatched errors from within components [@aliabid94](https://github.com/aliabid94) in [PR 4786](https://github.com/gradio-app/gradio/pull/4786) + +### Other Changes: + +- Change styling of status and toast error components by [@hannahblair](https://github.com/hannahblair) in [PR 4454](https://github.com/gradio-app/gradio/pull/4454). +- Clean up unnecessary `new Promise()`s by [@akx](https://github.com/akx) in [PR 4442](https://github.com/gradio-app/gradio/pull/4442). +- Minor UI cleanup for Examples and Dataframe components [@aliabid94](https://github.com/aliabid94) in [PR 4455](https://github.com/gradio-app/gradio/pull/4455). +- Minor UI cleanup for Examples and Dataframe components [@aliabid94](https://github.com/aliabid94) in [PR 4455](https://github.com/gradio-app/gradio/pull/4455). +- Add Catalan translation [@jordimas](https://github.com/jordimas) in [PR 4483](https://github.com/gradio-app/gradio/pull/4483). +- The API endpoint that loads examples upon click has been given an explicit name ("/load_examples") by [@abidlabs](https://github.com/abidlabs) in [PR 4456](https://github.com/gradio-app/gradio/pull/4456). +- Allows configuration of FastAPI app when calling `mount_gradio_app`, by [@charlesfrye](https://github.com/charlesfrye) in [PR4519](https://github.com/gradio-app/gradio/pull/4519). + +### Breaking Changes: + +- The behavior of the `Clear` button has been changed for `Slider`, `CheckboxGroup`, `Radio`, `Dropdown` components by [@abidlabs](https://github.com/abidlabs) in [PR 4456](https://github.com/gradio-app/gradio/pull/4456). The Clear button now sets the value of these components to be empty as opposed to the original default set by the developer. This is to make them in line with the rest of the Gradio components. +- Python 3.7 end of life is June 27 2023. Gradio will no longer support python 3.7 by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4484](https://github.com/gradio-app/gradio/pull/4484) +- Removed `$` as a default LaTeX delimiter for the `Chatbot` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4516](https://github.com/gradio-app/gradio/pull/4516). The specific LaTeX delimeters can be set using the new `latex_delimiters` parameter in `Chatbot`. + +## 3.34.0 + +### New Features: + +- The `gr.UploadButton` component now supports the `variant` and `interactive` parameters by [@abidlabs](https://github.com/abidlabs) in [PR 4436](https://github.com/gradio-app/gradio/pull/4436). + +### Bug Fixes: + +- Remove target="\_blank" override on anchor tags with internal targets by [@hannahblair](https://github.com/hannahblair) in [PR 4405](https://github.com/gradio-app/gradio/pull/4405) +- Fixed bug where `gr.File(file_count='multiple')` could not be cached as output by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4421](https://github.com/gradio-app/gradio/pull/4421) +- Restricts the domains that can be proxied via `/proxy` route by [@abidlabs](https://github.com/abidlabs) in [PR 4406](https://github.com/gradio-app/gradio/pull/4406). +- Fixes issue where `gr.UploadButton` could not be used to upload the same file twice by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4437](https://github.com/gradio-app/gradio/pull/4437) +- Fixes bug where `/proxy` route was being incorrectly constructed by the frontend by [@abidlabs](https://github.com/abidlabs) in [PR 4430](https://github.com/gradio-app/gradio/pull/4430). +- Fix z-index of status component by [@hannahblair](https://github.com/hannahblair) in [PR 4429](https://github.com/gradio-app/gradio/pull/4429) +- Fix video rendering in Safari by [@aliabid94](https://github.com/aliabid94) in [PR 4433](https://github.com/gradio-app/gradio/pull/4433). +- The output directory for files downloaded when calling Blocks as a function is now set to a temporary directory by default (instead of the working directory in some cases) by [@abidlabs](https://github.com/abidlabs) in [PR 4501](https://github.com/gradio-app/gradio/pull/4501) + +### Other Changes: + +- When running on Spaces, handler functions will be transformed by the [PySpaces](https://pypi.org/project/spaces/) library in order to make them work with specific hardware. It will have no effect on standalone Gradio apps or regular Gradio Spaces and can be globally deactivated as follows : `import spaces; spaces.disable_gradio_auto_wrap()` by [@cbensimon](https://github.com/cbensimon) in [PR 4389](https://github.com/gradio-app/gradio/pull/4389). +- Deprecated `.style` parameter and moved arguments to constructor. Added support for `.update()` to all arguments initially in style. Added `scale` and `min_width` support to every Component. By [@aliabid94](https://github.com/aliabid94) in [PR 4374](https://github.com/gradio-app/gradio/pull/4374) + +### Breaking Changes: + +No changes to highlight. + +## 3.33.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Allow `every` to work with generators by [@dkjshk](https://github.com/dkjshk) in [PR 4434](https://github.com/gradio-app/gradio/pull/4434) +- Fix z-index of status component by [@hannahblair](https://github.com/hannahblair) in [PR 4429](https://github.com/gradio-app/gradio/pull/4429) +- Allow gradio to work offline, by [@aliabid94](https://github.com/aliabid94) in [PR 4398](https://github.com/gradio-app/gradio/pull/4398). +- Fixed `validate_url` to check for 403 errors and use a GET request in place of a HEAD by [@alvindaiyan](https://github.com/alvindaiyan) in [PR 4388](https://github.com/gradio-app/gradio/pull/4388). + +### Other Changes: + +- More explicit error message when share link binary is blocked by antivirus by [@abidlabs](https://github.com/abidlabs) in [PR 4380](https://github.com/gradio-app/gradio/pull/4380). + +### Breaking Changes: + +No changes to highlight. + +## 3.33.0 + +### New Features: + +- Introduced `gradio deploy` to launch a Gradio app to Spaces directly from your terminal. By [@aliabid94](https://github.com/aliabid94) in [PR 4033](https://github.com/gradio-app/gradio/pull/4033). +- Introduce `show_progress='corner'` argument to event listeners, which will not cover the output components with the progress animation, but instead show it in the corner of the components. By [@aliabid94](https://github.com/aliabid94) in [PR 4396](https://github.com/gradio-app/gradio/pull/4396). + +### Bug Fixes: + +- Fix bug where Label change event was triggering itself by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4371](https://github.com/gradio-app/gradio/pull/4371) +- Make `Blocks.load` behave like other event listeners (allows chaining `then` off of it) [@anentropic](https://github.com/anentropic/) in [PR 4304](https://github.com/gradio-app/gradio/pull/4304) +- Respect `interactive=True` in output components of a `gr.Interface` by [@abidlabs](https://github.com/abidlabs) in [PR 4356](https://github.com/gradio-app/gradio/pull/4356). +- Remove unused frontend code by [@akx](https://github.com/akx) in [PR 4275](https://github.com/gradio-app/gradio/pull/4275) +- Fixes favicon path on Windows by [@abidlabs](https://github.com/abidlabs) in [PR 4369](https://github.com/gradio-app/gradio/pull/4369). +- Prevent path traversal in `/file` routes by [@abidlabs](https://github.com/abidlabs) in [PR 4370](https://github.com/gradio-app/gradio/pull/4370). +- Do not send HF token to other domains via `/proxy` route by [@abidlabs](https://github.com/abidlabs) in [PR 4368](https://github.com/gradio-app/gradio/pull/4368). +- Replace default `markedjs` sanitize function with DOMPurify sanitizer for `gr.Chatbot()` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4360](https://github.com/gradio-app/gradio/pull/4360) +- Prevent the creation of duplicate copy buttons in the chatbot and ensure copy buttons work in non-secure contexts by [@binary-husky](https://github.com/binary-husky) in [PR 4350](https://github.com/gradio-app/gradio/pull/4350). + +### Other Changes: + +- Remove flicker of loading bar by adding opacity transition, by [@aliabid94](https://github.com/aliabid94) in [PR 4349](https://github.com/gradio-app/gradio/pull/4349). +- Performance optimization in the frontend's Blocks code by [@akx](https://github.com/akx) in [PR 4334](https://github.com/gradio-app/gradio/pull/4334) +- Upgrade the pnpm lock file format version from v6.0 to v6.1 by [@whitphx](https://github.com/whitphx) in [PR 4393](https://github.com/gradio-app/gradio/pull/4393) + +### Breaking Changes: + +- The `/file=` route no longer allows accessing dotfiles or files in "dot directories" by [@akx](https://github.com/akx) in [PR 4303](https://github.com/gradio-app/gradio/pull/4303) + +## 3.32.0 + +### New Features: + +- `Interface.launch()` and `Blocks.launch()` now accept an `app_kwargs` argument to allow customizing the configuration of the underlying FastAPI app, by [@akx](https://github.com/akx) in [PR 4282](https://github.com/gradio-app/gradio/pull/4282) + +### Bug Fixes: + +- Fixed Gallery/AnnotatedImage components not respecting GRADIO_DEFAULT_DIR variable by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4256](https://github.com/gradio-app/gradio/pull/4256) +- Fixed Gallery/AnnotatedImage components resaving identical images by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4256](https://github.com/gradio-app/gradio/pull/4256) +- Fixed Audio/Video/File components creating empty tempfiles on each run by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4256](https://github.com/gradio-app/gradio/pull/4256) +- Fixed the behavior of the `run_on_click` parameter in `gr.Examples` by [@abidlabs](https://github.com/abidlabs) in [PR 4258](https://github.com/gradio-app/gradio/pull/4258). +- Ensure error modal displays when the queue is enabled by [@pngwn](https://github.com/pngwn) in [PR 4273](https://github.com/gradio-app/gradio/pull/4273) +- Ensure js client respcts the full root when making requests to the server by [@pngwn](https://github.com/pngwn) in [PR 4271](https://github.com/gradio-app/gradio/pull/4271) + +### Other Changes: + +- Refactor web component `initial_height` attribute by [@whitphx](https://github.com/whitphx) in [PR 4223](https://github.com/gradio-app/gradio/pull/4223) +- Relocate `mount_css` fn to remove circular dependency [@whitphx](https://github.com/whitphx) in [PR 4222](https://github.com/gradio-app/gradio/pull/4222) +- Upgrade Black to 23.3 by [@akx](https://github.com/akx) in [PR 4259](https://github.com/gradio-app/gradio/pull/4259) +- Add frontend LaTeX support in `gr.Chatbot()` using `KaTeX` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4285](https://github.com/gradio-app/gradio/pull/4285). + +### Breaking Changes: + +No changes to highlight. + +## 3.31.0 + +### New Features: + +- The reloader command (`gradio app.py`) can now accept command line arguments by [@micky2be](https://github.com/micky2be) in [PR 4119](https://github.com/gradio-app/gradio/pull/4119) +- Added `format` argument to `Audio` component by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4178](https://github.com/gradio-app/gradio/pull/4178) +- Add JS client code snippets to use via api page by [@aliabd](https://github.com/aliabd) in [PR 3927](https://github.com/gradio-app/gradio/pull/3927). +- Update to the JS client by [@pngwn](https://github.com/pngwn) in [PR 4202](https://github.com/gradio-app/gradio/pull/4202) + +### Bug Fixes: + +- Fix "TypeError: issubclass() arg 1 must be a class" When use Optional[Types] by [@lingfengchencn](https://github.com/lingfengchencn) in [PR 4200](https://github.com/gradio-app/gradio/pull/4200). +- Gradio will no longer send any analytics or call home if analytics are disabled with the GRADIO_ANALYTICS_ENABLED environment variable. By [@akx](https://github.com/akx) in [PR 4194](https://github.com/gradio-app/gradio/pull/4194) and [PR 4236](https://github.com/gradio-app/gradio/pull/4236) +- The deprecation warnings for kwargs now show the actual stack level for the invocation, by [@akx](https://github.com/akx) in [PR 4203](https://github.com/gradio-app/gradio/pull/4203). +- Fix "TypeError: issubclass() arg 1 must be a class" When use Optional[Types] by [@lingfengchencn](https://github.com/lingfengchencn) in [PR 4200](https://github.com/gradio-app/gradio/pull/4200). +- Ensure cancelling functions work correctly by [@pngwn](https://github.com/pngwn) in [PR 4225](https://github.com/gradio-app/gradio/pull/4225) +- Fixes a bug with typing.get_type_hints() on Python 3.9 by [@abidlabs](https://github.com/abidlabs) in [PR 4228](https://github.com/gradio-app/gradio/pull/4228). +- Fixes JSONDecodeError by [@davidai](https://github.com/davidai) in [PR 4241](https://github.com/gradio-app/gradio/pull/4241) +- Fix `chatbot_dialogpt` demo by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4238](https://github.com/gradio-app/gradio/pull/4238). + +### Other Changes: + +- Change `gr.Chatbot()` markdown parsing to frontend using `marked` library and `prism` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4150](https://github.com/gradio-app/gradio/pull/4150) +- Update the js client by [@pngwn](https://github.com/pngwn) in [PR 3899](https://github.com/gradio-app/gradio/pull/3899) +- Fix documentation for the shape of the numpy array produced by the `Image` component by [@der3318](https://github.com/der3318) in [PR 4204](https://github.com/gradio-app/gradio/pull/4204). +- Updates the timeout for websocket messaging from 1 second to 5 seconds by [@abidlabs](https://github.com/abidlabs) in [PR 4235](https://github.com/gradio-app/gradio/pull/4235) + +### Breaking Changes: + +No changes to highlight. + +## 3.30.0 + +### New Features: + +- Adds a `root_path` parameter to `launch()` that allows running Gradio applications on subpaths (e.g. www.example.com/app) behind a proxy, by [@abidlabs](https://github.com/abidlabs) in [PR 4133](https://github.com/gradio-app/gradio/pull/4133) +- Fix dropdown change listener to trigger on change when updated as an output by [@aliabid94](https://github.com/aliabid94) in [PR 4128](https://github.com/gradio-app/gradio/pull/4128). +- Add `.input` event listener, which is only triggered when a user changes the component value (as compared to `.change`, which is also triggered when a component updates as the result of a function trigger), by [@aliabid94](https://github.com/aliabid94) in [PR 4157](https://github.com/gradio-app/gradio/pull/4157). + +### Bug Fixes: + +- Records username when flagging by [@abidlabs](https://github.com/abidlabs) in [PR 4135](https://github.com/gradio-app/gradio/pull/4135) +- Fix website build issue by [@aliabd](https://github.com/aliabd) in [PR 4142](https://github.com/gradio-app/gradio/pull/4142) +- Fix lang agnostic type info for `gr.File(file_count='multiple')` output components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4153](https://github.com/gradio-app/gradio/pull/4153) + +### Other Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +## 3.29.0 + +### New Features: + +- Returning language agnostic types in the `/info` route by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4039](https://github.com/gradio-app/gradio/pull/4039) + +### Bug Fixes: + +- Allow users to upload audio files in Audio component on iOS by by [@aliabid94](https://github.com/aliabid94) in [PR 4071](https://github.com/gradio-app/gradio/pull/4071). +- Fixes the gradio theme builder error that appeared on launch by [@aliabid94](https://github.com/aliabid94) and [@abidlabs](https://github.com/abidlabs) in [PR 4080](https://github.com/gradio-app/gradio/pull/4080) +- Keep Accordion content in DOM by [@aliabid94](https://github.com/aliabid94) in [PR 4070](https://github.com/gradio-app/gradio/pull/4073) +- Fixed bug where type hints in functions caused the event handler to crash by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4068](https://github.com/gradio-app/gradio/pull/4068) +- Fix dropdown default value not appearing by [@aliabid94](https://github.com/aliabid94) in [PR 4072](https://github.com/gradio-app/gradio/pull/4072). +- Soft theme label color fix by [@aliabid94](https://github.com/aliabid94) in [PR 4070](https://github.com/gradio-app/gradio/pull/4070) +- Fix `gr.Slider` `release` event not triggering on mobile by [@space-nuko](https://github.com/space-nuko) in [PR 4098](https://github.com/gradio-app/gradio/pull/4098) +- Removes extraneous `State` component info from the `/info` route by [@abidlabs](https://github.com/freddyaboulton) in [PR 4107](https://github.com/gradio-app/gradio/pull/4107) +- Make .then() work even if first event fails by [@aliabid94](https://github.com/aliabid94) in [PR 4115](https://github.com/gradio-app/gradio/pull/4115). + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Allow users to submit with enter in Interfaces with textbox / number inputs [@aliabid94](https://github.com/aliabid94) in [PR 4090](https://github.com/gradio-app/gradio/pull/4090). +- Updates gradio's requirements.txt to requires uvicorn>=0.14.0 by [@abidlabs](https://github.com/abidlabs) in [PR 4086](https://github.com/gradio-app/gradio/pull/4086) +- Updates some error messaging by [@abidlabs](https://github.com/abidlabs) in [PR 4086](https://github.com/gradio-app/gradio/pull/4086) +- Renames simplified Chinese translation file from `zh-cn.json` to `zh-CN.json` by [@abidlabs](https://github.com/abidlabs) in [PR 4086](https://github.com/gradio-app/gradio/pull/4086) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.28.3 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fixes issue with indentation in `gr.Code()` component with streaming by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4043](https://github.com/gradio-app/gradio/pull/4043) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.28.2 + +### Bug Fixes + +- Code component visual updates by [@pngwn](https://github.com/pngwn) in [PR 4051](https://github.com/gradio-app/gradio/pull/4051) + +### New Features: + +- Add support for `visual-question-answering`, `document-question-answering`, and `image-to-text` using `gr.Interface.load("models/...")` and `gr.Interface.from_pipeline` by [@osanseviero](https://github.com/osanseviero) in [PR 3887](https://github.com/gradio-app/gradio/pull/3887) +- Add code block support in `gr.Chatbot()`, by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 4048](https://github.com/gradio-app/gradio/pull/4048) +- Adds the ability to blocklist filepaths (and also improves the allowlist mechanism) by [@abidlabs](https://github.com/abidlabs) in [PR 4047](https://github.com/gradio-app/gradio/pull/4047). +- Adds the ability to specify the upload directory via an environment variable by [@abidlabs](https://github.com/abidlabs) in [PR 4047](https://github.com/gradio-app/gradio/pull/4047). + +### Bug Fixes: + +- Fixes issue with `matplotlib` not rendering correctly if the backend was not set to `Agg` by [@abidlabs](https://github.com/abidlabs) in [PR 4029](https://github.com/gradio-app/gradio/pull/4029) +- Fixes bug where rendering the same `gr.State` across different Interfaces/Blocks within larger Blocks would not work by [@abidlabs](https://github.com/abidlabs) in [PR 4030](https://github.com/gradio-app/gradio/pull/4030) +- Code component visual updates by [@pngwn](https://github.com/pngwn) in [PR 4051](https://github.com/gradio-app/gradio/pull/4051) + +### Documentation Changes: + +- Adds a Guide on how to use the Python Client within a FastAPI app, by [@abidlabs](https://github.com/abidlabs) in [PR 3892](https://github.com/gradio-app/gradio/pull/3892) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +- `gr.HuggingFaceDatasetSaver` behavior changed internally. The `flagging/` folder is not a `.git/` folder anymore when using it. `organization` parameter is now ignored in favor of passing a full dataset id as `dataset_name` (e.g. `"username/my-dataset"`). +- New lines (`\n`) are not automatically converted to `
` in `gr.Markdown()` or `gr.Chatbot()`. For multiple new lines, a developer must add multiple `
` tags. + +### Full Changelog: + +- Safer version of `gr.HuggingFaceDatasetSaver` using HTTP methods instead of git pull/push by [@Wauplin](https://github.com/Wauplin) in [PR 3973](https://github.com/gradio-app/gradio/pull/3973) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.28.1 + +### New Features: + +- Add a "clear mask" button to `gr.Image` sketch modes, by [@space-nuko](https://github.com/space-nuko) in [PR 3615](https://github.com/gradio-app/gradio/pull/3615) + +### Bug Fixes: + +- Fix dropdown default value not appearing by [@aliabid94](https://github.com/aliabid94) in [PR 3996](https://github.com/gradio-app/gradio/pull/3996). +- Fix faded coloring of output textboxes in iOS / Safari by [@aliabid94](https://github.com/aliabid94) in [PR 3993](https://github.com/gradio-app/gradio/pull/3993) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +- CI: Simplified Python CI workflow by [@akx](https://github.com/akx) in [PR 3982](https://github.com/gradio-app/gradio/pull/3982) +- Upgrade pyright to 1.1.305 by [@akx](https://github.com/akx) in [PR 4042](https://github.com/gradio-app/gradio/pull/4042) +- More Ruff rules are enabled and lint errors fixed by [@akx](https://github.com/akx) in [PR 4038](https://github.com/gradio-app/gradio/pull/4038) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.28.0 + +### Bug Fixes: + +- Fix duplicate play commands in full-screen mode of 'video'. by [@tomchang25](https://github.com/tomchang25) in [PR 3968](https://github.com/gradio-app/gradio/pull/3968). +- Fix the issue of the UI stuck caused by the 'selected' of DataFrame not being reset. by [@tomchang25](https://github.com/tomchang25) in [PR 3916](https://github.com/gradio-app/gradio/pull/3916). +- Fix issue where `gr.Video()` would not work inside a `gr.Tab()` by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3891](https://github.com/gradio-app/gradio/pull/3891) +- Fixed issue with old_value check in File. by [@tomchang25](https://github.com/tomchang25) in [PR 3859](https://github.com/gradio-app/gradio/pull/3859). +- Fixed bug where all bokeh plots appeared in the same div by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3896](https://github.com/gradio-app/gradio/pull/3896) +- Fixed image outputs to automatically take full output image height, unless explicitly set, by [@aliabid94](https://github.com/aliabid94) in [PR 3905](https://github.com/gradio-app/gradio/pull/3905) +- Fix issue in `gr.Gallery()` where setting height causes aspect ratio of images to collapse by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3830](https://github.com/gradio-app/gradio/pull/3830) +- Fix issue where requesting for a non-existing file would trigger a 500 error by [@micky2be](https://github.com/micky2be) in `[PR 3895](https://github.com/gradio-app/gradio/pull/3895)`. +- Fix bugs with abspath about symlinks, and unresolvable path on Windows by [@micky2be](https://github.com/micky2be) in `[PR 3895](https://github.com/gradio-app/gradio/pull/3895)`. +- Fixes type in client `Status` enum by [@10zinten](https://github.com/10zinten) in [PR 3931](https://github.com/gradio-app/gradio/pull/3931) +- Fix `gr.ChatBot` to handle image url [tye-singwa](https://github.com/tye-signwa) in [PR 3953](https://github.com/gradio-app/gradio/pull/3953) +- Move Google Tag Manager related initialization code to analytics-enabled block by [@akx](https://github.com/akx) in [PR 3956](https://github.com/gradio-app/gradio/pull/3956) +- Fix bug where port was not reused if the demo was closed and then re-launched by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3896](https://github.com/gradio-app/gradio/pull/3959) +- Fixes issue where dropdown does not position itself at selected element when opened [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3639](https://github.com/gradio-app/gradio/pull/3639) + +### Documentation Changes: + +- Make use of `gr` consistent across the docs by [@duerrsimon](https://github.com/duerrsimon) in [PR 3901](https://github.com/gradio-app/gradio/pull/3901) +- Fixed typo in theming-guide.md by [@eltociear](https://github.com/eltociear) in [PR 3952](https://github.com/gradio-app/gradio/pull/3952) + +### Testing and Infrastructure Changes: + +- CI: Python backend lint is only run once, by [@akx](https://github.com/akx) in [PR 3960](https://github.com/gradio-app/gradio/pull/3960) +- Format invocations and concatenations were replaced by f-strings where possible by [@akx](https://github.com/akx) in [PR 3984](https://github.com/gradio-app/gradio/pull/3984) +- Linting rules were made more strict and issues fixed by [@akx](https://github.com/akx) in [PR 3979](https://github.com/gradio-app/gradio/pull/3979). + +### Breaking Changes: + +- Some re-exports in `gradio.themes` utilities (introduced in 3.24.0) have been eradicated. + By [@akx](https://github.com/akx) in [PR 3958](https://github.com/gradio-app/gradio/pull/3958) + +### Full Changelog: + +- Add DESCRIPTION.md to image_segmentation demo by [@aliabd](https://github.com/aliabd) in [PR 3866](https://github.com/gradio-app/gradio/pull/3866) +- Fix error in running `gr.themes.builder()` by [@deepkyu](https://github.com/deepkyu) in [PR 3869](https://github.com/gradio-app/gradio/pull/3869) +- Fixed a JavaScript TypeError when loading custom JS with `_js` and setting `outputs` to `None` in `gradio.Blocks()` by [@DavG25](https://github.com/DavG25) in [PR 3883](https://github.com/gradio-app/gradio/pull/3883) +- Fixed bg_background_fill theme property to expand to whole background, block_radius to affect form elements as well, and added block_label_shadow theme property by [@aliabid94](https://github.com/aliabid94) in [PR 3590](https://github.com/gradio-app/gradio/pull/3590) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.27.0 + +### New Features: + +###### AnnotatedImage Component + +New AnnotatedImage component allows users to highlight regions of an image, either by providing bounding boxes, or 0-1 pixel masks. This component is useful for tasks such as image segmentation, object detection, and image captioning. + +![AnnotatedImage screenshot](https://user-images.githubusercontent.com/7870876/232142720-86e0020f-beaf-47b9-a843-689c9621f09c.gif) + +Example usage: + +```python +with gr.Blocks() as demo: + img = gr.Image() + img_section = gr.AnnotatedImage() + def mask(img): + top_left_corner = [0, 0, 20, 20] + random_mask = np.random.randint(0, 2, img.shape[:2]) + return (img, [(top_left_corner, "left corner"), (random_mask, "random")]) + img.change(mask, img, img_section) +``` + +See the [image_segmentation demo](https://github.com/gradio-app/gradio/tree/main/demo/image_segmentation) for a full example. By [@aliabid94](https://github.com/aliabid94) in [PR 3836](https://github.com/gradio-app/gradio/pull/3836) + +### Bug Fixes: + +No changes to highlight. + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.26.0 + +### New Features: + +###### `Video` component supports subtitles + +- Allow the video component to accept subtitles as input, by [@tomchang25](https://github.com/tomchang25) in [PR 3673](https://github.com/gradio-app/gradio/pull/3673). To provide subtitles, simply return a tuple consisting of `(path_to_video, path_to_subtitles)` from your function. Both `.srt` and `.vtt` formats are supported: + +```py +with gr.Blocks() as demo: + gr.Video(("video.mp4", "captions.srt")) +``` + +### Bug Fixes: + +- Fix code markdown support in `gr.Chatbot()` component by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3816](https://github.com/gradio-app/gradio/pull/3816) + +### Documentation Changes: + +- Updates the "view API" page in Gradio apps to use the `gradio_client` library by [@aliabd](https://github.com/aliabd) in [PR 3765](https://github.com/gradio-app/gradio/pull/3765) + +- Read more about how to use the `gradio_client` library here: https://gradio.app/getting-started-with-the-python-client/ + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.25.0 + +### New Features: + +- Improve error messages when number of inputs/outputs to event handlers mismatch, by [@space-nuko](https://github.com/space-nuko) in [PR 3519](https://github.com/gradio-app/gradio/pull/3519) + +- Add `select` listener to Images, allowing users to click on any part of an image and get the coordinates of the click by [@aliabid94](https://github.com/aliabid94) in [PR 3786](https://github.com/gradio-app/gradio/pull/3786). + +```python +with gr.Blocks() as demo: + img = gr.Image() + textbox = gr.Textbox() + + def select_handler(img, evt: gr.SelectData): + selected_pixel = img[evt.index[1], evt.index[0]] + return f"Selected pixel: {selected_pixel}" + + img.select(select_handler, img, textbox) +``` + +![Recording 2023-04-08 at 17 44 39](https://user-images.githubusercontent.com/7870876/230748572-90a2a8d5-116d-4769-bb53-5516555fbd0f.gif) + +### Bug Fixes: + +- Increase timeout for sending analytics data by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3647](https://github.com/gradio-app/gradio/pull/3647) +- Fix bug where http token was not accessed over websocket connections by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3735](https://github.com/gradio-app/gradio/pull/3735) +- Add ability to specify `rows`, `columns` and `object-fit` in `style()` for `gr.Gallery()` component by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3586](https://github.com/gradio-app/gradio/pull/3586) +- Fix bug where recording an audio file through the microphone resulted in a corrupted file name by [@abidlabs](https://github.com/abidlabs) in [PR 3770](https://github.com/gradio-app/gradio/pull/3770) +- Added "ssl_verify" to blocks.launch method to allow for use of self-signed certs by [@garrettsutula](https://github.com/garrettsutula) in [PR 3873](https://github.com/gradio-app/gradio/pull/3873) +- Fix bug where iterators where not being reset for processes that terminated early by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3777](https://github.com/gradio-app/gradio/pull/3777) +- Fix bug where the upload button was not properly handling the `file_count='multiple'` case by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3782](https://github.com/gradio-app/gradio/pull/3782) +- Fix bug where use Via API button was giving error by [@Devang-C](https://github.com/Devang-C) in [PR 3783](https://github.com/gradio-app/gradio/pull/3783) + +### Documentation Changes: + +- Fix invalid argument docstrings, by [@akx](https://github.com/akx) in [PR 3740](https://github.com/gradio-app/gradio/pull/3740) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fixed IPv6 listening to work with bracket [::1] notation, by [@dsully](https://github.com/dsully) in [PR 3695](https://github.com/gradio-app/gradio/pull/3695) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.24.1 + +### New Features: + +- No changes to highlight. + +### Bug Fixes: + +- Fixes Chatbot issue where new lines were being created every time a message was sent back and forth by [@aliabid94](https://github.com/aliabid94) in [PR 3717](https://github.com/gradio-app/gradio/pull/3717). +- Fixes data updating in DataFrame invoking a `select` event once the dataframe has been selected. By [@yiyuezhuo](https://github.com/yiyuezhuo) in [PR 3861](https://github.com/gradio-app/gradio/pull/3861) +- Fixes false positive warning which is due to too strict type checking by [@yiyuezhuo](https://github.com/yiyuezhuo) in [PR 3837](https://github.com/gradio-app/gradio/pull/3837). + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.24.0 + +### New Features: + +- Trigger the release event when Slider number input is released or unfocused by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3589](https://github.com/gradio-app/gradio/pull/3589) +- Created Theme Builder, which allows users to create themes without writing any code, by [@aliabid94](https://github.com/aliabid94) in [PR 3664](https://github.com/gradio-app/gradio/pull/3664). Launch by: + + ```python + import gradio as gr + gr.themes.builder() + ``` + + ![Theme Builder](https://user-images.githubusercontent.com/7870876/228204929-d71cbba5-69c2-45b3-bd20-e3a201d98b12.png) + +- The `Dropdown` component now has a `allow_custom_value` parameter that lets users type in custom values not in the original list of choices. +- The `Colorpicker` component now has a `.blur()` event + +###### Added a download button for videos! 📥 + +![download_video](https://user-images.githubusercontent.com/41651716/227009612-9bc5fb72-2a44-4c55-9b7b-a0fa098e7f25.gif) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3581](https://github.com/gradio-app/gradio/pull/3581). + +- Trigger the release event when Slider number input is released or unfocused by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3589](https://github.com/gradio-app/gradio/pull/3589) + +### Bug Fixes: + +- Fixed bug where text for altair plots was not legible in dark mode by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3555](https://github.com/gradio-app/gradio/pull/3555) +- Fixes `Chatbot` and `Image` components so that files passed during processing are added to a directory where they can be served from, by [@abidlabs](https://github.com/abidlabs) in [PR 3523](https://github.com/gradio-app/gradio/pull/3523) +- Use Gradio API server to send telemetry using `huggingface_hub` [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3488](https://github.com/gradio-app/gradio/pull/3488) +- Fixes an an issue where if the Blocks scope was not exited, then State could be shared across sessions, by [@abidlabs](https://github.com/abidlabs) in [PR 3600](https://github.com/gradio-app/gradio/pull/3600) +- Ensures that `gr.load()` loads and applies the upstream theme, by [@abidlabs](https://github.com/abidlabs) in [PR 3641](https://github.com/gradio-app/gradio/pull/3641) +- Fixed bug where "or" was not being localized in file upload text by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3599](https://github.com/gradio-app/gradio/pull/3599) +- Fixed bug where chatbot does not autoscroll inside of a tab, row or column by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3637](https://github.com/gradio-app/gradio/pull/3637) +- Fixed bug where textbox shrinks when `lines` set to larger than 20 by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3637](https://github.com/gradio-app/gradio/pull/3637) +- Ensure CSS has fully loaded before rendering the application, by [@pngwn](https://github.com/pngwn) in [PR 3573](https://github.com/gradio-app/gradio/pull/3573) +- Support using an empty list as `gr.Dataframe` value, by [@space-nuko](https://github.com/space-nuko) in [PR 3646](https://github.com/gradio-app/gradio/pull/3646) +- Fixed `gr.Image` not filling the entire element size, by [@space-nuko](https://github.com/space-nuko) in [PR 3649](https://github.com/gradio-app/gradio/pull/3649) +- Make `gr.Code` support the `lines` property, by [@space-nuko](https://github.com/space-nuko) in [PR 3651](https://github.com/gradio-app/gradio/pull/3651) +- Fixes certain `_js` return values being double wrapped in an array, by [@space-nuko](https://github.com/space-nuko) in [PR 3594](https://github.com/gradio-app/gradio/pull/3594) +- Correct the documentation of `gr.File` component to state that its preprocessing method converts the uploaded file to a temporary file, by @RussellLuo in [PR 3660](https://github.com/gradio-app/gradio/pull/3660) +- Fixed bug in Serializer ValueError text by [@osanseviero](https://github.com/osanseviero) in [PR 3669](https://github.com/gradio-app/gradio/pull/3669) +- Fix default parameter argument and `gr.Progress` used in same function, by [@space-nuko](https://github.com/space-nuko) in [PR 3671](https://github.com/gradio-app/gradio/pull/3671) +- Hide `Remove All` button in `gr.Dropdown` single-select mode by [@space-nuko](https://github.com/space-nuko) in [PR 3678](https://github.com/gradio-app/gradio/pull/3678) +- Fix broken spaces in docs by [@aliabd](https://github.com/aliabd) in [PR 3698](https://github.com/gradio-app/gradio/pull/3698) +- Fix items in `gr.Dropdown` besides the selected item receiving a checkmark, by [@space-nuko](https://github.com/space-nuko) in [PR 3644](https://github.com/gradio-app/gradio/pull/3644) +- Fix several `gr.Dropdown` issues and improve usability, by [@space-nuko](https://github.com/space-nuko) in [PR 3705](https://github.com/gradio-app/gradio/pull/3705) + +### Documentation Changes: + +- Makes some fixes to the Theme Guide related to naming of variables, by [@abidlabs](https://github.com/abidlabs) in [PR 3561](https://github.com/gradio-app/gradio/pull/3561) +- Documented `HuggingFaceDatasetJSONSaver` by [@osanseviero](https://github.com/osanseviero) in [PR 3604](https://github.com/gradio-app/gradio/pull/3604) +- Makes some additions to documentation of `Audio` and `State` components, and fixes the `pictionary` demo by [@abidlabs](https://github.com/abidlabs) in [PR 3611](https://github.com/gradio-app/gradio/pull/3611) +- Fix outdated sharing your app guide by [@aliabd](https://github.com/aliabd) in [PR 3699](https://github.com/gradio-app/gradio/pull/3699) + +### Testing and Infrastructure Changes: + +- Removed heavily-mocked tests related to comet_ml, wandb, and mlflow as they added a significant amount of test dependencies that prevented installation of test dependencies on Windows environments. By [@abidlabs](https://github.com/abidlabs) in [PR 3608](https://github.com/gradio-app/gradio/pull/3608) +- Added Windows continuous integration, by [@space-nuko](https://github.com/space-nuko) in [PR 3628](https://github.com/gradio-app/gradio/pull/3628) +- Switched linting from flake8 + isort to `ruff`, by [@akx](https://github.com/akx) in [PR 3710](https://github.com/gradio-app/gradio/pull/3710) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Mobile responsive iframes in themes guide by [@aliabd](https://github.com/aliabd) in [PR 3562](https://github.com/gradio-app/gradio/pull/3562) +- Remove extra $demo from theme guide by [@aliabd](https://github.com/aliabd) in [PR 3563](https://github.com/gradio-app/gradio/pull/3563) +- Set the theme name to be the upstream repo name when loading from the hub by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3595](https://github.com/gradio-app/gradio/pull/3595) +- Copy everything in website Dockerfile, fix build issues by [@aliabd](https://github.com/aliabd) in [PR 3659](https://github.com/gradio-app/gradio/pull/3659) +- Raise error when an event is queued but the queue is not configured by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3640](https://github.com/gradio-app/gradio/pull/3640) +- Allows users to apss in a string name for a built-in theme, by [@abidlabs](https://github.com/abidlabs) in [PR 3641](https://github.com/gradio-app/gradio/pull/3641) +- Added `orig_name` to Video output in the backend so that the front end can set the right name for downloaded video files by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3700](https://github.com/gradio-app/gradio/pull/3700) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.23.0 + +### New Features: + +###### Theme Sharing! + +Once you have created a theme, you can upload it to the HuggingFace Hub to let others view it, use it, and build off of it! You can also download, reuse, and remix other peoples' themes. See https://gradio.app/theming-guide/ for more details. + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3428](https://github.com/gradio-app/gradio/pull/3428) + +### Bug Fixes: + +- Removes leading spaces from all lines of code uniformly in the `gr.Code()` component. By [@abidlabs](https://github.com/abidlabs) in [PR 3556](https://github.com/gradio-app/gradio/pull/3556) +- Fixed broken login page, by [@aliabid94](https://github.com/aliabid94) in [PR 3529](https://github.com/gradio-app/gradio/pull/3529) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fix rendering of dropdowns to take more space, and related bugs, by [@aliabid94](https://github.com/aliabid94) in [PR 3549](https://github.com/gradio-app/gradio/pull/3549) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.22.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Restore label bars by [@aliabid94](https://github.com/aliabid94) in [PR 3507](https://github.com/gradio-app/gradio/pull/3507) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.22.0 + +### New Features: + +###### Official Theme release + +Gradio now supports a new theme system, which allows you to customize the look and feel of your app. You can now use the `theme=` kwarg to pass in a prebuilt theme, or customize your own! See https://gradio.app/theming-guide/ for more details. By [@aliabid94](https://github.com/aliabid94) in [PR 3470](https://github.com/gradio-app/gradio/pull/3470) and [PR 3497](https://github.com/gradio-app/gradio/pull/3497) + +###### `elem_classes` + +Add keyword argument `elem_classes` to Components to control class names of components, in the same manner as existing `elem_id`. +By [@aliabid94](https://github.com/aliabid94) in [PR 3466](https://github.com/gradio-app/gradio/pull/3466) + +### Bug Fixes: + +- Fixes the File.upload() event trigger which broke as part of the change in how we uploaded files by [@abidlabs](https://github.com/abidlabs) in [PR 3462](https://github.com/gradio-app/gradio/pull/3462) +- Fixed issue with `gr.Request` object failing to handle dictionaries when nested keys couldn't be converted to variable names [#3454](https://github.com/gradio-app/gradio/issues/3454) by [@radames](https://github.com/radames) in [PR 3459](https://github.com/gradio-app/gradio/pull/3459) +- Fixed bug where css and client api was not working properly when mounted in a subpath by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3482](https://github.com/gradio-app/gradio/pull/3482) + +### Documentation Changes: + +- Document gr.Error in the docs by [@aliabd](https://github.com/aliabd) in [PR 3465](https://github.com/gradio-app/gradio/pull/3465) + +### Testing and Infrastructure Changes: + +- Pinned `pyright==1.1.298` for stability by [@abidlabs](https://github.com/abidlabs) in [PR 3475](https://github.com/gradio-app/gradio/pull/3475) +- Removed `IOComponent.add_interactive_to_config()` by [@space-nuko](https://github.com/space-nuko) in [PR 3476](https://github.com/gradio-app/gradio/pull/3476) +- Removed `IOComponent.generate_sample()` by [@space-nuko](https://github.com/space-nuko) in [PR 3475](https://github.com/gradio-app/gradio/pull/3483) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Revert primary button background color in dark mode by [@aliabid94](https://github.com/aliabid94) in [PR 3468](https://github.com/gradio-app/gradio/pull/3468) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.21.0 + +### New Features: + +###### Theme Sharing 🎨 🤝 + +You can now share your gradio themes with the world! + +After creating a theme, you can upload it to the HuggingFace Hub to let others view it, use it, and build off of it! + +###### Uploading + +There are two ways to upload a theme, via the theme class instance or the command line. + +1. Via the class instance + +```python +my_theme.push_to_hub(repo_name="my_theme", + version="0.2.0", + hf_token="...") +``` + +2. Via the command line + +First save the theme to disk + +```python +my_theme.dump(filename="my_theme.json") +``` + +Then use the `upload_theme` command: + +```bash +upload_theme\ +"my_theme.json"\ +"my_theme"\ +"0.2.0"\ +"" +``` + +The `version` must be a valid [semantic version](https://www.geeksforgeeks.org/introduction-semantic-versioning/) string. + +This creates a space on the huggingface hub to host the theme files and show potential users a preview of your theme. + +An example theme space is here: https://huggingface.co/spaces/freddyaboulton/dracula_revamped + +###### Downloading + +To use a theme from the hub, use the `from_hub` method on the `ThemeClass` and pass it to your app: + +```python +my_theme = gr.Theme.from_hub("freddyaboulton/my_theme") + +with gr.Blocks(theme=my_theme) as demo: + .... +``` + +You can also pass the theme string directly to `Blocks` or `Interface` (`gr.Blocks(theme="freddyaboulton/my_theme")`) + +You can pin your app to an upstream theme version by using semantic versioning expressions. + +For example, the following would ensure the theme we load from the `my_theme` repo was between versions `0.1.0` and `0.2.0`: + +```python +with gr.Blocks(theme="freddyaboulton/my_theme@>=0.1.0,<0.2.0") as demo: + .... +``` + +by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3428](https://github.com/gradio-app/gradio/pull/3428) + +###### Code component 🦾 + +New code component allows you to enter, edit and display code with full syntax highlighting by [@pngwn](https://github.com/pngwn) in [PR 3421](https://github.com/gradio-app/gradio/pull/3421) + +###### The `Chatbot` component now supports audio, video, and images + +The `Chatbot` component now supports audio, video, and images with a simple syntax: simply +pass in a tuple with the URL or filepath (the second optional element of the tuple is alt text), and the image/audio/video will be displayed: + +```python +gr.Chatbot([ + (("driving.mp4",), "cool video"), + (("cantina.wav",), "cool audio"), + (("lion.jpg", "A lion"), "cool pic"), +]).style(height=800) +``` + +image + +Note: images were previously supported via Markdown syntax and that is still supported for backwards compatibility. By [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3413](https://github.com/gradio-app/gradio/pull/3413) + +- Allow consecutive function triggers with `.then` and `.success` by [@aliabid94](https://github.com/aliabid94) in [PR 3430](https://github.com/gradio-app/gradio/pull/3430) + +- New code component allows you to enter, edit and display code with full syntax highlighting by [@pngwn](https://github.com/pngwn) in [PR 3421](https://github.com/gradio-app/gradio/pull/3421) + +![](https://user-images.githubusercontent.com/12937446/224116643-5cfb94b3-93ce-43ee-bb7b-c25c3b66e0a1.png) + +- Added the `.select()` event listener, which also includes event data that can be passed as an argument to a function with type hint `gr.SelectData`. The following components support the `.select()` event listener: Chatbot, CheckboxGroup, Dataframe, Dropdown, File, Gallery, HighlightedText, Label, Radio, TabItem, Tab, Textbox. Example usage: + +```python +import gradio as gr + +with gr.Blocks() as demo: + gallery = gr.Gallery(["images/1.jpg", "images/2.jpg", "images/3.jpg"]) + selected_index = gr.Textbox() + + def on_select(evt: gr.SelectData): + return evt.index + + gallery.select(on_select, None, selected_index) +``` + +By [@aliabid94](https://github.com/aliabid94) in [PR 3399](https://github.com/gradio-app/gradio/pull/3399) + +- The `Textbox` component now includes a copy button by [@abidlabs](https://github.com/abidlabs) in [PR 3452](https://github.com/gradio-app/gradio/pull/3452) + +### Bug Fixes: + +- Use `huggingface_hub` to send telemetry on `interface` and `blocks`; eventually to replace segment by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3342](https://github.com/gradio-app/gradio/pull/3342) +- Ensure load events created by components (randomize for slider, callable values) are never queued unless every is passed by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3391](https://github.com/gradio-app/gradio/pull/3391) +- Prevent in-place updates of `generic_update` by shallow copying by [@gitgithan](https://github.com/gitgithan) in [PR 3405](https://github.com/gradio-app/gradio/pull/3405) to fix [#3282](https://github.com/gradio-app/gradio/issues/3282) +- Fix bug caused by not importing `BlockContext` in `utils.py` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3424](https://github.com/gradio-app/gradio/pull/3424) +- Ensure dropdown does not highlight partial matches by [@pngwn](https://github.com/pngwn) in [PR 3421](https://github.com/gradio-app/gradio/pull/3421) +- Fix mic button display by [@aliabid94](https://github.com/aliabid94) in [PR 3456](https://github.com/gradio-app/gradio/pull/3456) + +### Documentation Changes: + +- Added a section on security and access when sharing Gradio apps by [@abidlabs](https://github.com/abidlabs) in [PR 3408](https://github.com/gradio-app/gradio/pull/3408) +- Add Chinese README by [@uanu2002](https://github.com/uanu2002) in [PR 3394](https://github.com/gradio-app/gradio/pull/3394) +- Adds documentation for web components by [@abidlabs](https://github.com/abidlabs) in [PR 3407](https://github.com/gradio-app/gradio/pull/3407) +- Fixed link in Chinese readme by [@eltociear](https://github.com/eltociear) in [PR 3417](https://github.com/gradio-app/gradio/pull/3417) +- Document Blocks methods by [@aliabd](https://github.com/aliabd) in [PR 3427](https://github.com/gradio-app/gradio/pull/3427) +- Fixed bug where event handlers were not showing up in documentation by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3434](https://github.com/gradio-app/gradio/pull/3434) + +### Testing and Infrastructure Changes: + +- Fixes tests that were failing locally but passing on CI by [@abidlabs](https://github.com/abidlabs) in [PR 3411](https://github.com/gradio-app/gradio/pull/3411) +- Remove codecov from the repo by [@aliabd](https://github.com/aliabd) in [PR 3415](https://github.com/gradio-app/gradio/pull/3415) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Prevent in-place updates of `generic_update` by shallow copying by [@gitgithan](https://github.com/gitgithan) in [PR 3405](https://github.com/gradio-app/gradio/pull/3405) to fix [#3282](https://github.com/gradio-app/gradio/issues/3282) +- Persist file names of files uploaded through any Gradio component by [@abidlabs](https://github.com/abidlabs) in [PR 3412](https://github.com/gradio-app/gradio/pull/3412) +- Fix markdown embedded component in docs by [@aliabd](https://github.com/aliabd) in [PR 3410](https://github.com/gradio-app/gradio/pull/3410) +- Clean up event listeners code by [@aliabid94](https://github.com/aliabid94) in [PR 3420](https://github.com/gradio-app/gradio/pull/3420) +- Fix css issue with spaces logo by [@aliabd](https://github.com/aliabd) in [PR 3422](https://github.com/gradio-app/gradio/pull/3422) +- Makes a few fixes to the `JSON` component (show_label parameter, icons) in [@abidlabs](https://github.com/abidlabs) in [PR 3451](https://github.com/gradio-app/gradio/pull/3451) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.20.1 + +### New Features: + +- Add `height` kwarg to style in `gr.Chatbot()` component by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3369](https://github.com/gradio-app/gradio/pull/3369) + +```python +chatbot = gr.Chatbot().style(height=500) +``` + +### Bug Fixes: + +- Ensure uploaded images are always shown in the sketch tool by [@pngwn](https://github.com/pngwn) in [PR 3386](https://github.com/gradio-app/gradio/pull/3386) +- Fixes bug where when if fn is a non-static class member, then self should be ignored as the first param of the fn by [@or25](https://github.com/or25) in [PR #3227](https://github.com/gradio-app/gradio/pull/3227) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.20.0 + +### New Features: + +###### Release event for Slider + +Now you can trigger your python function to run when the slider is released as opposed to every slider change value! + +Simply use the `release` method on the slider + +```python +slider.release(function, inputs=[...], outputs=[...], api_name="predict") +``` + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3353](https://github.com/gradio-app/gradio/pull/3353) + +###### Dropdown Component Updates + +The standard dropdown component now supports searching for choices. Also when `multiselect` is `True`, you can specify `max_choices` to set the maximum number of choices you want the user to be able to select from the dropdown component. + +```python +gr.Dropdown(label="Choose your favorite colors", choices=["red", "blue", "green", "yellow", "orange"], multiselect=True, max_choices=2) +``` + +by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3211](https://github.com/gradio-app/gradio/pull/3211) + +###### Download button for images 🖼️ + +Output images will now automatically have a download button displayed to make it easier to save and share +the results of Machine Learning art models. + +![download_sketch](https://user-images.githubusercontent.com/41651716/221025113-e693bf41-eabd-42b3-a4f2-26f2708d98fe.gif) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3297](https://github.com/gradio-app/gradio/pull/3297) + +- Updated image upload component to accept all image formats, including lossless formats like .webp by [@fienestar](https://github.com/fienestar) in [PR 3225](https://github.com/gradio-app/gradio/pull/3225) +- Adds a disabled mode to the `gr.Button` component by setting `interactive=False` by [@abidlabs](https://github.com/abidlabs) in [PR 3266](https://github.com/gradio-app/gradio/pull/3266) and [PR 3288](https://github.com/gradio-app/gradio/pull/3288) +- Adds visual feedback to the when the Flag button is clicked, by [@abidlabs](https://github.com/abidlabs) in [PR 3289](https://github.com/gradio-app/gradio/pull/3289) +- Adds ability to set `flagging_options` display text and saved flag separately by [@abidlabs](https://github.com/abidlabs) in [PR 3289](https://github.com/gradio-app/gradio/pull/3289) +- Allow the setting of `brush_radius` for the `Image` component both as a default and via `Image.update()` by [@pngwn](https://github.com/pngwn) in [PR 3277](https://github.com/gradio-app/gradio/pull/3277) +- Added `info=` argument to form components to enable extra context provided to users, by [@aliabid94](https://github.com/aliabid94) in [PR 3291](https://github.com/gradio-app/gradio/pull/3291) +- Allow developers to access the username of a logged-in user from the `gr.Request()` object using the `.username` attribute by [@abidlabs](https://github.com/abidlabs) in [PR 3296](https://github.com/gradio-app/gradio/pull/3296) +- Add `preview` option to `Gallery.style` that launches the gallery in preview mode when first loaded by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3345](https://github.com/gradio-app/gradio/pull/3345) + +### Bug Fixes: + +- Ensure `mirror_webcam` is always respected by [@pngwn](https://github.com/pngwn) in [PR 3245](https://github.com/gradio-app/gradio/pull/3245) +- Fix issue where updated markdown links were not being opened in a new tab by [@gante](https://github.com/gante) in [PR 3236](https://github.com/gradio-app/gradio/pull/3236) +- API Docs Fixes by [@aliabd](https://github.com/aliabd) in [PR 3287](https://github.com/gradio-app/gradio/pull/3287) +- Added a timeout to queue messages as some demos were experiencing infinitely growing queues from active jobs waiting forever for clients to respond by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3196](https://github.com/gradio-app/gradio/pull/3196) +- Fixes the height of rendered LaTeX images so that they match the height of surrounding text by [@abidlabs](https://github.com/abidlabs) in [PR 3258](https://github.com/gradio-app/gradio/pull/3258) and in [PR 3276](https://github.com/gradio-app/gradio/pull/3276) +- Fix bug where matplotlib images where always too small on the front end by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3274](https://github.com/gradio-app/gradio/pull/3274) +- Remove embed's `initial_height` when loading is complete so the embed finds its natural height once it is loaded [@pngwn](https://github.com/pngwn) in [PR 3292](https://github.com/gradio-app/gradio/pull/3292) +- Prevent Sketch from crashing when a default image is provided by [@pngwn](https://github.com/pngwn) in [PR 3277](https://github.com/gradio-app/gradio/pull/3277) +- Respect the `shape` argument on the front end when creating Image Sketches by [@pngwn](https://github.com/pngwn) in [PR 3277](https://github.com/gradio-app/gradio/pull/3277) +- Fix infinite loop caused by setting `Dropdown's` value to be `[]` and adding a change event on the dropdown by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3295](https://github.com/gradio-app/gradio/pull/3295) +- Fix change event listed twice in image docs by [@aliabd](https://github.com/aliabd) in [PR 3318](https://github.com/gradio-app/gradio/pull/3318) +- Fix bug that cause UI to be vertically centered at all times by [@pngwn](https://github.com/pngwn) in [PR 3336](https://github.com/gradio-app/gradio/pull/3336) +- Fix bug where `height` set in `Gallery.style` was not respected by the front-end by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3343](https://github.com/gradio-app/gradio/pull/3343) +- Ensure markdown lists are rendered correctly by [@pngwn](https://github.com/pngwn) in [PR 3341](https://github.com/gradio-app/gradio/pull/3341) +- Ensure that the initial empty value for `gr.Dropdown(Multiselect=True)` is an empty list and the initial value for `gr.Dropdown(Multiselect=False)` is an empty string by [@pngwn](https://github.com/pngwn) in [PR 3338](https://github.com/gradio-app/gradio/pull/3338) +- Ensure uploaded images respect the shape property when the canvas is also enabled by [@pngwn](https://github.com/pngwn) in [PR 3351](https://github.com/gradio-app/gradio/pull/3351) +- Ensure that Google Analytics works correctly when gradio apps are created with `analytics_enabled=True` by [@abidlabs](https://github.com/abidlabs) in [PR 3349](https://github.com/gradio-app/gradio/pull/3349) +- Fix bug where files were being re-uploaded after updates by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3375](https://github.com/gradio-app/gradio/pull/3375) +- Fix error when using backen_fn and custom js at the same time by [@jialeicui](https://github.com/jialeicui) in [PR 3358](https://github.com/gradio-app/gradio/pull/3358) +- Support new embeds for huggingface spaces subdomains by [@pngwn](https://github.com/pngwn) in [PR 3367](https://github.com/gradio-app/gradio/pull/3367) + +### Documentation Changes: + +- Added the `types` field to the dependency field in the config by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3315](https://github.com/gradio-app/gradio/pull/3315) +- Gradio Status Page by [@aliabd](https://github.com/aliabd) in [PR 3331](https://github.com/gradio-app/gradio/pull/3331) +- Adds a Guide on setting up a dashboard from Supabase data using the `gr.BarPlot` + component by [@abidlabs](https://github.com/abidlabs) in [PR 3275](https://github.com/gradio-app/gradio/pull/3275) + +### Testing and Infrastructure Changes: + +- Adds a script to benchmark the performance of the queue and adds some instructions on how to use it. By [@freddyaboulton](https://github.com/freddyaboulton) and [@abidlabs](https://github.com/abidlabs) in [PR 3272](https://github.com/gradio-app/gradio/pull/3272) +- Flaky python tests no longer cancel non-flaky tests by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3344](https://github.com/gradio-app/gradio/pull/3344) + +### Breaking Changes: + +- Chatbot bubble colors can no longer be set by `chatbot.style(color_map=)` by [@aliabid94] in [PR 3370](https://github.com/gradio-app/gradio/pull/3370) + +### Full Changelog: + +- Fixed comment typo in components.py by [@eltociear](https://github.com/eltociear) in [PR 3235](https://github.com/gradio-app/gradio/pull/3235) +- Cleaned up chatbot ui look and feel by [@aliabid94] in [PR 3370](https://github.com/gradio-app/gradio/pull/3370) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.19.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- UI fixes including footer and API docs by [@aliabid94](https://github.com/aliabid94) in [PR 3242](https://github.com/gradio-app/gradio/pull/3242) +- Updated image upload component to accept all image formats, including lossless formats like .webp by [@fienestar](https://github.com/fienestar) in [PR 3225](https://github.com/gradio-app/gradio/pull/3225) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Added backend support for themes by [@aliabid94](https://github.com/aliabid94) in [PR 2931](https://github.com/gradio-app/gradio/pull/2931) +- Added support for button sizes "lg" (default) and "sm". + +### Contributors Shoutout: + +No changes to highlight. + +## 3.19.0 + +### New Features: + +###### Improved embedding experience + +When embedding a spaces-hosted gradio app as a web component, you now get an improved UI linking back to the original space, better error handling and more intelligent load performance. No changes are required to your code to benefit from this enhanced experience; simply upgrade your gradio SDK to the latest version. + +![](https://user-images.githubusercontent.com/12937446/219653294-86937632-72c1-4e93-a77c-af705d49382a.png) + +This behaviour is configurable. You can disable the info panel at the bottom by passing `info="false"`. You can disable the container entirely by passing `container="false"`. + +Error statuses are reported in the UI with an easy way for end-users to report problems to the original space author via the community tab of that Hugginface space: + +![](https://user-images.githubusercontent.com/12937446/219655499-88019443-d694-44e7-9e6d-242e19d10a5c.png) + +By default, gradio apps are lazy loaded, vastly improving performance when there are several demos on the page. Metadata is loaded ahead of time, but the space will only be loaded and rendered when it is in view. + +This behaviour is configurable. You can pass `eager="true"` to load and render the space regardless of whether or not it is currently on the screen. + +by [@pngwn](https://github.com/pngwn) in [PR 3205](https://github.com/gradio-app/gradio/pull/3205) + +###### New `gr.BarPlot` component! 📊 + +Create interactive bar plots from a high-level interface with `gr.BarPlot`. +No need to remember matplotlib syntax anymore! + +Example usage: + +```python +import gradio as gr +import pandas as pd + +simple = pd.DataFrame({ + 'a': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'], + 'b': [28, 55, 43, 91, 81, 53, 19, 87, 52] +}) + +with gr.Blocks() as demo: + gr.BarPlot( + simple, + x="a", + y="b", + title="Simple Bar Plot with made up data", + tooltip=['a', 'b'], + ) + +demo.launch() +``` + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3157](https://github.com/gradio-app/gradio/pull/3157) + +###### Bokeh plots are back! 🌠 + +Fixed a bug that prevented bokeh plots from being displayed on the front end and extended support for both 2.x and 3.x versions of bokeh! + +![image](https://user-images.githubusercontent.com/41651716/219468324-0d82e07f-8fb4-4ff9-b40c-8250b29e45f7.png) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3212](https://github.com/gradio-app/gradio/pull/3212) + +### Bug Fixes: + +- Adds ability to add a single message from the bot or user side. Ex: specify `None` as the second value in the tuple, to add a single message in the chatbot from the "bot" side. + +```python +gr.Chatbot([("Hi, I'm DialoGPT. Try asking me a question.", None)]) +``` + +By [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3165](https://github.com/gradio-app/gradio/pull/3165) + +- Fixes `gr.utils.delete_none` to only remove props whose values are `None` from the config by [@abidlabs](https://github.com/abidlabs) in [PR 3188](https://github.com/gradio-app/gradio/pull/3188) +- Fix bug where embedded demos were not loading files properly by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3177](https://github.com/gradio-app/gradio/pull/3177) +- The `change` event is now triggered when users click the 'Clear All' button of the multiselect DropDown component by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3195](https://github.com/gradio-app/gradio/pull/3195) +- Stops File component from freezing when a large file is uploaded by [@aliabid94](https://github.com/aliabid94) in [PR 3191](https://github.com/gradio-app/gradio/pull/3191) +- Support Chinese pinyin in Dataframe by [@aliabid94](https://github.com/aliabid94) in [PR 3206](https://github.com/gradio-app/gradio/pull/3206) +- The `clear` event is now triggered when images are cleared by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3218](https://github.com/gradio-app/gradio/pull/3218) +- Fix bug where auth cookies where not sent when connecting to an app via http by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3223](https://github.com/gradio-app/gradio/pull/3223) +- Ensure latext CSS is always applied in light and dark mode by [@pngwn](https://github.com/pngwn) in [PR 3233](https://github.com/gradio-app/gradio/pull/3233) + +### Documentation Changes: + +- Sort components in docs by alphabetic order by [@aliabd](https://github.com/aliabd) in [PR 3152](https://github.com/gradio-app/gradio/pull/3152) +- Changes to W&B guide by [@scottire](https://github.com/scottire) in [PR 3153](https://github.com/gradio-app/gradio/pull/3153) +- Keep pnginfo metadata for gallery by [@wfng92](https://github.com/wfng92) in [PR 3150](https://github.com/gradio-app/gradio/pull/3150) +- Add a section on how to run a Gradio app locally [@osanseviero](https://github.com/osanseviero) in [PR 3170](https://github.com/gradio-app/gradio/pull/3170) +- Fixed typos in gradio events function documentation by [@vidalmaxime](https://github.com/vidalmaxime) in [PR 3168](https://github.com/gradio-app/gradio/pull/3168) +- Added an example using Gradio's batch mode with the diffusers library by [@abidlabs](https://github.com/abidlabs) in [PR 3224](https://github.com/gradio-app/gradio/pull/3224) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fix demos page css and add close demos button by [@aliabd](https://github.com/aliabd) in [PR 3151](https://github.com/gradio-app/gradio/pull/3151) +- Caches temp files from base64 input data by giving them a deterministic path based on the contents of data by [@abidlabs](https://github.com/abidlabs) in [PR 3197](https://github.com/gradio-app/gradio/pull/3197) +- Better warnings (when there is a mismatch between the number of output components and values returned by a function, or when the `File` component or `UploadButton` component includes a `file_types` parameter along with `file_count=="dir"`) by [@abidlabs](https://github.com/abidlabs) in [PR 3194](https://github.com/gradio-app/gradio/pull/3194) +- Raises a `gr.Error` instead of a regular Python error when you use `gr.Interface.load()` to load a model and there's an error querying the HF API by [@abidlabs](https://github.com/abidlabs) in [PR 3194](https://github.com/gradio-app/gradio/pull/3194) +- Fixed gradio share links so that they are persistent and do not reset if network + connection is disrupted by by [XciD](https://github.com/XciD), [Wauplin](https://github.com/Wauplin), and [@abidlabs](https://github.com/abidlabs) in [PR 3149](https://github.com/gradio-app/gradio/pull/3149) and a follow-up to allow it to work for users upgrading from a previous Gradio version in [PR 3221](https://github.com/gradio-app/gradio/pull/3221) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.18.0 + +### New Features: + +###### Revamped Stop Button for Interfaces 🛑 + +If your Interface function is a generator, there used to be a separate `Stop` button displayed next +to the `Submit` button. + +We've revamed the `Submit` button so that it turns into a `Stop` button during the generation process. +Clicking on the `Stop` button will cancel the generation and turn it back to a `Submit` button. +The `Stop` button will automatically turn back to a `Submit` button at the end of the generation if you don't use it! + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3124](https://github.com/gradio-app/gradio/pull/3124) + +###### Queue now works with reload mode! + +You can now call `queue` on your `demo` outside of the `if __name__ == "__main__"` block and +run the script in reload mode with the `gradio` command. + +Any changes to the `app.py` file will be reflected in the webpage automatically and the queue will work +properly! + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3089](https://github.com/gradio-app/gradio/pull/3089) + +###### Allow serving files from additional directories + +```python +demo = gr.Interface(...) +demo.launch( + file_directories=["/var/lib/demo/path/to/resources"] +) +``` + +By [@maxaudron](https://github.com/maxaudron) in [PR 3075](https://github.com/gradio-app/gradio/pull/3075) + +### Bug Fixes: + +- Fixes URL resolution on Windows by [@abidlabs](https://github.com/abidlabs) in [PR 3108](https://github.com/gradio-app/gradio/pull/3108) +- Example caching now works with components without a label attribute (e.g. `Column`) by [@abidlabs](https://github.com/abidlabs) in [PR 3123](https://github.com/gradio-app/gradio/pull/3123) +- Ensure the Video component correctly resets the UI state when a new video source is loaded and reduce choppiness of UI by [@pngwn](https://github.com/abidlabs) in [PR 3117](https://github.com/gradio-app/gradio/pull/3117) +- Fixes loading private Spaces by [@abidlabs](https://github.com/abidlabs) in [PR 3068](https://github.com/gradio-app/gradio/pull/3068) +- Added a warning when attempting to launch an `Interface` via the `%%blocks` jupyter notebook magic command by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3126](https://github.com/gradio-app/gradio/pull/3126) +- Fixes bug where interactive output image cannot be set when in edit mode by [@dawoodkhan82](https://github.com/@dawoodkhan82) in [PR 3135](https://github.com/gradio-app/gradio/pull/3135) +- A share link will automatically be created when running on Sagemaker notebooks so that the front-end is properly displayed by [@abidlabs](https://github.com/abidlabs) in [PR 3137](https://github.com/gradio-app/gradio/pull/3137) +- Fixes a few dropdown component issues; hide checkmark next to options as expected, and keyboard hover is visible by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3145]https://github.com/gradio-app/gradio/pull/3145) +- Fixed bug where example pagination buttons were not visible in dark mode or displayed under the examples table. By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3144](https://github.com/gradio-app/gradio/pull/3144) +- Fixed bug where the font color of axis labels and titles for native plots did not respond to dark mode preferences. By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3146](https://github.com/gradio-app/gradio/pull/3146) + +### Documentation Changes: + +- Added a guide on the 4 kinds of Gradio Interfaces by [@yvrjsharma](https://github.com/yvrjsharma) and [@abidlabs](https://github.com/abidlabs) in [PR 3003](https://github.com/gradio-app/gradio/pull/3003) +- Explained that the parameters in `launch` will not be respected when using reload mode, e.g. `gradio` command by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3089](https://github.com/gradio-app/gradio/pull/3089) +- Added a demo to show how to set up variable numbers of outputs in Gradio by [@abidlabs](https://github.com/abidlabs) in [PR 3127](https://github.com/gradio-app/gradio/pull/3127) +- Updated docs to reflect that the `equal_height` parameter should be passed to the `.style()` method of `gr.Row()` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3125](https://github.com/gradio-app/gradio/pull/3125) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Changed URL of final image for `fake_diffusion` demos by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3120](https://github.com/gradio-app/gradio/pull/3120) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.17.1 + +### New Features: + +###### iOS image rotation fixed 🔄 + +Previously photos uploaded via iOS would be rotated after processing. This has been fixed by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3089](https://github.com/gradio-app/gradio/pull/3091) + +######### Before + +![image](https://user-images.githubusercontent.com/41651716/215846507-a36e9d05-1ac2-4867-8ab3-ce045a9415d9.png) + +######### After + +![image](https://user-images.githubusercontent.com/41651716/215846554-e41773ed-70f0-491a-9952-6a18babf91ef.png) + +###### Run on Kaggle kernels 🧪 + +A share link will automatically be created when running on Kaggle kernels (notebooks) so that the front-end is properly displayed. + +![image](https://user-images.githubusercontent.com/41651716/216104254-2cf55599-449c-436c-b57e-40f6a83f9eee.png) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3101](https://github.com/gradio-app/gradio/pull/3101) + +### Bug Fixes: + +- Fix bug where examples were not rendered correctly for demos created with Blocks api that had multiple input compinents by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3090](https://github.com/gradio-app/gradio/pull/3090) +- Fix change event listener for JSON, HighlightedText, Chatbot by [@aliabid94](https://github.com/aliabid94) in [PR 3095](https://github.com/gradio-app/gradio/pull/3095) +- Fixes bug where video and file change event not working [@tomchang25](https://github.com/tomchang25) in [PR 3098](https://github.com/gradio-app/gradio/pull/3098) +- Fixes bug where static_video play and pause event not working [@tomchang25](https://github.com/tomchang25) in [PR 3098](https://github.com/gradio-app/gradio/pull/3098) +- Fixed `Gallery.style(grid=...)` by by [@aliabd](https://github.com/aliabd) in [PR 3107](https://github.com/gradio-app/gradio/pull/3107) + +### Documentation Changes: + +- Update chatbot guide to include blocks demo and markdown support section by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3023](https://github.com/gradio-app/gradio/pull/3023) + +* Fix a broken link in the Quick Start guide, by [@cakiki](https://github.com/cakiki) in [PR 3109](https://github.com/gradio-app/gradio/pull/3109) +* Better docs navigation on mobile by [@aliabd](https://github.com/aliabd) in [PR 3112](https://github.com/gradio-app/gradio/pull/3112) +* Add a guide on using Gradio with [Comet](https://comet.com/), by [@DN6](https://github.com/DN6/) in [PR 3058](https://github.com/gradio-app/gradio/pull/3058) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Set minimum `markdown-it-py` version to `2.0.0` so that the dollar math plugin is compatible by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3102](https://github.com/gradio-app/gradio/pull/3102) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.17.0 + +### New Features: + +###### Extended support for Interface.load! 🏗️ + +You can now load `image-to-text` and `conversational` pipelines from the hub! + +###### Image-to-text Demo + +```python +io = gr.Interface.load("models/nlpconnect/vit-gpt2-image-captioning", + api_key="") +io.launch() +``` + +image + +###### conversational Demo + +```python +chatbot = gr.Interface.load("models/microsoft/DialoGPT-medium", + api_key="") +chatbot.launch() +``` + +![chatbot_load](https://user-images.githubusercontent.com/41651716/213260220-3eaa25b7-a38b-48c6-adeb-2718bdf297a2.gif) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3011](https://github.com/gradio-app/gradio/pull/3011) + +###### Download Button added to Model3D Output Component 📥 + +No need for an additional file output component to enable model3d file downloads anymore. We now added a download button to the model3d component itself. + +Screenshot 2023-01-18 at 3 52 45 PM + +By [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3014](https://github.com/gradio-app/gradio/pull/3014) + +###### Fixing Auth on Spaces 🔑 + +Authentication on spaces works now! Third party cookies must be enabled on your browser to be able +to log in. Some browsers disable third party cookies by default (Safari, Chrome Incognito). + +![auth_spaces](https://user-images.githubusercontent.com/41651716/215528417-09538933-0576-4d1d-b3b9-1e877ab01905.gif) + +### Bug Fixes: + +- Fixes bug where interpretation event was not configured correctly by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2993](https://github.com/gradio-app/gradio/pull/2993) +- Fix relative import bug in reload mode by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2992](https://github.com/gradio-app/gradio/pull/2992) +- Fixes bug where png files were not being recognized when uploading images by [@abidlabs](https://github.com/abidlabs) in [PR 3002](https://github.com/gradio-app/gradio/pull/3002) +- Fixes bug where external Spaces could not be loaded and used as functions if they returned files by [@abidlabs](https://github.com/abidlabs) in [PR 3004](https://github.com/gradio-app/gradio/pull/3004) +- Fix bug where file serialization output was not JSON serializable by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2999](https://github.com/gradio-app/gradio/pull/2999) +- Fixes bug where png files were not being recognized when uploading images by [@abidlabs](https://github.com/abidlabs) in [PR 3002](https://github.com/gradio-app/gradio/pull/3002) +- Fixes bug where temporary uploaded files were not being added to temp sets by [@abidlabs](https://github.com/abidlabs) in [PR 3005](https://github.com/gradio-app/gradio/pull/3005) +- Fixes issue where markdown support in chatbot breaks older demos [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3006](https://github.com/gradio-app/gradio/pull/3006) +- Fixes the `/file/` route that was broken in a recent change in [PR 3010](https://github.com/gradio-app/gradio/pull/3010) +- Fix bug where the Image component could not serialize image urls by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2957](https://github.com/gradio-app/gradio/pull/2957) +- Fix forwarding for guides after SEO renaming by [@aliabd](https://github.com/aliabd) in [PR 3017](https://github.com/gradio-app/gradio/pull/3017) +- Switch all pages on the website to use latest stable gradio by [@aliabd](https://github.com/aliabd) in [PR 3016](https://github.com/gradio-app/gradio/pull/3016) +- Fix bug related to deprecated parameters in `huggingface_hub` for the HuggingFaceDatasetSaver in [PR 3025](https://github.com/gradio-app/gradio/pull/3025) +- Added better support for symlinks in the way absolute paths are resolved by [@abidlabs](https://github.com/abidlabs) in [PR 3037](https://github.com/gradio-app/gradio/pull/3037) +- Fix several minor frontend bugs (loading animation, examples as gallery) frontend [@aliabid94](https://github.com/3026) in [PR 2961](https://github.com/gradio-app/gradio/pull/3026). +- Fixes bug that the chatbot sample code does not work with certain input value by [@petrov826](https://github.com/petrov826) in [PR 3039](https://github.com/gradio-app/gradio/pull/3039). +- Fix shadows for form element and ensure focus styles more visible in dark mode [@pngwn](https://github.com/pngwn) in [PR 3042](https://github.com/gradio-app/gradio/pull/3042). +- Fixed bug where the Checkbox and Dropdown change events were not triggered in response to other component changes by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3045](https://github.com/gradio-app/gradio/pull/3045) +- Fix bug where the queue was not properly restarted after launching a `closed` app by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3022](https://github.com/gradio-app/gradio/pull/3022) +- Adding missing embedded components on docs by [@aliabd](https://github.com/aliabd) in [PR 3027](https://github.com/gradio-app/gradio/pull/3027) +- Fixes bug where app would crash if the `file_types` parameter of `gr.File` or `gr.UploadButton` was not a list by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3048](https://github.com/gradio-app/gradio/pull/3048) +- Ensure CSS mounts correctly regardless of how many Gradio instances are on the page [@pngwn](https://github.com/pngwn) in [PR 3059](https://github.com/gradio-app/gradio/pull/3059). +- Fix bug where input component was not hidden in the frontend for `UploadButton` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3053](https://github.com/gradio-app/gradio/pull/3053) +- Fixes issue where after clicking submit or undo, the sketch output wouldn't clear. [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3047](https://github.com/gradio-app/gradio/pull/3047) +- Ensure spaces embedded via the web component always use the correct URLs for server requests and change ports for testing to avoid strange collisions when users are working with embedded apps locally by [@pngwn](https://github.com/pngwn) in [PR 3065](https://github.com/gradio-app/gradio/pull/3065) +- Preserve selected image of Gallery through updated by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3061](https://github.com/gradio-app/gradio/pull/3061) +- Fix bug where auth was not respected on HF spaces by [@freddyaboulton](https://github.com/freddyaboulton) and [@aliabid94](https://github.com/aliabid94) in [PR 3049](https://github.com/gradio-app/gradio/pull/3049) +- Fixes bug where tabs selected attribute not working if manually change tab by [@tomchang25](https://github.com/tomchang25) in [3055](https://github.com/gradio-app/gradio/pull/3055) +- Change chatbot to show dots on progress, and fix bug where chatbot would not stick to bottom in the case of images by [@aliabid94](https://github.com/aliabid94) in [PR 3067](https://github.com/gradio-app/gradio/pull/3079) + +### Documentation Changes: + +- SEO improvements to guides by[@aliabd](https://github.com/aliabd) in [PR 2915](https://github.com/gradio-app/gradio/pull/2915) +- Use `gr.LinePlot` for the `blocks_kinematics` demo by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2998](https://github.com/gradio-app/gradio/pull/2998) +- Updated the `interface_series_load` to include some inline markdown code by [@abidlabs](https://github.com/abidlabs) in [PR 3051](https://github.com/gradio-app/gradio/pull/3051) + +### Testing and Infrastructure Changes: + +- Adds a GitHub action to test if any large files (> 5MB) are present by [@abidlabs](https://github.com/abidlabs) in [PR 3013](https://github.com/gradio-app/gradio/pull/3013) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Rewrote frontend using CSS variables for themes by [@pngwn](https://github.com/pngwn) in [PR 2840](https://github.com/gradio-app/gradio/pull/2840) +- Moved telemetry requests to run on background threads by [@abidlabs](https://github.com/abidlabs) in [PR 3054](https://github.com/gradio-app/gradio/pull/3054) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.16.2 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fixed file upload fails for files with zero size by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2923](https://github.com/gradio-app/gradio/pull/2923) +- Fixed bug where `mount_gradio_app` would not launch if the queue was enabled in a gradio app by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2939](https://github.com/gradio-app/gradio/pull/2939) +- Fix custom long CSS handling in Blocks by [@anton-l](https://github.com/anton-l) in [PR 2953](https://github.com/gradio-app/gradio/pull/2953) +- Recovers the dropdown change event by [@abidlabs](https://github.com/abidlabs) in [PR 2954](https://github.com/gradio-app/gradio/pull/2954). +- Fix audio file output by [@aliabid94](https://github.com/aliabid94) in [PR 2961](https://github.com/gradio-app/gradio/pull/2961). +- Fixed bug where file extensions of really long files were not kept after download by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2929](https://github.com/gradio-app/gradio/pull/2929) +- Fix bug where outputs for examples where not being returned by the backend by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2955](https://github.com/gradio-app/gradio/pull/2955) +- Fix bug in `blocks_plug` demo that prevented switching tabs programmatically with python [@TashaSkyUp](https://github.com/https://github.com/TashaSkyUp) in [PR 2971](https://github.com/gradio-app/gradio/pull/2971). + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.16.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Fix audio file output by [@aliabid94](https://github.com/aliabid94) in [PR 2950](https://github.com/gradio-app/gradio/pull/2950). + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.16.0 + +### New Features: + +###### Send custom progress updates by adding a `gr.Progress` argument after the input arguments to any function. Example: + +```python +def reverse(word, progress=gr.Progress()): + progress(0, desc="Starting") + time.sleep(1) + new_string = "" + for letter in progress.tqdm(word, desc="Reversing"): + time.sleep(0.25) + new_string = letter + new_string + return new_string + +demo = gr.Interface(reverse, gr.Text(), gr.Text()) +``` + +Progress indicator bar by [@aliabid94](https://github.com/aliabid94) in [PR 2750](https://github.com/gradio-app/gradio/pull/2750). + +- Added `title` argument to `TabbedInterface` by @MohamedAliRashad in [#2888](https://github.com/gradio-app/gradio/pull/2888) +- Add support for specifying file extensions for `gr.File` and `gr.UploadButton`, using `file_types` parameter (e.g `gr.File(file_count="multiple", file_types=["text", ".json", ".csv"])`) by @dawoodkhan82 in [#2901](https://github.com/gradio-app/gradio/pull/2901) +- Added `multiselect` option to `Dropdown` by @dawoodkhan82 in [#2871](https://github.com/gradio-app/gradio/pull/2871) + +###### With `multiselect` set to `true` a user can now select multiple options from the `gr.Dropdown` component. + +```python +gr.Dropdown(["angola", "pakistan", "canada"], multiselect=True, value=["angola"]) +``` + +Screenshot 2023-01-03 at 4 14 36 PM + +### Bug Fixes: + +- Fixed bug where an error opening an audio file led to a crash by [@FelixDombek](https://github.com/FelixDombek) in [PR 2898](https://github.com/gradio-app/gradio/pull/2898) +- Fixed bug where setting `default_enabled=False` made it so that the entire queue did not start by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2876](https://github.com/gradio-app/gradio/pull/2876) +- Fixed bug where csv preview for DataFrame examples would show filename instead of file contents by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2877](https://github.com/gradio-app/gradio/pull/2877) +- Fixed bug where an error raised after yielding iterative output would not be displayed in the browser by + [@JaySmithWpg](https://github.com/JaySmithWpg) in [PR 2889](https://github.com/gradio-app/gradio/pull/2889) +- Fixed bug in `blocks_style` demo that was preventing it from launching by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2890](https://github.com/gradio-app/gradio/pull/2890) +- Fixed bug where files could not be downloaded by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2926](https://github.com/gradio-app/gradio/pull/2926) +- Fixed bug where cached examples were not displaying properly by [@a-rogalska](https://github.com/a-rogalska) in [PR 2974](https://github.com/gradio-app/gradio/pull/2974) + +### Documentation Changes: + +- Added a Guide on using Google Sheets to create a real-time dashboard with Gradio's `DataFrame` and `LinePlot` component, by [@abidlabs](https://github.com/abidlabs) in [PR 2816](https://github.com/gradio-app/gradio/pull/2816) +- Add a components - events matrix on the docs by [@aliabd](https://github.com/aliabd) in [PR 2921](https://github.com/gradio-app/gradio/pull/2921) + +### Testing and Infrastructure Changes: + +- Deployed PRs from forks to spaces by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2895](https://github.com/gradio-app/gradio/pull/2895) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- The `default_enabled` parameter of the `Blocks.queue` method has no effect by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2876](https://github.com/gradio-app/gradio/pull/2876) +- Added typing to several Python files in codebase by [@abidlabs](https://github.com/abidlabs) in [PR 2887](https://github.com/gradio-app/gradio/pull/2887) +- Excluding untracked files from demo notebook check action by [@aliabd](https://github.com/aliabd) in [PR 2897](https://github.com/gradio-app/gradio/pull/2897) +- Optimize images and gifs by [@aliabd](https://github.com/aliabd) in [PR 2922](https://github.com/gradio-app/gradio/pull/2922) +- Updated typing by [@1nF0rmed](https://github.com/1nF0rmed) in [PR 2904](https://github.com/gradio-app/gradio/pull/2904) + +### Contributors Shoutout: + +- @JaySmithWpg for making their first contribution to gradio! +- @MohamedAliRashad for making their first contribution to gradio! + +## 3.15.0 + +### New Features: + +Gradio's newest plotting component `gr.LinePlot`! 📈 + +With this component you can easily create time series visualizations with customizable +appearance for your demos and dashboards ... all without having to know an external plotting library. + +For an example of the api see below: + +```python +gr.LinePlot(stocks, + x="date", + y="price", + color="symbol", + color_legend_position="bottom", + width=600, height=400, title="Stock Prices") +``` + +![image](https://user-images.githubusercontent.com/41651716/208711646-81ae3745-149b-46a3-babd-0569aecdd409.png) + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2807](https://github.com/gradio-app/gradio/pull/2807) + +### Bug Fixes: + +- Fixed bug where the `examples_per_page` parameter of the `Examples` component was not passed to the internal `Dataset` component by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2861](https://github.com/gradio-app/gradio/pull/2861) +- Fixes loading Spaces that have components with default values by [@abidlabs](https://github.com/abidlabs) in [PR 2855](https://github.com/gradio-app/gradio/pull/2855) +- Fixes flagging when `allow_flagging="auto"` in `gr.Interface()` by [@abidlabs](https://github.com/abidlabs) in [PR 2695](https://github.com/gradio-app/gradio/pull/2695) +- Fixed bug where passing a non-list value to `gr.CheckboxGroup` would crash the entire app by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2866](https://github.com/gradio-app/gradio/pull/2866) + +### Documentation Changes: + +- Added a Guide on using BigQuery with Gradio's `DataFrame` and `ScatterPlot` component, + by [@abidlabs](https://github.com/abidlabs) in [PR 2794](https://github.com/gradio-app/gradio/pull/2794) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fixed importing gradio can cause PIL.Image.registered_extensions() to break by `[@aliencaocao](https://github.com/aliencaocao)` in `[PR 2846](https://github.com/gradio-app/gradio/pull/2846)` +- Fix css glitch and navigation in docs by [@aliabd](https://github.com/aliabd) in [PR 2856](https://github.com/gradio-app/gradio/pull/2856) +- Added the ability to set `x_lim`, `y_lim` and legend positions for `gr.ScatterPlot` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2807](https://github.com/gradio-app/gradio/pull/2807) +- Remove footers and min-height the correct way by [@aliabd](https://github.com/aliabd) in [PR 2860](https://github.com/gradio-app/gradio/pull/2860) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.14.0 + +### New Features: + +###### Add Waveform Visual Support to Audio + +Adds a `gr.make_waveform()` function that creates a waveform video by combining an audio and an optional background image by [@dawoodkhan82](http://github.com/dawoodkhan82) and [@aliabid94](http://github.com/aliabid94) in [PR 2706](https://github.com/gradio-app/gradio/pull/2706. Helpful for making audio outputs much more shareable. + +![waveform screenrecording](https://user-images.githubusercontent.com/7870876/206062396-164a5e71-451a-4fe0-94a7-cbe9269d57e6.gif) + +###### Allows Every Component to Accept an `every` Parameter + +When a component's initial value is a function, the `every` parameter re-runs the function every `every` seconds. By [@abidlabs](https://github.com/abidlabs) in [PR 2806](https://github.com/gradio-app/gradio/pull/2806). Here's a code example: + +```py +import gradio as gr + +with gr.Blocks() as demo: + df = gr.DataFrame(run_query, every=60*60) + +demo.queue().launch() +``` + +### Bug Fixes: + +- Fixed issue where too many temporary files were created, all with randomly generated + filepaths. Now fewer temporary files are created and are assigned a path that is a + hash based on the file contents by [@abidlabs](https://github.com/abidlabs) in [PR 2758](https://github.com/gradio-app/gradio/pull/2758) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.13.2 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +\*No changes to highlight. + +- + +### Documentation Changes: + +- Improves documentation of several queuing-related parameters by [@abidlabs](https://github.com/abidlabs) in [PR 2825](https://github.com/gradio-app/gradio/pull/2825) + +### Testing and Infrastructure Changes: + +- Remove h11 pinning by [@ecederstrand](https://github.com/ecederstrand) in [PR 2820](https://github.com/gradio-app/gradio/pull/2820) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +No changes to highlight. + +### Contributors Shoutout: + +No changes to highlight. + +## 3.13.1 + +### New Features: + +###### New Shareable Links + +Replaces tunneling logic based on ssh port-forwarding to that based on `frp` by [XciD](https://github.com/XciD) and [Wauplin](https://github.com/Wauplin) in [PR 2509](https://github.com/gradio-app/gradio/pull/2509) + +You don't need to do anything differently, but when you set `share=True` in `launch()`, +you'll get this message and a public link that look a little bit different: + +```bash +Setting up a public link... we have recently upgraded the way public links are generated. If you encounter any problems, please downgrade to gradio version 3.13.0 +. +Running on public URL: https://bec81a83-5b5c-471e.gradio.live +``` + +These links are a more secure and scalable way to create shareable demos! + +### Bug Fixes: + +- Allows `gr.Dataframe()` to take a `pandas.DataFrame` that includes numpy array and other types as its initial value, by [@abidlabs](https://github.com/abidlabs) in [PR 2804](https://github.com/gradio-app/gradio/pull/2804) +- Add `altair` to requirements.txt by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2811](https://github.com/gradio-app/gradio/pull/2811) +- Added aria-labels to icon buttons that are built into UI components by [@emilyuhde](http://github.com/emilyuhde) in [PR 2791](https://github.com/gradio-app/gradio/pull/2791) + +### Documentation Changes: + +- Fixed some typos in the "Plot Component for Maps" guide by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2811](https://github.com/gradio-app/gradio/pull/2811) + +### Testing and Infrastructure Changes: + +- Fixed test for IP address by [@abidlabs](https://github.com/abidlabs) in [PR 2808](https://github.com/gradio-app/gradio/pull/2808) + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fixed typo in parameter `visible` in classes in `templates.py` by [@abidlabs](https://github.com/abidlabs) in [PR 2805](https://github.com/gradio-app/gradio/pull/2805) +- Switched external service for getting IP address from `https://api.ipify.org` to `https://checkip.amazonaws.com/` by [@abidlabs](https://github.com/abidlabs) in [PR 2810](https://github.com/gradio-app/gradio/pull/2810) + +### Contributors Shoutout: + +No changes to highlight. + +- Fixed typo in parameter `visible` in classes in `templates.py` by [@abidlabs](https://github.com/abidlabs) in [PR 2805](https://github.com/gradio-app/gradio/pull/2805) +- Switched external service for getting IP address from `https://api.ipify.org` to `https://checkip.amazonaws.com/` by [@abidlabs](https://github.com/abidlabs) in [PR 2810](https://github.com/gradio-app/gradio/pull/2810) + +## 3.13.0 + +### New Features: + +###### Scatter plot component + +It is now possible to create a scatter plot natively in Gradio! + +The `gr.ScatterPlot` component accepts a pandas dataframe and some optional configuration parameters +and will automatically create a plot for you! + +This is the first of many native plotting components in Gradio! + +For an example of how to use `gr.ScatterPlot` see below: + +```python +import gradio as gr +from vega_datasets import data + +cars = data.cars() + +with gr.Blocks() as demo: + gr.ScatterPlot(show_label=False, + value=cars, + x="Horsepower", + y="Miles_per_Gallon", + color="Origin", + tooltip="Name", + title="Car Data", + y_title="Miles per Gallon", + color_legend_title="Origin of Car").style(container=False) + +demo.launch() +``` + +image + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2764](https://github.com/gradio-app/gradio/pull/2764) + +###### Support for altair plots + +The `Plot` component can now accept altair plots as values! +Simply return an altair plot from your event listener and gradio will display it in the front-end. +See the example below: + +```python +import gradio as gr +import altair as alt +from vega_datasets import data + +cars = data.cars() +chart = ( + alt.Chart(cars) + .mark_point() + .encode( + x="Horsepower", + y="Miles_per_Gallon", + color="Origin", + ) +) + +with gr.Blocks() as demo: + gr.Plot(value=chart) +demo.launch() +``` + +image + +By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2741](https://github.com/gradio-app/gradio/pull/2741) + +###### Set the background color of a Label component + +The `Label` component now accepts a `color` argument by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2736](https://github.com/gradio-app/gradio/pull/2736). +The `color` argument should either be a valid css color name or hexadecimal string. +You can update the color with `gr.Label.update`! + +This lets you create Alert and Warning boxes with the `Label` component. See below: + +```python +import gradio as gr +import random + +def update_color(value): + if value < 0: + # This is bad so use red + return "#FF0000" + elif 0 <= value <= 20: + # Ok but pay attention (use orange) + return "#ff9966" + else: + # Nothing to worry about + return None + +def update_value(): + choice = random.choice(['good', 'bad', 'so-so']) + color = update_color(choice) + return gr.Label.update(value=choice, color=color) + + +with gr.Blocks() as demo: + label = gr.Label(value=-10) + demo.load(lambda: update_value(), inputs=None, outputs=[label], every=1) +demo.queue().launch() +``` + +![label_bg_color_update](https://user-images.githubusercontent.com/41651716/204400372-80e53857-f26f-4a38-a1ae-1acadff75e89.gif) + +###### Add Brazilian Portuguese translation + +Add Brazilian Portuguese translation (pt-BR.json) by [@pstwh](http://github.com/pstwh) in [PR 2753](https://github.com/gradio-app/gradio/pull/2753): + +image + +### Bug Fixes: + +- Fixed issue where image thumbnails were not showing when an example directory was provided + by [@abidlabs](https://github.com/abidlabs) in [PR 2745](https://github.com/gradio-app/gradio/pull/2745) +- Fixed bug loading audio input models from the hub by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2779](https://github.com/gradio-app/gradio/pull/2779). +- Fixed issue where entities were not merged when highlighted text was generated from the + dictionary inputs [@payoto](https://github.com/payoto) in [PR 2767](https://github.com/gradio-app/gradio/pull/2767) +- Fixed bug where generating events did not finish running even if the websocket connection was closed by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2783](https://github.com/gradio-app/gradio/pull/2783). + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Images in the chatbot component are now resized if they exceed a max width by [@abidlabs](https://github.com/abidlabs) in [PR 2748](https://github.com/gradio-app/gradio/pull/2748) +- Missing parameters have been added to `gr.Blocks().load()` by [@abidlabs](https://github.com/abidlabs) in [PR 2755](https://github.com/gradio-app/gradio/pull/2755) +- Deindex share URLs from search by [@aliabd](https://github.com/aliabd) in [PR 2772](https://github.com/gradio-app/gradio/pull/2772) +- Redirect old links and fix broken ones by [@aliabd](https://github.com/aliabd) in [PR 2774](https://github.com/gradio-app/gradio/pull/2774) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.12.0 + +### New Features: + +###### The `Chatbot` component now supports a subset of Markdown (including bold, italics, code, images) + +You can now pass in some Markdown to the Chatbot component and it will show up, +meaning that you can pass in images as well! by [@abidlabs](https://github.com/abidlabs) in [PR 2731](https://github.com/gradio-app/gradio/pull/2731) + +Here's a simple example that references a local image `lion.jpg` that is in the same +folder as the Python script: + +```py +import gradio as gr + +with gr.Blocks() as demo: + gr.Chatbot([("hi", "hello **abubakar**"), ("![](/file=lion.jpg)", "cool pic")]) + +demo.launch() +``` + +![Alt text](https://user-images.githubusercontent.com/1778297/204357455-5c1a4002-eee7-479d-9a1e-ba2c12522723.png) + +To see a more realistic example, see the new demo `/demo/chatbot_multimodal/run.py`. + +###### Latex support + +Added mathtext (a subset of latex) support to gr.Markdown. Added by [@kashif](https://github.com/kashif) and [@aliabid94](https://github.com/aliabid94) in [PR 2696](https://github.com/gradio-app/gradio/pull/2696). + +Example of how it can be used: + +```python +gr.Markdown( + r""" + # Hello World! $\frac{\sqrt{x + y}}{4}$ is today's lesson. + """) +``` + +###### Update Accordion properties from the backend + +You can now update the Accordion `label` and `open` status with `gr.Accordion.update` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2690](https://github.com/gradio-app/gradio/pull/2690) + +```python +import gradio as gr + +with gr.Blocks() as demo: + with gr.Accordion(label="Open for greeting", open=False) as accordion: + gr.Textbox("Hello!") + open_btn = gr.Button(value="Open Accordion") + close_btn = gr.Button(value="Close Accordion") + open_btn.click( + lambda: gr.Accordion.update(open=True, label="Open Accordion"), + inputs=None, + outputs=[accordion], + ) + close_btn.click( + lambda: gr.Accordion.update(open=False, label="Closed Accordion"), + inputs=None, + outputs=[accordion], + ) +demo.launch() +``` + +![update_accordion](https://user-images.githubusercontent.com/41651716/203164176-b102eae3-babe-4986-ae30-3ab4f400cedc.gif) + +### Bug Fixes: + +- Fixed bug where requests timeout is missing from utils.version_check() by [@yujiehecs](https://github.com/yujiehecs) in [PR 2729](https://github.com/gradio-app/gradio/pull/2729) +- Fixed bug where so that the `File` component can properly preprocess files to "binary" byte-string format by [CoffeeVampir3](https://github.com/CoffeeVampir3) in [PR 2727](https://github.com/gradio-app/gradio/pull/2727) +- Fixed bug to ensure that filenames are less than 200 characters even for non-English languages by [@SkyTNT](https://github.com/SkyTNT) in [PR 2685](https://github.com/gradio-app/gradio/pull/2685) + +### Documentation Changes: + +- Performance improvements to docs on mobile by [@aliabd](https://github.com/aliabd) in [PR 2730](https://github.com/gradio-app/gradio/pull/2730) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Make try examples button more prominent by [@aliabd](https://github.com/aliabd) in [PR 2705](https://github.com/gradio-app/gradio/pull/2705) +- Fix id clashes in docs by [@aliabd](https://github.com/aliabd) in [PR 2713](https://github.com/gradio-app/gradio/pull/2713) +- Fix typos in guide docs by [@andridns](https://github.com/andridns) in [PR 2722](https://github.com/gradio-app/gradio/pull/2722) +- Add option to `include_audio` in Video component. When `True`, for `source="webcam"` this will record audio and video, for `source="upload"` this will retain the audio in an uploaded video by [@mandargogate](https://github.com/MandarGogate) in [PR 2721](https://github.com/gradio-app/gradio/pull/2721) + +### Contributors Shoutout: + +- [@andridns](https://github.com/andridns) made their first contribution in [PR 2722](https://github.com/gradio-app/gradio/pull/2722)! + +## 3.11.0 + +### New Features: + +###### Upload Button + +There is now a new component called the `UploadButton` which is a file upload component but in button form! You can also specify what file types it should accept in the form of a list (ex: `image`, `video`, `audio`, `text`, or generic `file`). Added by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2591](https://github.com/gradio-app/gradio/pull/2591). + +Example of how it can be used: + +```python +import gradio as gr + +def upload_file(files): + file_paths = [file.name for file in files] + return file_paths + +with gr.Blocks() as demo: + file_output = gr.File() + upload_button = gr.UploadButton("Click to Upload a File", file_types=["image", "video"], file_count="multiple") + upload_button.upload(upload_file, upload_button, file_output) + +demo.launch() +``` + +###### Revamped API documentation page + +New API Docs page with in-browser playground and updated aesthetics. [@gary149](https://github.com/gary149) in [PR 2652](https://github.com/gradio-app/gradio/pull/2652) + +###### Revamped Login page + +Previously our login page had its own CSS, had no dark mode, and had an ugly json message on the wrong credentials. Made the page more aesthetically consistent, added dark mode support, and a nicer error message. [@aliabid94](https://github.com/aliabid94) in [PR 2684](https://github.com/gradio-app/gradio/pull/2684) + +###### Accessing the Requests Object Directly + +You can now access the Request object directly in your Python function by [@abidlabs](https://github.com/abidlabs) in [PR 2641](https://github.com/gradio-app/gradio/pull/2641). This means that you can access request headers, the client IP address, and so on. In order to use it, add a parameter to your function and set its type hint to be `gr.Request`. Here's a simple example: + +```py +import gradio as gr + +def echo(name, request: gr.Request): + if request: + print("Request headers dictionary:", request.headers) + print("IP address:", request.client.host) + return name + +io = gr.Interface(echo, "textbox", "textbox").launch() +``` + +### Bug Fixes: + +- Fixed bug that limited files from being sent over websockets to 16MB. The new limit + is now 1GB by [@abidlabs](https://github.com/abidlabs) in [PR 2709](https://github.com/gradio-app/gradio/pull/2709) + +### Documentation Changes: + +- Updated documentation for embedding Gradio demos on Spaces as web components by + [@julien-c](https://github.com/julien-c) in [PR 2698](https://github.com/gradio-app/gradio/pull/2698) +- Updated IFrames in Guides to use the host URL instead of the Space name to be consistent with the new method for embedding Spaces, by + [@julien-c](https://github.com/julien-c) in [PR 2692](https://github.com/gradio-app/gradio/pull/2692) +- Colab buttons on every demo in the website! Just click open in colab, and run the demo there. + +https://user-images.githubusercontent.com/9021060/202878400-cb16ed47-f4dd-4cb0-b2f0-102a9ff64135.mov + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Better warnings and error messages for `gr.Interface.load()` by [@abidlabs](https://github.com/abidlabs) in [PR 2694](https://github.com/gradio-app/gradio/pull/2694) +- Add open in colab buttons to demos in docs and /demos by [@aliabd](https://github.com/aliabd) in [PR 2608](https://github.com/gradio-app/gradio/pull/2608) +- Apply different formatting for the types in component docstrings by [@aliabd](https://github.com/aliabd) in [PR 2707](https://github.com/gradio-app/gradio/pull/2707) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.10.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Passes kwargs into `gr.Interface.load()` by [@abidlabs](https://github.com/abidlabs) in [PR 2669](https://github.com/gradio-app/gradio/pull/2669) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Clean up printed statements in Embedded Colab Mode by [@aliabid94](https://github.com/aliabid94) in [PR 2612](https://github.com/gradio-app/gradio/pull/2612) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.10.0 + +- Add support for `'password'` and `'email'` types to `Textbox`. [@pngwn](https://github.com/pngwn) in [PR 2653](https://github.com/gradio-app/gradio/pull/2653) +- `gr.Textbox` component will now raise an exception if `type` is not "text", "email", or "password" [@pngwn](https://github.com/pngwn) in [PR 2653](https://github.com/gradio-app/gradio/pull/2653). This will cause demos using the deprecated `gr.Textbox(type="number")` to raise an exception. + +### Bug Fixes: + +- Updated the minimum FastApi used in tests to version 0.87 by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2647](https://github.com/gradio-app/gradio/pull/2647) +- Fixed bug where interfaces with examples could not be loaded with `gr.Interface.load` by [@freddyaboulton](https://github.com/freddyaboulton) [PR 2640](https://github.com/gradio-app/gradio/pull/2640) +- Fixed bug where the `interactive` property of a component could not be updated by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2639](https://github.com/gradio-app/gradio/pull/2639) +- Fixed bug where some URLs were not being recognized as valid URLs and thus were not + loading correctly in various components by [@abidlabs](https://github.com/abidlabs) in [PR 2659](https://github.com/gradio-app/gradio/pull/2659) + +### Documentation Changes: + +- Fix some typos in the embedded demo names in "05_using_blocks_like_functions.md" by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2656](https://github.com/gradio-app/gradio/pull/2656) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Add support for `'password'` and `'email'` types to `Textbox`. [@pngwn](https://github.com/pngwn) in [PR 2653](https://github.com/gradio-app/gradio/pull/2653) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.9.1 + +### New Features: + +No changes to highlight. + +### Bug Fixes: + +- Only set a min height on md and html when loading by [@pngwn](https://github.com/pngwn) in [PR 2623](https://github.com/gradio-app/gradio/pull/2623) + +### Documentation Changes: + +- See docs for the latest gradio commit to main as well the latest pip release: + +![main-vs-pip](https://user-images.githubusercontent.com/9021060/199607887-aab1ae4e-a070-4527-966d-024397abe15b.gif) + +- Modified the "Connecting To a Database Guide" to use `pd.read_sql` as opposed to low-level postgres connector by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2604](https://github.com/gradio-app/gradio/pull/2604) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Dropdown for seeing docs as latest or main by [@aliabd](https://github.com/aliabd) in [PR 2544](https://github.com/gradio-app/gradio/pull/2544) +- Allow `gr.Templates` to accept parameters to override the defaults by [@abidlabs](https://github.com/abidlabs) in [PR 2600](https://github.com/gradio-app/gradio/pull/2600) +- Components now throw a `ValueError()` if constructed with invalid parameters for `type` or `source` (for components that take those parameters) in [PR 2610](https://github.com/gradio-app/gradio/pull/2610) +- Allow auth with using queue by [@GLGDLY](https://github.com/GLGDLY) in [PR 2611](https://github.com/gradio-app/gradio/pull/2611) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.9 + +### New Features: + +- Gradio is now embedded directly in colab without requiring the share link by [@aliabid94](https://github.com/aliabid94) in [PR 2455](https://github.com/gradio-app/gradio/pull/2455) + +###### Calling functions by api_name in loaded apps + +When you load an upstream app with `gr.Blocks.load`, you can now specify which fn +to call with the `api_name` parameter. + +```python +import gradio as gr +english_translator = gr.Blocks.load(name="spaces/gradio/english-translator") +german = english_translator("My name is Freddy", api_name='translate-to-german') +``` + +The `api_name` parameter will take precedence over the `fn_index` parameter. + +### Bug Fixes: + +- Fixed bug where None could not be used for File,Model3D, and Audio examples by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2588](https://github.com/gradio-app/gradio/pull/2588) +- Fixed links in Plotly map guide + demo by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2578](https://github.com/gradio-app/gradio/pull/2578) +- `gr.Blocks.load()` now correctly loads example files from Spaces [@abidlabs](https://github.com/abidlabs) in [PR 2594](https://github.com/gradio-app/gradio/pull/2594) +- Fixed bug when image clear started upload dialog [@mezotaken](https://github.com/mezotaken) in [PR 2577](https://github.com/gradio-app/gradio/pull/2577) + +### Documentation Changes: + +- Added a Guide on how to configure the queue for maximum performance by [@abidlabs](https://github.com/abidlabs) in [PR 2558](https://github.com/gradio-app/gradio/pull/2558) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Add `api_name` to `Blocks.__call__` by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2593](https://github.com/gradio-app/gradio/pull/2593) +- Update queue with using deque & update requirements by [@GLGDLY](https://github.com/GLGDLY) in [PR 2428](https://github.com/gradio-app/gradio/pull/2428) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.8.2 + +### Bug Fixes: + +- Ensure gradio apps embedded via spaces use the correct endpoint for predictions. [@pngwn](https://github.com/pngwn) in [PR 2567](https://github.com/gradio-app/gradio/pull/2567) +- Ensure gradio apps embedded via spaces use the correct websocket protocol. [@pngwn](https://github.com/pngwn) in [PR 2571](https://github.com/gradio-app/gradio/pull/2571) + +### New Features: + +###### Running Events Continuously + +Gradio now supports the ability to run an event continuously on a fixed schedule. To use this feature, +pass `every=# of seconds` to the event definition. This will run the event every given number of seconds! + +This can be used to: + +- Create live visualizations that show the most up to date data +- Refresh the state of the frontend automatically in response to changes in the backend + +Here is an example of a live plot that refreshes every half second: + +```python +import math +import gradio as gr +import plotly.express as px +import numpy as np + + +plot_end = 2 * math.pi + + +def get_plot(period=1): + global plot_end + x = np.arange(plot_end - 2 * math.pi, plot_end, 0.02) + y = np.sin(2*math.pi*period * x) + fig = px.line(x=x, y=y) + plot_end += 2 * math.pi + return fig + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + gr.Markdown("Change the value of the slider to automatically update the plot") + period = gr.Slider(label="Period of plot", value=1, minimum=0, maximum=10, step=1) + plot = gr.Plot(label="Plot (updates every half second)") + + dep = demo.load(get_plot, None, plot, every=0.5) + period.change(get_plot, period, plot, every=0.5, cancels=[dep]) + +demo.queue().launch() +``` + +![live_demo](https://user-images.githubusercontent.com/41651716/198357377-633ce460-4e31-47bd-8202-1440cdd6fe19.gif) + +### Bug Fixes: + +No changes to highlight. + +### Documentation Changes: + +- Explained how to set up `queue` and `auth` when working with reload mode by by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3089](https://github.com/gradio-app/gradio/pull/3089) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Allows loading private Spaces by passing an an `api_key` to `gr.Interface.load()` + by [@abidlabs](https://github.com/abidlabs) in [PR 2568](https://github.com/gradio-app/gradio/pull/2568) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.8 + +### New Features: + +- Allows event listeners to accept a single dictionary as its argument, where the keys are the components and the values are the component values. This is set by passing the input components in the event listener as a set instead of a list. [@aliabid94](https://github.com/aliabid94) in [PR 2550](https://github.com/gradio-app/gradio/pull/2550) + +### Bug Fixes: + +- Fix whitespace issue when using plotly. [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2548](https://github.com/gradio-app/gradio/pull/2548) +- Apply appropriate alt text to all gallery images. [@camenduru](https://github.com/camenduru) in [PR 2358](https://github.com/gradio-app/gradio/pull/2538) +- Removed erroneous tkinter import in gradio.blocks by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2555](https://github.com/gradio-app/gradio/pull/2555) + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Added the `every` keyword to event listeners that runs events on a fixed schedule by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2512](https://github.com/gradio-app/gradio/pull/2512) +- Fix whitespace issue when using plotly. [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2548](https://github.com/gradio-app/gradio/pull/2548) +- Apply appropriate alt text to all gallery images. [@camenduru](https://github.com/camenduru) in [PR 2358](https://github.com/gradio-app/gradio/pull/2538) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.7 + +### New Features: + +###### Batched Functions + +Gradio now supports the ability to pass _batched_ functions. Batched functions are just +functions which take in a list of inputs and return a list of predictions. + +For example, here is a batched function that takes in two lists of inputs (a list of +words and a list of ints), and returns a list of trimmed words as output: + +```py +import time + +def trim_words(words, lens): + trimmed_words = [] + time.sleep(5) + for w, l in zip(words, lens): + trimmed_words.append(w[:l]) + return [trimmed_words] +``` + +The advantage of using batched functions is that if you enable queuing, the Gradio +server can automatically _batch_ incoming requests and process them in parallel, +potentially speeding up your demo. Here's what the Gradio code looks like (notice +the `batch=True` and `max_batch_size=16` -- both of these parameters can be passed +into event triggers or into the `Interface` class) + +```py +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + word = gr.Textbox(label="word", value="abc") + leng = gr.Number(label="leng", precision=0, value=1) + output = gr.Textbox(label="Output") + with gr.Row(): + run = gr.Button() + + event = run.click(trim_words, [word, leng], output, batch=True, max_batch_size=16) + +demo.queue() +demo.launch() +``` + +In the example above, 16 requests could be processed in parallel (for a total inference +time of 5 seconds), instead of each request being processed separately (for a total +inference time of 80 seconds). + +###### Upload Event + +`Video`, `Audio`, `Image`, and `File` components now support a `upload()` event that is triggered when a user uploads a file into any of these components. + +Example usage: + +```py +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + input_video = gr.Video() + output_video = gr.Video() + + # Clears the output video when an input video is uploaded + input_video.upload(lambda : None, None, output_video) +``` + +### Bug Fixes: + +- Fixes issue where plotly animations, interactivity, titles, legends, were not working properly. [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2486](https://github.com/gradio-app/gradio/pull/2486) +- Prevent requests to the `/api` endpoint from skipping the queue if the queue is enabled for that event by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2493](https://github.com/gradio-app/gradio/pull/2493) +- Fixes a bug with `cancels` in event triggers so that it works properly if multiple + Blocks are rendered by [@abidlabs](https://github.com/abidlabs) in [PR 2530](https://github.com/gradio-app/gradio/pull/2530) +- Prevent invalid targets of events from crashing the whole application. [@pngwn](https://github.com/pngwn) in [PR 2534](https://github.com/gradio-app/gradio/pull/2534) +- Properly dequeue cancelled events when multiple apps are rendered by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2540](https://github.com/gradio-app/gradio/pull/2540) +- Fixes videos being cropped due to height/width params not being used [@hannahblair](https://github.com/hannahblair) in [PR 4946](https://github.com/gradio-app/gradio/pull/4946) + +### Documentation Changes: + +- Added an example interactive dashboard to the "Tabular & Plots" section of the Demos page by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2508](https://github.com/gradio-app/gradio/pull/2508) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Fixes the error message if a user builds Gradio locally and tries to use `share=True` by [@abidlabs](https://github.com/abidlabs) in [PR 2502](https://github.com/gradio-app/gradio/pull/2502) +- Allows the render() function to return self by [@Raul9595](https://github.com/Raul9595) in [PR 2514](https://github.com/gradio-app/gradio/pull/2514) +- Fixes issue where plotly animations, interactivity, titles, legends, were not working properly. [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2486](https://github.com/gradio-app/gradio/pull/2486) +- Gradio now supports batched functions by [@abidlabs](https://github.com/abidlabs) in [PR 2218](https://github.com/gradio-app/gradio/pull/2218) +- Add `upload` event for `Video`, `Audio`, `Image`, and `File` components [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2448](https://github.com/gradio-app/gradio/pull/2456) +- Changes websocket path for Spaces as it is no longer necessary to have a different URL for websocket connections on Spaces by [@abidlabs](https://github.com/abidlabs) in [PR 2528](https://github.com/gradio-app/gradio/pull/2528) +- Clearer error message when events are defined outside of a Blocks scope, and a warning if you + try to use `Series` or `Parallel` with `Blocks` by [@abidlabs](https://github.com/abidlabs) in [PR 2543](https://github.com/gradio-app/gradio/pull/2543) +- Adds support for audio samples that are in `float64`, `float16`, or `uint16` formats by [@abidlabs](https://github.com/abidlabs) in [PR 2545](https://github.com/gradio-app/gradio/pull/2545) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.6 + +### New Features: + +###### Cancelling Running Events + +Running events can be cancelled when other events are triggered! To test this feature, pass the `cancels` parameter to the event listener. +For this feature to work, the queue must be enabled. + +![cancel_on_change_rl](https://user-images.githubusercontent.com/41651716/195952623-61a606bd-e82b-4e1a-802e-223154cb8727.gif) + +Code: + +```python +import time +import gradio as gr + +def fake_diffusion(steps): + for i in range(steps): + time.sleep(1) + yield str(i) + +def long_prediction(*args, **kwargs): + time.sleep(10) + return 42 + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + n = gr.Slider(1, 10, value=9, step=1, label="Number Steps") + run = gr.Button() + output = gr.Textbox(label="Iterative Output") + stop = gr.Button(value="Stop Iterating") + with gr.Column(): + prediction = gr.Number(label="Expensive Calculation") + run_pred = gr.Button(value="Run Expensive Calculation") + with gr.Column(): + cancel_on_change = gr.Textbox(label="Cancel Iteration and Expensive Calculation on Change") + + click_event = run.click(fake_diffusion, n, output) + stop.click(fn=None, inputs=None, outputs=None, cancels=[click_event]) + pred_event = run_pred.click(fn=long_prediction, inputs=None, outputs=prediction) + + cancel_on_change.change(None, None, None, cancels=[click_event, pred_event]) + + +demo.queue(concurrency_count=1, max_size=20).launch() +``` + +For interfaces, a stop button will be added automatically if the function uses a `yield` statement. + +```python +import gradio as gr +import time + +def iteration(steps): + for i in range(steps): + time.sleep(0.5) + yield i + +gr.Interface(iteration, + inputs=gr.Slider(minimum=1, maximum=10, step=1, value=5), + outputs=gr.Number()).queue().launch() +``` + +![stop_interface_rl](https://user-images.githubusercontent.com/41651716/195952883-e7ca4235-aae3-4852-8f28-96d01d0c5822.gif) + +### Bug Fixes: + +- Add loading status tracker UI to HTML and Markdown components. [@pngwn](https://github.com/pngwn) in [PR 2474](https://github.com/gradio-app/gradio/pull/2474) +- Fixed videos being mirrored in the front-end if source is not webcam by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2475](https://github.com/gradio-app/gradio/pull/2475) +- Add clear button for timeseries component [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2487](https://github.com/gradio-app/gradio/pull/2487) +- Removes special characters from temporary filenames so that the files can be served by components [@abidlabs](https://github.com/abidlabs) in [PR 2480](https://github.com/gradio-app/gradio/pull/2480) +- Fixed infinite reload loop when mounting gradio as a sub application by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2477](https://github.com/gradio-app/gradio/pull/2477) + +### Documentation Changes: + +- Adds a demo to show how a sound alert can be played upon completion of a prediction by [@abidlabs](https://github.com/abidlabs) in [PR 2478](https://github.com/gradio-app/gradio/pull/2478) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Enable running events to be cancelled from other events by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2433](https://github.com/gradio-app/gradio/pull/2433) +- Small fix for version check before reuploading demos by [@aliabd](https://github.com/aliabd) in [PR 2469](https://github.com/gradio-app/gradio/pull/2469) +- Add loading status tracker UI to HTML and Markdown components. [@pngwn](https://github.com/pngwn) in [PR 2400](https://github.com/gradio-app/gradio/pull/2474) +- Add clear button for timeseries component [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2487](https://github.com/gradio-app/gradio/pull/2487) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.5 + +### Bug Fixes: + +- Ensure that Gradio does not take control of the HTML page title when embedding a gradio app as a web component, this behaviour flipped by adding `control_page_title="true"` to the webcomponent. [@pngwn](https://github.com/pngwn) in [PR 2400](https://github.com/gradio-app/gradio/pull/2400) +- Decreased latency in iterative-output demos by making the iteration asynchronous [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2409](https://github.com/gradio-app/gradio/pull/2409) +- Fixed queue getting stuck under very high load by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2374](https://github.com/gradio-app/gradio/pull/2374) +- Ensure that components always behave as if `interactive=True` were set when the following conditions are true: + + - no default value is provided, + - they are not set as the input or output of an event, + - `interactive` kwarg is not set. + + [@pngwn](https://github.com/pngwn) in [PR 2459](https://github.com/gradio-app/gradio/pull/2459) + +### New Features: + +- When an `Image` component is set to `source="upload"`, it is now possible to drag and drop and image to replace a previously uploaded image by [@pngwn](https://github.com/pngwn) in [PR 1711](https://github.com/gradio-app/gradio/issues/1711) +- The `gr.Dataset` component now accepts `HTML` and `Markdown` components by [@abidlabs](https://github.com/abidlabs) in [PR 2437](https://github.com/gradio-app/gradio/pull/2437) + +### Documentation Changes: + +- Improved documentation for the `gr.Dataset` component by [@abidlabs](https://github.com/abidlabs) in [PR 2437](https://github.com/gradio-app/gradio/pull/2437) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +- The `Carousel` component is officially deprecated. Since gradio 3.0, code containing the `Carousel` component would throw warnings. As of the next release, the `Carousel` component will raise an exception. + +### Full Changelog: + +- Speeds up Gallery component by using temporary files instead of base64 representation in the front-end by [@proxyphi](https://github.com/proxyphi), [@pngwn](https://github.com/pngwn), and [@abidlabs](https://github.com/abidlabs) in [PR 2265](https://github.com/gradio-app/gradio/pull/2265) +- Fixed some embedded demos in the guides by not loading the gradio web component in some guides by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2403](https://github.com/gradio-app/gradio/pull/2403) +- When an `Image` component is set to `source="upload"`, it is now possible to drag and drop and image to replace a previously uploaded image by [@pngwn](https://github.com/pngwn) in [PR 2400](https://github.com/gradio-app/gradio/pull/2410) +- Improve documentation of the `Blocks.load()` event by [@abidlabs](https://github.com/abidlabs) in [PR 2413](https://github.com/gradio-app/gradio/pull/2413) +- Decreased latency in iterative-output demos by making the iteration asynchronous [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2409](https://github.com/gradio-app/gradio/pull/2409) +- Updated share link message to reference new Spaces Hardware [@abidlabs](https://github.com/abidlabs) in [PR 2423](https://github.com/gradio-app/gradio/pull/2423) +- Automatically restart spaces if they're down by [@aliabd](https://github.com/aliabd) in [PR 2405](https://github.com/gradio-app/gradio/pull/2405) +- Carousel component is now deprecated by [@abidlabs](https://github.com/abidlabs) in [PR 2434](https://github.com/gradio-app/gradio/pull/2434) +- Build Gradio from source in ui tests by by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2440](https://github.com/gradio-app/gradio/pull/2440) +- Change "return ValueError" to "raise ValueError" by [@vzakharov](https://github.com/vzakharov) in [PR 2445](https://github.com/gradio-app/gradio/pull/2445) +- Add guide on creating a map demo using the `gr.Plot()` component [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2402](https://github.com/gradio-app/gradio/pull/2402) +- Add blur event for `Textbox` and `Number` components [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2448](https://github.com/gradio-app/gradio/pull/2448) +- Stops a gradio launch from hogging a port even after it's been killed [@aliabid94](https://github.com/aliabid94) in [PR 2453](https://github.com/gradio-app/gradio/pull/2453) +- Fix embedded interfaces on touch screen devices by [@aliabd](https://github.com/aliabd) in [PR 2457](https://github.com/gradio-app/gradio/pull/2457) +- Upload all demos to spaces by [@aliabd](https://github.com/aliabd) in [PR 2281](https://github.com/gradio-app/gradio/pull/2281) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.4.1 + +### New Features: + +###### 1. See Past and Upcoming Changes in the Release History 👀 + +You can now see gradio's release history directly on the website, and also keep track of upcoming changes. Just go [here](https://gradio.app/changelog/). + +![release-history](https://user-images.githubusercontent.com/9021060/193145458-3de699f7-7620-45de-aa73-a1c1b9b96257.gif) + +### Bug Fixes: + +1. Fix typo in guide image path by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2357](https://github.com/gradio-app/gradio/pull/2357) +2. Raise error if Blocks has duplicate component with same IDs by [@abidlabs](https://github.com/abidlabs) in [PR 2359](https://github.com/gradio-app/gradio/pull/2359) +3. Catch the permission exception on the audio component by [@Ian-GL](https://github.com/Ian-GL) in [PR 2330](https://github.com/gradio-app/gradio/pull/2330) +4. Fix image_classifier_interface_load demo by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2365](https://github.com/gradio-app/gradio/pull/2365) +5. Fix combining adjacent components without gaps by introducing `gr.Row(variant="compact")` by [@aliabid94](https://github.com/aliabid94) in [PR 2291](https://github.com/gradio-app/gradio/pull/2291) This comes with deprecation of the following arguments for `Component.style`: `round`, `margin`, `border`. +6. Fix audio streaming, which was previously choppy in [PR 2351](https://github.com/gradio-app/gradio/pull/2351). Big thanks to [@yannickfunk](https://github.com/yannickfunk) for the proposed solution. +7. Fix bug where new typeable slider doesn't respect the minimum and maximum values [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2380](https://github.com/gradio-app/gradio/pull/2380) + +### Documentation Changes: + +1. New Guide: Connecting to a Database 🗄️ + + A new guide by [@freddyaboulton](https://github.com/freddyaboulton) that explains how you can use Gradio to connect your app to a database. Read more [here](https://gradio.app/connecting_to_a_database/). + +2. New Guide: Running Background Tasks 🥷 + + A new guide by [@freddyaboulton](https://github.com/freddyaboulton) that explains how you can run background tasks from your gradio app. Read more [here](https://gradio.app/running_background_tasks/). + +3. Small fixes to docs for `Image` component by [@abidlabs](https://github.com/abidlabs) in [PR 2372](https://github.com/gradio-app/gradio/pull/2372) + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- Create a guide on how to connect an app to a database hosted on the cloud by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2341](https://github.com/gradio-app/gradio/pull/2341) +- Removes `analytics` dependency by [@abidlabs](https://github.com/abidlabs) in [PR 2347](https://github.com/gradio-app/gradio/pull/2347) +- Add guide on launching background tasks from your app by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2350](https://github.com/gradio-app/gradio/pull/2350) +- Fix typo in guide image path by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2357](https://github.com/gradio-app/gradio/pull/2357) +- Raise error if Blocks has duplicate component with same IDs by [@abidlabs](https://github.com/abidlabs) in [PR 2359](https://github.com/gradio-app/gradio/pull/2359) +- Hotfix: fix version back to 3.4 by [@abidlabs](https://github.com/abidlabs) in [PR 2361](https://github.com/gradio-app/gradio/pull/2361) +- Change version.txt to 3.4 instead of 3.4.0 by [@aliabd](https://github.com/aliabd) in [PR 2363](https://github.com/gradio-app/gradio/pull/2363) +- Catch the permission exception on the audio component by [@Ian-GL](https://github.com/Ian-GL) in [PR 2330](https://github.com/gradio-app/gradio/pull/2330) +- Fix image_classifier_interface_load demo by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2365](https://github.com/gradio-app/gradio/pull/2365) +- Small fixes to docs for `Image` component by [@abidlabs](https://github.com/abidlabs) in [PR 2372](https://github.com/gradio-app/gradio/pull/2372) +- Automated Release Notes by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2306](https://github.com/gradio-app/gradio/pull/2306) +- Fixed small typos in the docs [@julien-c](https://github.com/julien-c) in [PR 2373](https://github.com/gradio-app/gradio/pull/2373) +- Adds ability to disable pre/post-processing for examples [@abidlabs](https://github.com/abidlabs) in [PR 2383](https://github.com/gradio-app/gradio/pull/2383) +- Copy changelog file in website docker by [@aliabd](https://github.com/aliabd) in [PR 2384](https://github.com/gradio-app/gradio/pull/2384) +- Lets users provide a `gr.update()` dictionary even if post-processing is disabled [@abidlabs](https://github.com/abidlabs) in [PR 2385](https://github.com/gradio-app/gradio/pull/2385) +- Fix bug where errors would cause apps run in reload mode to hang forever by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2394](https://github.com/gradio-app/gradio/pull/2394) +- Fix bug where new typeable slider doesn't respect the minimum and maximum values [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2380](https://github.com/gradio-app/gradio/pull/2380) + +### Contributors Shoutout: + +No changes to highlight. + +## 3.4 + +### New Features: + +###### 1. Gallery Captions 🖼️ + +You can now pass captions to images in the Gallery component. To do so you need to pass a {List} of (image, {str} caption) tuples. This is optional and the component also accepts just a list of the images. + +Here's an example: + +```python +import gradio as gr + +images_with_captions = [ + ("https://images.unsplash.com/photo-1551969014-7d2c4cddf0b6", "Cheetah by David Groves"), + ("https://images.unsplash.com/photo-1546182990-dffeafbe841d", "Lion by Francesco"), + ("https://images.unsplash.com/photo-1561731216-c3a4d99437d5", "Tiger by Mike Marrah") + ] + +with gr.Blocks() as demo: + gr.Gallery(value=images_with_captions) + +demo.launch() +``` + +gallery_captions + +###### 2. Type Values into the Slider 🔢 + +You can now type values directly on the Slider component! Here's what it looks like: + +![type-slider](https://user-images.githubusercontent.com/9021060/192399877-76b662a1-fede-4417-a932-fc15f0da7360.gif) + +###### 3. Better Sketching and Inpainting 🎨 + +We've made a lot of changes to our Image component so that it can support better sketching and inpainting. + +Now supports: + +- A standalone black-and-white sketch + +```python +import gradio as gr +demo = gr.Interface(lambda x: x, gr.Sketchpad(), gr.Image()) +demo.launch() +``` + +![bw](https://user-images.githubusercontent.com/9021060/192410264-b08632b5-7b2a-4f86-afb0-5760e7b474cf.gif) + +- A standalone color sketch + +```python +import gradio as gr +demo = gr.Interface(lambda x: x, gr.Paint(), gr.Image()) +demo.launch() +``` + +![color-sketch](https://user-images.githubusercontent.com/9021060/192410500-3c8c3e64-a5fd-4df2-a991-f0a5cef93728.gif) + +- An uploadable image with black-and-white or color sketching + +```python +import gradio as gr +demo = gr.Interface(lambda x: x, gr.Image(source='upload', tool='color-sketch'), gr.Image()) # for black and white, tool = 'sketch' +demo.launch() +``` + +![sketch-new](https://user-images.githubusercontent.com/9021060/192402422-e53cb7b6-024e-448c-87eb-d6a35a63c476.gif) + +- Webcam with black-and-white or color sketching + +```python +import gradio as gr +demo = gr.Interface(lambda x: x, gr.Image(source='webcam', tool='color-sketch'), gr.Image()) # for black and white, tool = 'sketch' +demo.launch() +``` + +![webcam-sketch](https://user-images.githubusercontent.com/9021060/192410820-0ffaf324-776e-4e1f-9de6-0fdbbf4940fa.gif) + +As well as other fixes + +### Bug Fixes: + +1. Fix bug where max concurrency count is not respected in queue by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2286](https://github.com/gradio-app/gradio/pull/2286) +2. fix : queue could be blocked by [@SkyTNT](https://github.com/SkyTNT) in [PR 2288](https://github.com/gradio-app/gradio/pull/2288) +3. Supports `gr.update()` in example caching by [@abidlabs](https://github.com/abidlabs) in [PR 2309](https://github.com/gradio-app/gradio/pull/2309) +4. Clipboard fix for iframes by [@abidlabs](https://github.com/abidlabs) in [PR 2321](https://github.com/gradio-app/gradio/pull/2321) +5. Fix: Dataframe column headers are reset when you add a new column by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2318](https://github.com/gradio-app/gradio/pull/2318) +6. Added support for URLs for Video, Audio, and Image by [@abidlabs](https://github.com/abidlabs) in [PR 2256](https://github.com/gradio-app/gradio/pull/2256) +7. Add documentation about how to create and use the Gradio FastAPI app by [@abidlabs](https://github.com/abidlabs) in [PR 2263](https://github.com/gradio-app/gradio/pull/2263) + +### Documentation Changes: + +1. Adding a Playground Tab to the Website by [@aliabd](https://github.com/aliabd) in [PR 1860](https://github.com/gradio-app/gradio/pull/1860) +2. Gradio for Tabular Data Science Workflows Guide by [@merveenoyan](https://github.com/merveenoyan) in [PR 2199](https://github.com/gradio-app/gradio/pull/2199) +3. Promotes `postprocess` and `preprocess` to documented parameters by [@abidlabs](https://github.com/abidlabs) in [PR 2293](https://github.com/gradio-app/gradio/pull/2293) +4. Update 2)key_features.md by [@voidxd](https://github.com/voidxd) in [PR 2326](https://github.com/gradio-app/gradio/pull/2326) +5. Add docs to blocks context postprocessing function by [@Ian-GL](https://github.com/Ian-GL) in [PR 2332](https://github.com/gradio-app/gradio/pull/2332) + +### Testing and Infrastructure Changes + +1. Website fixes and refactoring by [@aliabd](https://github.com/aliabd) in [PR 2280](https://github.com/gradio-app/gradio/pull/2280) +2. Don't deploy to spaces on release by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2313](https://github.com/gradio-app/gradio/pull/2313) + +### Full Changelog: + +- Website fixes and refactoring by [@aliabd](https://github.com/aliabd) in [PR 2280](https://github.com/gradio-app/gradio/pull/2280) +- Fix bug where max concurrency count is not respected in queue by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2286](https://github.com/gradio-app/gradio/pull/2286) +- Promotes `postprocess` and `preprocess` to documented parameters by [@abidlabs](https://github.com/abidlabs) in [PR 2293](https://github.com/gradio-app/gradio/pull/2293) +- Raise warning when trying to cache examples but not all inputs have examples by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2279](https://github.com/gradio-app/gradio/pull/2279) +- fix : queue could be blocked by [@SkyTNT](https://github.com/SkyTNT) in [PR 2288](https://github.com/gradio-app/gradio/pull/2288) +- Don't deploy to spaces on release by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2313](https://github.com/gradio-app/gradio/pull/2313) +- Supports `gr.update()` in example caching by [@abidlabs](https://github.com/abidlabs) in [PR 2309](https://github.com/gradio-app/gradio/pull/2309) +- Respect Upstream Queue when loading interfaces/blocks from Spaces by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2294](https://github.com/gradio-app/gradio/pull/2294) +- Clipboard fix for iframes by [@abidlabs](https://github.com/abidlabs) in [PR 2321](https://github.com/gradio-app/gradio/pull/2321) +- Sketching + Inpainting Capabilities to Gradio by [@abidlabs](https://github.com/abidlabs) in [PR 2144](https://github.com/gradio-app/gradio/pull/2144) +- Update 2)key_features.md by [@voidxd](https://github.com/voidxd) in [PR 2326](https://github.com/gradio-app/gradio/pull/2326) +- release 3.4b3 by [@abidlabs](https://github.com/abidlabs) in [PR 2328](https://github.com/gradio-app/gradio/pull/2328) +- Fix: Dataframe column headers are reset when you add a new column by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2318](https://github.com/gradio-app/gradio/pull/2318) +- Start queue when gradio is a sub application by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2319](https://github.com/gradio-app/gradio/pull/2319) +- Fix Web Tracker Script by [@aliabd](https://github.com/aliabd) in [PR 2308](https://github.com/gradio-app/gradio/pull/2308) +- Add docs to blocks context postprocessing function by [@Ian-GL](https://github.com/Ian-GL) in [PR 2332](https://github.com/gradio-app/gradio/pull/2332) +- Fix typo in iterator variable name in run_predict function by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2340](https://github.com/gradio-app/gradio/pull/2340) +- Add captions to galleries by [@aliabid94](https://github.com/aliabid94) in [PR 2284](https://github.com/gradio-app/gradio/pull/2284) +- Typeable value on gradio.Slider by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2329](https://github.com/gradio-app/gradio/pull/2329) + +### Contributors Shoutout: + +- [@SkyTNT](https://github.com/SkyTNT) made their first contribution in [PR 2288](https://github.com/gradio-app/gradio/pull/2288) +- [@voidxd](https://github.com/voidxd) made their first contribution in [PR 2326](https://github.com/gradio-app/gradio/pull/2326) + +## 3.3 + +### New Features: + +###### 1. Iterative Outputs ⏳ + +You can now create an iterative output simply by having your function return a generator! + +Here's (part of) an example that was used to generate the interface below it. [See full code](https://colab.research.google.com/drive/1m9bWS6B82CT7bw-m4L6AJR8za7fEK7Ov?usp=sharing). + +```python +def predict(steps, seed): + generator = torch.manual_seed(seed) + for i in range(1,steps): + yield pipeline(generator=generator, num_inference_steps=i)["sample"][0] +``` + +![example](https://user-images.githubusercontent.com/9021060/189086273-f5e7087d-71fa-4158-90a9-08e84da0421c.mp4) + +###### 2. Accordion Layout 🆕 + +This version of Gradio introduces a new layout component to Blocks: the Accordion. Wrap your elements in a neat, expandable layout that allows users to toggle them as needed. + +Usage: ([Read the docs](https://gradio.app/docs/#accordion)) + +```python +with gr.Accordion("open up"): +# components here +``` + +![accordion](https://user-images.githubusercontent.com/9021060/189088465-f0ffd7f0-fc6a-42dc-9249-11c5e1e0529b.gif) + +###### 3. Skops Integration 📈 + +Our new integration with [skops](https://huggingface.co/blog/skops) allows you to load tabular classification and regression models directly from the [hub](https://huggingface.co/models). + +Here's a classification example showing how quick it is to set up an interface for a [model](https://huggingface.co/scikit-learn/tabular-playground). + +```python +import gradio as gr +gr.Interface.load("models/scikit-learn/tabular-playground").launch() +``` + +![187936493-5c90c01d-a6dd-400f-aa42-833a096156a1](https://user-images.githubusercontent.com/9021060/189090519-328fbcb4-120b-43c8-aa54-d6fccfa6b7e8.png) + +### Bug Fixes: + +No changes to highlight. + +### Documentation Changes: + +No changes to highlight. + +### Testing and Infrastructure Changes: + +No changes to highlight. + +### Breaking Changes: + +No changes to highlight. + +### Full Changelog: + +- safari fixes by [@pngwn](https://github.com/pngwn) in [PR 2138](https://github.com/gradio-app/gradio/pull/2138) +- Fix roundedness and form borders by [@aliabid94](https://github.com/aliabid94) in [PR 2147](https://github.com/gradio-app/gradio/pull/2147) +- Better processing of example data prior to creating dataset component by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2147](https://github.com/gradio-app/gradio/pull/2147) +- Show error on Connection drops by [@aliabid94](https://github.com/aliabid94) in [PR 2147](https://github.com/gradio-app/gradio/pull/2147) +- 3.2 release! by [@abidlabs](https://github.com/abidlabs) in [PR 2139](https://github.com/gradio-app/gradio/pull/2139) +- Fixed Named API Requests by [@abidlabs](https://github.com/abidlabs) in [PR 2151](https://github.com/gradio-app/gradio/pull/2151) +- Quick Fix: Cannot upload Model3D image after clearing it by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2168](https://github.com/gradio-app/gradio/pull/2168) +- Fixed misleading log when server_name is '0.0.0.0' by [@lamhoangtung](https://github.com/lamhoangtung) in [PR 2176](https://github.com/gradio-app/gradio/pull/2176) +- Keep embedded PngInfo metadata by [@cobryan05](https://github.com/cobryan05) in [PR 2170](https://github.com/gradio-app/gradio/pull/2170) +- Skops integration: Load tabular classification and regression models from the hub by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2126](https://github.com/gradio-app/gradio/pull/2126) +- Respect original filename when cached example files are downloaded by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2145](https://github.com/gradio-app/gradio/pull/2145) +- Add manual trigger to deploy to pypi by [@abidlabs](https://github.com/abidlabs) in [PR 2192](https://github.com/gradio-app/gradio/pull/2192) +- Fix bugs with gr.update by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2157](https://github.com/gradio-app/gradio/pull/2157) +- Make queue per app by [@aliabid94](https://github.com/aliabid94) in [PR 2193](https://github.com/gradio-app/gradio/pull/2193) +- Preserve Labels In Interpretation Components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2166](https://github.com/gradio-app/gradio/pull/2166) +- Quick Fix: Multiple file download not working by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2169](https://github.com/gradio-app/gradio/pull/2169) +- use correct MIME type for js-script file by [@daspartho](https://github.com/daspartho) in [PR 2200](https://github.com/gradio-app/gradio/pull/2200) +- Add accordion component by [@aliabid94](https://github.com/aliabid94) in [PR 2208](https://github.com/gradio-app/gradio/pull/2208) + +### Contributors Shoutout: + +- [@lamhoangtung](https://github.com/lamhoangtung) made their first contribution in [PR 2176](https://github.com/gradio-app/gradio/pull/2176) +- [@cobryan05](https://github.com/cobryan05) made their first contribution in [PR 2170](https://github.com/gradio-app/gradio/pull/2170) +- [@daspartho](https://github.com/daspartho) made their first contribution in [PR 2200](https://github.com/gradio-app/gradio/pull/2200) + +## 3.2 + +### New Features: + +###### 1. Improvements to Queuing 🥇 + +We've implemented a brand new queuing system based on **web sockets** instead of HTTP long polling. Among other things, this allows us to manage queue sizes better on Hugging Face Spaces. There are also additional queue-related parameters you can add: + +- Now supports concurrent workers (parallelization) + +```python +demo = gr.Interface(...) +demo.queue(concurrency_count=3) +demo.launch() +``` + +- Configure a maximum queue size + +```python +demo = gr.Interface(...) +demo.queue(max_size=100) +demo.launch() +``` + +- If a user closes their tab / browser, they leave the queue, which means the demo will run faster for everyone else + +###### 2. Fixes to Examples + +- Dataframe examples will render properly, and look much clearer in the UI: (thanks to PR #2125) + +![Screen Shot 2022-08-30 at 8 29 58 PM](https://user-images.githubusercontent.com/9021060/187586561-d915bafb-f968-4966-b9a2-ef41119692b2.png) + +- Image and Video thumbnails are cropped to look neater and more uniform: (thanks to PR #2109) + +![Screen Shot 2022-08-30 at 8 32 15 PM](https://user-images.githubusercontent.com/9021060/187586890-56e1e4f0-1b84-42d9-a82f-911772c41030.png) + +- Other fixes in PR #2131 and #2064 make it easier to design and use Examples + +###### 3. Component Fixes 🧱 + +- Specify the width and height of an image in its style tag (thanks to PR #2133) + +```python +components.Image().style(height=260, width=300) +``` + +- Automatic conversion of videos so they are playable in the browser (thanks to PR #2003). Gradio will check if a video's format is playable in the browser and, if it isn't, will automatically convert it to a format that is (mp4). +- Pass in a json filepath to the Label component (thanks to PR #2083) +- Randomize the default value of a Slider (thanks to PR #1935) + +![slider-random](https://user-images.githubusercontent.com/9021060/187596230-3db9697f-9f4d-42f5-9387-d77573513448.gif) + +- Improvements to State in PR #2100 + +###### 4. Ability to Randomize Input Sliders and Reload Data whenever the Page Loads + +- In some cases, you want to be able to show a different set of input data to every user as they load the page app. For example, you might want to randomize the value of a "seed" `Slider` input. Or you might want to show a `Textbox` with the current date. We now supporting passing _functions_ as the default value in input components. When you pass in a function, it gets **re-evaluated** every time someone loads the demo, allowing you to reload / change data for different users. + +Here's an example loading the current date time into an input Textbox: + +```python +import gradio as gr +import datetime + +with gr.Blocks() as demo: + gr.Textbox(datetime.datetime.now) + +demo.launch() +``` + +Note that we don't evaluate the function -- `datetime.datetime.now()` -- we pass in the function itself to get this behavior -- `datetime.datetime.now` + +Because randomizing the initial value of `Slider` is a common use case, we've added a `randomize` keyword argument you can use to randomize its initial value: + +```python +import gradio as gr +demo = gr.Interface(lambda x:x, gr.Slider(0, 10, randomize=True), "number") +demo.launch() +``` + +###### 5. New Guide 🖊️ + +- [Gradio and W&B Integration](https://gradio.app/Gradio_and_Wandb_Integration/) + +### Full Changelog: + +- Reset components to original state by setting value to None by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2044](https://github.com/gradio-app/gradio/pull/2044) +- Cleaning up the way data is processed for components by [@abidlabs](https://github.com/abidlabs) in [PR 1967](https://github.com/gradio-app/gradio/pull/1967) +- version 3.1.8b by [@abidlabs](https://github.com/abidlabs) in [PR 2063](https://github.com/gradio-app/gradio/pull/2063) +- Wandb guide by [@AK391](https://github.com/AK391) in [PR 1898](https://github.com/gradio-app/gradio/pull/1898) +- Add a flagging callback to save json files to a hugging face dataset by [@chrisemezue](https://github.com/chrisemezue) in [PR 1821](https://github.com/gradio-app/gradio/pull/1821) +- Add data science demos to landing page by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2067](https://github.com/gradio-app/gradio/pull/2067) +- Hide time series + xgboost demos by default by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2079](https://github.com/gradio-app/gradio/pull/2079) +- Encourage people to keep trying when queue full by [@apolinario](https://github.com/apolinario) in [PR 2076](https://github.com/gradio-app/gradio/pull/2076) +- Updated our analytics on creation of Blocks/Interface by [@abidlabs](https://github.com/abidlabs) in [PR 2082](https://github.com/gradio-app/gradio/pull/2082) +- `Label` component now accepts file paths to `.json` files by [@abidlabs](https://github.com/abidlabs) in [PR 2083](https://github.com/gradio-app/gradio/pull/2083) +- Fix issues related to demos in Spaces by [@abidlabs](https://github.com/abidlabs) in [PR 2086](https://github.com/gradio-app/gradio/pull/2086) +- Fix TimeSeries examples not properly displayed in UI by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2064](https://github.com/gradio-app/gradio/pull/2064) +- Fix infinite requests when doing tab item select by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2070](https://github.com/gradio-app/gradio/pull/2070) +- Accept deprecated `file` route as well by [@abidlabs](https://github.com/abidlabs) in [PR 2099](https://github.com/gradio-app/gradio/pull/2099) +- Allow frontend method execution on Block.load event by [@codedealer](https://github.com/codedealer) in [PR 2108](https://github.com/gradio-app/gradio/pull/2108) +- Improvements to `State` by [@abidlabs](https://github.com/abidlabs) in [PR 2100](https://github.com/gradio-app/gradio/pull/2100) +- Catch IndexError, KeyError in video_is_playable by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2113](https://github.com/gradio-app/gradio/pull/2113) +- Fix: Download button does not respect the filepath returned by the function by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2073](https://github.com/gradio-app/gradio/pull/2073) +- Refactoring Layout: Adding column widths, forms, and more. by [@aliabid94](https://github.com/aliabid94) in [PR 2097](https://github.com/gradio-app/gradio/pull/2097) +- Update CONTRIBUTING.md by [@abidlabs](https://github.com/abidlabs) in [PR 2118](https://github.com/gradio-app/gradio/pull/2118) +- 2092 df ex by [@pngwn](https://github.com/pngwn) in [PR 2125](https://github.com/gradio-app/gradio/pull/2125) +- feat(samples table/gallery): Crop thumbs to square by [@ronvoluted](https://github.com/ronvoluted) in [PR 2109](https://github.com/gradio-app/gradio/pull/2109) +- Some enhancements to `gr.Examples` by [@abidlabs](https://github.com/abidlabs) in [PR 2131](https://github.com/gradio-app/gradio/pull/2131) +- Image size fix by [@aliabid94](https://github.com/aliabid94) in [PR 2133](https://github.com/gradio-app/gradio/pull/2133) + +### Contributors Shoutout: + +- [@chrisemezue](https://github.com/chrisemezue) made their first contribution in [PR 1821](https://github.com/gradio-app/gradio/pull/1821) +- [@apolinario](https://github.com/apolinario) made their first contribution in [PR 2076](https://github.com/gradio-app/gradio/pull/2076) +- [@codedealer](https://github.com/codedealer) made their first contribution in [PR 2108](https://github.com/gradio-app/gradio/pull/2108) + +## 3.1 + +### New Features: + +###### 1. Embedding Demos on Any Website 💻 + +With PR #1444, Gradio is now distributed as a web component. This means demos can be natively embedded on websites. You'll just need to add two lines: one to load the gradio javascript, and one to link to the demos backend. + +Here's a simple example that embeds the demo from a Hugging Face space: + +```html + + +``` + +But you can also embed demos that are running anywhere, you just need to link the demo to `src` instead of `space`. In fact, all the demos on the gradio website are embedded this way: + +Screen Shot 2022-07-14 at 2 41 44 PM + +Read more in the [Embedding Gradio Demos](https://gradio.app/embedding_gradio_demos) guide. + +###### 2. Reload Mode 👨‍💻 + +Reload mode helps developers create gradio demos faster by automatically reloading the demo whenever the code changes. It can support development on Python IDEs (VS Code, PyCharm, etc), the terminal, as well as Jupyter notebooks. + +If your demo code is in a script named `app.py`, instead of running `python app.py` you can now run `gradio app.py` and that will launch the demo in reload mode: + +```bash +Launching in reload mode on: http://127.0.0.1:7860 (Press CTRL+C to quit) +Watching... +WARNING: The --reload flag should not be used in production on Windows. +``` + +If you're working from a Jupyter or Colab Notebook, use these magic commands instead: `%load_ext gradio` when you import gradio, and `%%blocks` in the top of the cell with the demo code. Here's an example that shows how much faster the development becomes: + +![Blocks](https://user-images.githubusercontent.com/9021060/178986488-ed378cc8-5141-4330-ba41-672b676863d0.gif) + +###### 3. Inpainting Support on `gr.Image()` 🎨 + +We updated the Image component to add support for inpainting demos. It works by adding `tool="sketch"` as a parameter, that passes both an image and a sketchable mask to your prediction function. + +Here's an example from the [LAMA space](https://huggingface.co/spaces/akhaliq/lama): + +![FXApVlFVsAALSD-](https://user-images.githubusercontent.com/9021060/178989479-549867c8-7fb0-436a-a97d-1e91c9f5e611.jpeg) + +###### 4. Markdown and HTML support in Dataframes 🔢 + +We upgraded the Dataframe component in PR #1684 to support rendering Markdown and HTML inside the cells. + +This means you can build Dataframes that look like the following: + +![image (8)](https://user-images.githubusercontent.com/9021060/178991233-41cb07a5-e7a3-433e-89b8-319bc78eb9c2.png) + +###### 5. `gr.Examples()` for Blocks 🧱 + +We've added the `gr.Examples` component helper to allow you to add examples to any Blocks demo. This class is a wrapper over the `gr.Dataset` component. + +Screen Shot 2022-07-14 at 2 23 50 PM + +gr.Examples takes two required parameters: + +- `examples` which takes in a nested list +- `inputs` which takes in a component or list of components + +You can read more in the [Examples docs](https://gradio.app/docs/#examples) or the [Adding Examples to your Demos guide](https://gradio.app/adding_examples_to_your_app/). + +###### 6. Fixes to Audio Streaming + +With [PR 1828](https://github.com/gradio-app/gradio/pull/1828) we now hide the status loading animation, as well as remove the echo in streaming. Check out the [stream_audio](https://github.com/gradio-app/gradio/blob/main/demo/stream_audio/run.py) demo for more or read through our [Real Time Speech Recognition](https://gradio.app/real_time_speech_recognition/) guide. + +Screen Shot 2022-07-19 at 6 02 35 PM + +### Full Changelog: + +- File component: list multiple files and allow for download #1446 by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1681](https://github.com/gradio-app/gradio/pull/1681) +- Add ColorPicker to docs by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1768](https://github.com/gradio-app/gradio/pull/1768) +- Mock out requests in TestRequest unit tests by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1794](https://github.com/gradio-app/gradio/pull/1794) +- Add requirements.txt and test_files to source dist by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1817](https://github.com/gradio-app/gradio/pull/1817) +- refactor: f-string for tunneling.py by [@nhankiet](https://github.com/nhankiet) in [PR 1819](https://github.com/gradio-app/gradio/pull/1819) +- Miscellaneous formatting improvements to website by [@aliabd](https://github.com/aliabd) in [PR 1754](https://github.com/gradio-app/gradio/pull/1754) +- `integrate()` method moved to `Blocks` by [@abidlabs](https://github.com/abidlabs) in [PR 1776](https://github.com/gradio-app/gradio/pull/1776) +- Add python-3.7 tests by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1818](https://github.com/gradio-app/gradio/pull/1818) +- Copy test dir in website dockers by [@aliabd](https://github.com/aliabd) in [PR 1827](https://github.com/gradio-app/gradio/pull/1827) +- Add info to docs on how to set default values for components by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1788](https://github.com/gradio-app/gradio/pull/1788) +- Embedding Components on Docs by [@aliabd](https://github.com/aliabd) in [PR 1726](https://github.com/gradio-app/gradio/pull/1726) +- Remove usage of deprecated gr.inputs and gr.outputs from website by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1796](https://github.com/gradio-app/gradio/pull/1796) +- Some cleanups to the docs page by [@abidlabs](https://github.com/abidlabs) in [PR 1822](https://github.com/gradio-app/gradio/pull/1822) + +### Contributors Shoutout: + +- [@nhankiet](https://github.com/nhankiet) made their first contribution in [PR 1819](https://github.com/gradio-app/gradio/pull/1819) + +## 3.0 + +###### 🔥 Gradio 3.0 is the biggest update to the library, ever. + +### New Features: + +###### 1. Blocks 🧱 + +Blocks is a new, low-level API that allows you to have full control over the data flows and layout of your application. It allows you to build very complex, multi-step applications. For example, you might want to: + +- Group together related demos as multiple tabs in one web app +- Change the layout of your demo instead of just having all of the inputs on the left and outputs on the right +- Have multi-step interfaces, in which the output of one model becomes the input to the next model, or have more flexible data flows in general +- Change a component's properties (for example, the choices in a Dropdown) or its visibility based on user input + +Here's a simple example that creates the demo below it: + +```python +import gradio as gr + +def update(name): + return f"Welcome to Gradio, {name}!" + +demo = gr.Blocks() + +with 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(fn=update, + inputs=inp, + outputs=out) + +demo.launch() +``` + +![hello-blocks](https://user-images.githubusercontent.com/9021060/168684108-78cbd24b-e6bd-4a04-a8d9-20d535203434.gif) + +Read our [Introduction to Blocks](http://gradio.app/introduction_to_blocks/) guide for more, and join the 🎈 [Gradio Blocks Party](https://huggingface.co/spaces/Gradio-Blocks/README)! + +###### 2. Our Revamped Design 🎨 + +We've upgraded our design across the entire library: from components, and layouts all the way to dark mode. + +![kitchen_sink](https://user-images.githubusercontent.com/9021060/168686333-7a6e3096-3e23-4309-abf2-5cd7736e0463.gif) + +###### 3. A New Website 💻 + +We've upgraded [gradio.app](https://gradio.app) to make it cleaner, faster and easier to use. Our docs now come with components and demos embedded directly on the page. So you can quickly get up to speed with what you're looking for. + +![website](https://user-images.githubusercontent.com/9021060/168687191-10d6a3bd-101f-423a-8193-48f47a5e077d.gif) + +###### 4. New Components: Model3D, Dataset, and More.. + +We've introduced a lot of new components in `3.0`, including `Model3D`, `Dataset`, `Markdown`, `Button` and `Gallery`. You can find all the components and play around with them [here](https://gradio.app/docs/#components). + +![Model3d](https://user-images.githubusercontent.com/9021060/168689062-6ad77151-8cc5-467d-916c-f7c78e52ec0c.gif) + +### Full Changelog: + +- Gradio dash fe by [@pngwn](https://github.com/pngwn) in [PR 807](https://github.com/gradio-app/gradio/pull/807) +- Blocks components by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 765](https://github.com/gradio-app/gradio/pull/765) +- Blocks components V2 by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 843](https://github.com/gradio-app/gradio/pull/843) +- Blocks-Backend-Events by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 844](https://github.com/gradio-app/gradio/pull/844) +- Interfaces from Blocks by [@aliabid94](https://github.com/aliabid94) in [PR 849](https://github.com/gradio-app/gradio/pull/849) +- Blocks dev by [@aliabid94](https://github.com/aliabid94) in [PR 853](https://github.com/gradio-app/gradio/pull/853) +- Started updating demos to use the new `gradio.components` syntax by [@abidlabs](https://github.com/abidlabs) in [PR 848](https://github.com/gradio-app/gradio/pull/848) +- add test infra + add browser tests to CI by [@pngwn](https://github.com/pngwn) in [PR 852](https://github.com/gradio-app/gradio/pull/852) +- 854 textbox by [@pngwn](https://github.com/pngwn) in [PR 859](https://github.com/gradio-app/gradio/pull/859) +- Getting old Python unit tests to pass on `blocks-dev` by [@abidlabs](https://github.com/abidlabs) in [PR 861](https://github.com/gradio-app/gradio/pull/861) +- initialise chatbot with empty array of messages by [@pngwn](https://github.com/pngwn) in [PR 867](https://github.com/gradio-app/gradio/pull/867) +- add test for output to input by [@pngwn](https://github.com/pngwn) in [PR 866](https://github.com/gradio-app/gradio/pull/866) +- More Interface -> Blocks features by [@aliabid94](https://github.com/aliabid94) in [PR 864](https://github.com/gradio-app/gradio/pull/864) +- Fixing external.py in blocks-dev to reflect the new HF Spaces paths by [@abidlabs](https://github.com/abidlabs) in [PR 879](https://github.com/gradio-app/gradio/pull/879) +- backend_default_value_refactoring by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 871](https://github.com/gradio-app/gradio/pull/871) +- fix default_value by [@pngwn](https://github.com/pngwn) in [PR 869](https://github.com/gradio-app/gradio/pull/869) +- fix buttons by [@aliabid94](https://github.com/aliabid94) in [PR 883](https://github.com/gradio-app/gradio/pull/883) +- Checking and updating more demos to use 3.0 syntax by [@abidlabs](https://github.com/abidlabs) in [PR 892](https://github.com/gradio-app/gradio/pull/892) +- Blocks Tests by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 902](https://github.com/gradio-app/gradio/pull/902) +- Interface fix by [@pngwn](https://github.com/pngwn) in [PR 901](https://github.com/gradio-app/gradio/pull/901) +- Quick fix: Issue 893 by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 907](https://github.com/gradio-app/gradio/pull/907) +- 3d Image Component by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 775](https://github.com/gradio-app/gradio/pull/775) +- fix endpoint url in prod by [@pngwn](https://github.com/pngwn) in [PR 911](https://github.com/gradio-app/gradio/pull/911) +- rename Model3d to Image3D by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 912](https://github.com/gradio-app/gradio/pull/912) +- update pypi to 2.9.1 by [@abidlabs](https://github.com/abidlabs) in [PR 916](https://github.com/gradio-app/gradio/pull/916) +- blocks-with-fix by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 917](https://github.com/gradio-app/gradio/pull/917) +- Restore Interpretation, Live, Auth, Queueing by [@aliabid94](https://github.com/aliabid94) in [PR 915](https://github.com/gradio-app/gradio/pull/915) +- Allow `Blocks` instances to be used like a `Block` in other `Blocks` by [@abidlabs](https://github.com/abidlabs) in [PR 919](https://github.com/gradio-app/gradio/pull/919) +- Redesign 1 by [@pngwn](https://github.com/pngwn) in [PR 918](https://github.com/gradio-app/gradio/pull/918) +- blocks-components-tests by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 904](https://github.com/gradio-app/gradio/pull/904) +- fix unit + browser tests by [@pngwn](https://github.com/pngwn) in [PR 926](https://github.com/gradio-app/gradio/pull/926) +- blocks-move-test-data by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 927](https://github.com/gradio-app/gradio/pull/927) +- remove debounce from form inputs by [@pngwn](https://github.com/pngwn) in [PR 932](https://github.com/gradio-app/gradio/pull/932) +- reimplement webcam video by [@pngwn](https://github.com/pngwn) in [PR 928](https://github.com/gradio-app/gradio/pull/928) +- blocks-move-test-data by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 941](https://github.com/gradio-app/gradio/pull/941) +- allow audio components to take a string value by [@pngwn](https://github.com/pngwn) in [PR 930](https://github.com/gradio-app/gradio/pull/930) +- static mode for textbox by [@pngwn](https://github.com/pngwn) in [PR 929](https://github.com/gradio-app/gradio/pull/929) +- fix file upload text by [@pngwn](https://github.com/pngwn) in [PR 931](https://github.com/gradio-app/gradio/pull/931) +- tabbed-interface-rewritten by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 958](https://github.com/gradio-app/gradio/pull/958) +- Gan demo fix by [@abidlabs](https://github.com/abidlabs) in [PR 965](https://github.com/gradio-app/gradio/pull/965) +- Blocks analytics by [@abidlabs](https://github.com/abidlabs) in [PR 947](https://github.com/gradio-app/gradio/pull/947) +- Blocks page load by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 963](https://github.com/gradio-app/gradio/pull/963) +- add frontend for page load events by [@pngwn](https://github.com/pngwn) in [PR 967](https://github.com/gradio-app/gradio/pull/967) +- fix i18n and some tweaks by [@pngwn](https://github.com/pngwn) in [PR 966](https://github.com/gradio-app/gradio/pull/966) +- add jinja2 to reqs by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 969](https://github.com/gradio-app/gradio/pull/969) +- Cleaning up `Launchable()` by [@abidlabs](https://github.com/abidlabs) in [PR 968](https://github.com/gradio-app/gradio/pull/968) +- Fix #944 by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 971](https://github.com/gradio-app/gradio/pull/971) +- New Blocks Demo: neural instrument cloning by [@abidlabs](https://github.com/abidlabs) in [PR 975](https://github.com/gradio-app/gradio/pull/975) +- Add huggingface_hub client library by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 973](https://github.com/gradio-app/gradio/pull/973) +- State and variables by [@aliabid94](https://github.com/aliabid94) in [PR 977](https://github.com/gradio-app/gradio/pull/977) +- update-components by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 986](https://github.com/gradio-app/gradio/pull/986) +- ensure dataframe updates as expected by [@pngwn](https://github.com/pngwn) in [PR 981](https://github.com/gradio-app/gradio/pull/981) +- test-guideline by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 990](https://github.com/gradio-app/gradio/pull/990) +- Issue #785: add footer by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 972](https://github.com/gradio-app/gradio/pull/972) +- indentation fix by [@abidlabs](https://github.com/abidlabs) in [PR 993](https://github.com/gradio-app/gradio/pull/993) +- missing quote by [@aliabd](https://github.com/aliabd) in [PR 996](https://github.com/gradio-app/gradio/pull/996) +- added interactive parameter to components by [@abidlabs](https://github.com/abidlabs) in [PR 992](https://github.com/gradio-app/gradio/pull/992) +- custom-components by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 985](https://github.com/gradio-app/gradio/pull/985) +- Refactor component shortcuts by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 995](https://github.com/gradio-app/gradio/pull/995) +- Plot Component by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 805](https://github.com/gradio-app/gradio/pull/805) +- updated PyPi version to 2.9.2 by [@abidlabs](https://github.com/abidlabs) in [PR 1002](https://github.com/gradio-app/gradio/pull/1002) +- Release 2.9.3 by [@abidlabs](https://github.com/abidlabs) in [PR 1003](https://github.com/gradio-app/gradio/pull/1003) +- Image3D Examples Fix by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1001](https://github.com/gradio-app/gradio/pull/1001) +- release 2.9.4 by [@abidlabs](https://github.com/abidlabs) in [PR 1006](https://github.com/gradio-app/gradio/pull/1006) +- templates import hotfix by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1008](https://github.com/gradio-app/gradio/pull/1008) +- Progress indicator bar by [@aliabid94](https://github.com/aliabid94) in [PR 997](https://github.com/gradio-app/gradio/pull/997) +- Fixed image input for absolute path by [@JefferyChiang](https://github.com/JefferyChiang) in [PR 1004](https://github.com/gradio-app/gradio/pull/1004) +- Model3D + Plot Components by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1010](https://github.com/gradio-app/gradio/pull/1010) +- Gradio Guides: Creating CryptoPunks with GANs by [@NimaBoscarino](https://github.com/NimaBoscarino) in [PR 1000](https://github.com/gradio-app/gradio/pull/1000) +- [BIG PR] Gradio blocks & redesigned components by [@abidlabs](https://github.com/abidlabs) in [PR 880](https://github.com/gradio-app/gradio/pull/880) +- fixed failing test on main by [@abidlabs](https://github.com/abidlabs) in [PR 1023](https://github.com/gradio-app/gradio/pull/1023) +- Use smaller ASR model in external test by [@abidlabs](https://github.com/abidlabs) in [PR 1024](https://github.com/gradio-app/gradio/pull/1024) +- updated PyPi version to 2.9.0b by [@abidlabs](https://github.com/abidlabs) in [PR 1026](https://github.com/gradio-app/gradio/pull/1026) +- Fixing import issues so that the package successfully installs on colab notebooks by [@abidlabs](https://github.com/abidlabs) in [PR 1027](https://github.com/gradio-app/gradio/pull/1027) +- Update website tracker slackbot by [@aliabd](https://github.com/aliabd) in [PR 1037](https://github.com/gradio-app/gradio/pull/1037) +- textbox-autoheight by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1009](https://github.com/gradio-app/gradio/pull/1009) +- Model3D Examples fixes by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1035](https://github.com/gradio-app/gradio/pull/1035) +- GAN Gradio Guide: Adjustments to iframe heights by [@NimaBoscarino](https://github.com/NimaBoscarino) in [PR 1042](https://github.com/gradio-app/gradio/pull/1042) +- added better default labels to form components by [@abidlabs](https://github.com/abidlabs) in [PR 1040](https://github.com/gradio-app/gradio/pull/1040) +- Slackbot web tracker fix by [@aliabd](https://github.com/aliabd) in [PR 1043](https://github.com/gradio-app/gradio/pull/1043) +- Plot fixes by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1044](https://github.com/gradio-app/gradio/pull/1044) +- Small fixes to the demos by [@abidlabs](https://github.com/abidlabs) in [PR 1030](https://github.com/gradio-app/gradio/pull/1030) +- fixing demo issue with website by [@aliabd](https://github.com/aliabd) in [PR 1047](https://github.com/gradio-app/gradio/pull/1047) +- [hotfix] HighlightedText by [@aliabid94](https://github.com/aliabid94) in [PR 1046](https://github.com/gradio-app/gradio/pull/1046) +- Update text by [@ronvoluted](https://github.com/ronvoluted) in [PR 1050](https://github.com/gradio-app/gradio/pull/1050) +- Update CONTRIBUTING.md by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1052](https://github.com/gradio-app/gradio/pull/1052) +- fix(ui): Increase contrast for footer by [@ronvoluted](https://github.com/ronvoluted) in [PR 1048](https://github.com/gradio-app/gradio/pull/1048) +- UI design update by [@gary149](https://github.com/gary149) in [PR 1041](https://github.com/gradio-app/gradio/pull/1041) +- updated PyPi version to 2.9.0b8 by [@abidlabs](https://github.com/abidlabs) in [PR 1059](https://github.com/gradio-app/gradio/pull/1059) +- Running, testing, and fixing demos by [@abidlabs](https://github.com/abidlabs) in [PR 1060](https://github.com/gradio-app/gradio/pull/1060) +- Form layout by [@pngwn](https://github.com/pngwn) in [PR 1054](https://github.com/gradio-app/gradio/pull/1054) +- inputless-interfaces by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1038](https://github.com/gradio-app/gradio/pull/1038) +- Update PULL_REQUEST_TEMPLATE.md by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1068](https://github.com/gradio-app/gradio/pull/1068) +- Upgrading node memory to 4gb in website Docker by [@aliabd](https://github.com/aliabd) in [PR 1069](https://github.com/gradio-app/gradio/pull/1069) +- Website reload error by [@aliabd](https://github.com/aliabd) in [PR 1079](https://github.com/gradio-app/gradio/pull/1079) +- fixed favicon issue by [@abidlabs](https://github.com/abidlabs) in [PR 1064](https://github.com/gradio-app/gradio/pull/1064) +- remove-queue-from-events by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1056](https://github.com/gradio-app/gradio/pull/1056) +- Enable vertex colors for OBJs files by [@radames](https://github.com/radames) in [PR 1074](https://github.com/gradio-app/gradio/pull/1074) +- Dark text by [@ronvoluted](https://github.com/ronvoluted) in [PR 1049](https://github.com/gradio-app/gradio/pull/1049) +- Scroll to output by [@pngwn](https://github.com/pngwn) in [PR 1077](https://github.com/gradio-app/gradio/pull/1077) +- Explicitly list pnpm version 6 in contributing guide by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1085](https://github.com/gradio-app/gradio/pull/1085) +- hotfix for encrypt issue by [@abidlabs](https://github.com/abidlabs) in [PR 1096](https://github.com/gradio-app/gradio/pull/1096) +- Release 2.9b9 by [@abidlabs](https://github.com/abidlabs) in [PR 1098](https://github.com/gradio-app/gradio/pull/1098) +- tweak node circleci settings by [@pngwn](https://github.com/pngwn) in [PR 1091](https://github.com/gradio-app/gradio/pull/1091) +- Website Reload Error by [@aliabd](https://github.com/aliabd) in [PR 1099](https://github.com/gradio-app/gradio/pull/1099) +- Website Reload: README in demos docker by [@aliabd](https://github.com/aliabd) in [PR 1100](https://github.com/gradio-app/gradio/pull/1100) +- Flagging fixes by [@abidlabs](https://github.com/abidlabs) in [PR 1081](https://github.com/gradio-app/gradio/pull/1081) +- Backend for optional labels by [@abidlabs](https://github.com/abidlabs) in [PR 1080](https://github.com/gradio-app/gradio/pull/1080) +- Optional labels fe by [@pngwn](https://github.com/pngwn) in [PR 1105](https://github.com/gradio-app/gradio/pull/1105) +- clean-deprecated-parameters by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1090](https://github.com/gradio-app/gradio/pull/1090) +- Blocks rendering fix by [@abidlabs](https://github.com/abidlabs) in [PR 1102](https://github.com/gradio-app/gradio/pull/1102) +- Redos #1106 by [@abidlabs](https://github.com/abidlabs) in [PR 1112](https://github.com/gradio-app/gradio/pull/1112) +- Interface types: handle input-only, output-only, and unified interfaces by [@abidlabs](https://github.com/abidlabs) in [PR 1108](https://github.com/gradio-app/gradio/pull/1108) +- Hotfix + New pypi release 2.9b11 by [@abidlabs](https://github.com/abidlabs) in [PR 1118](https://github.com/gradio-app/gradio/pull/1118) +- issue-checkbox by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1122](https://github.com/gradio-app/gradio/pull/1122) +- issue-checkbox-hotfix by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1127](https://github.com/gradio-app/gradio/pull/1127) +- Fix demos in website by [@aliabd](https://github.com/aliabd) in [PR 1130](https://github.com/gradio-app/gradio/pull/1130) +- Guide for Gradio ONNX model zoo on Huggingface by [@AK391](https://github.com/AK391) in [PR 1073](https://github.com/gradio-app/gradio/pull/1073) +- ONNX guide fixes by [@aliabd](https://github.com/aliabd) in [PR 1131](https://github.com/gradio-app/gradio/pull/1131) +- Stacked form inputs css by [@gary149](https://github.com/gary149) in [PR 1134](https://github.com/gradio-app/gradio/pull/1134) +- made default value in textbox empty string by [@abidlabs](https://github.com/abidlabs) in [PR 1135](https://github.com/gradio-app/gradio/pull/1135) +- Examples UI by [@gary149](https://github.com/gary149) in [PR 1121](https://github.com/gradio-app/gradio/pull/1121) +- Chatbot custom color support by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1092](https://github.com/gradio-app/gradio/pull/1092) +- highlighted text colors by [@pngwn](https://github.com/pngwn) in [PR 1119](https://github.com/gradio-app/gradio/pull/1119) +- pin to pnpm 6 for now by [@pngwn](https://github.com/pngwn) in [PR 1147](https://github.com/gradio-app/gradio/pull/1147) +- Restore queue in Blocks by [@aliabid94](https://github.com/aliabid94) in [PR 1137](https://github.com/gradio-app/gradio/pull/1137) +- add select event for tabitems by [@pngwn](https://github.com/pngwn) in [PR 1154](https://github.com/gradio-app/gradio/pull/1154) +- max_lines + autoheight for textbox by [@pngwn](https://github.com/pngwn) in [PR 1153](https://github.com/gradio-app/gradio/pull/1153) +- use color palette for chatbot by [@pngwn](https://github.com/pngwn) in [PR 1152](https://github.com/gradio-app/gradio/pull/1152) +- Timeseries improvements by [@pngwn](https://github.com/pngwn) in [PR 1149](https://github.com/gradio-app/gradio/pull/1149) +- move styling for interface panels to frontend by [@pngwn](https://github.com/pngwn) in [PR 1146](https://github.com/gradio-app/gradio/pull/1146) +- html tweaks by [@pngwn](https://github.com/pngwn) in [PR 1145](https://github.com/gradio-app/gradio/pull/1145) +- Issue #768: Support passing none to resize and crop image by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1144](https://github.com/gradio-app/gradio/pull/1144) +- image gallery component + img css by [@aliabid94](https://github.com/aliabid94) in [PR 1140](https://github.com/gradio-app/gradio/pull/1140) +- networking tweak by [@abidlabs](https://github.com/abidlabs) in [PR 1143](https://github.com/gradio-app/gradio/pull/1143) +- Allow enabling queue per event listener by [@aliabid94](https://github.com/aliabid94) in [PR 1155](https://github.com/gradio-app/gradio/pull/1155) +- config hotfix and v. 2.9b23 by [@abidlabs](https://github.com/abidlabs) in [PR 1158](https://github.com/gradio-app/gradio/pull/1158) +- Custom JS calls by [@aliabid94](https://github.com/aliabid94) in [PR 1082](https://github.com/gradio-app/gradio/pull/1082) +- Small fixes: queue default fix, ffmpeg installation message by [@abidlabs](https://github.com/abidlabs) in [PR 1159](https://github.com/gradio-app/gradio/pull/1159) +- formatting by [@abidlabs](https://github.com/abidlabs) in [PR 1161](https://github.com/gradio-app/gradio/pull/1161) +- enable flex grow for gr-box by [@radames](https://github.com/radames) in [PR 1165](https://github.com/gradio-app/gradio/pull/1165) +- 1148 loading by [@pngwn](https://github.com/pngwn) in [PR 1164](https://github.com/gradio-app/gradio/pull/1164) +- Put enable_queue kwarg back in launch() by [@aliabid94](https://github.com/aliabid94) in [PR 1167](https://github.com/gradio-app/gradio/pull/1167) +- A few small fixes by [@abidlabs](https://github.com/abidlabs) in [PR 1171](https://github.com/gradio-app/gradio/pull/1171) +- Hotfix for dropdown component by [@abidlabs](https://github.com/abidlabs) in [PR 1172](https://github.com/gradio-app/gradio/pull/1172) +- use secondary buttons in interface by [@pngwn](https://github.com/pngwn) in [PR 1173](https://github.com/gradio-app/gradio/pull/1173) +- 1183 component height by [@pngwn](https://github.com/pngwn) in [PR 1185](https://github.com/gradio-app/gradio/pull/1185) +- 962 dataframe by [@pngwn](https://github.com/pngwn) in [PR 1186](https://github.com/gradio-app/gradio/pull/1186) +- update-contributing by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1188](https://github.com/gradio-app/gradio/pull/1188) +- Table tweaks by [@pngwn](https://github.com/pngwn) in [PR 1195](https://github.com/gradio-app/gradio/pull/1195) +- wrap tab content in column by [@pngwn](https://github.com/pngwn) in [PR 1200](https://github.com/gradio-app/gradio/pull/1200) +- WIP: Add dark mode support by [@gary149](https://github.com/gary149) in [PR 1187](https://github.com/gradio-app/gradio/pull/1187) +- Restored /api/predict/ endpoint for Interfaces by [@abidlabs](https://github.com/abidlabs) in [PR 1199](https://github.com/gradio-app/gradio/pull/1199) +- hltext-label by [@pngwn](https://github.com/pngwn) in [PR 1204](https://github.com/gradio-app/gradio/pull/1204) +- add copy functionality to json by [@pngwn](https://github.com/pngwn) in [PR 1205](https://github.com/gradio-app/gradio/pull/1205) +- Update component config by [@aliabid94](https://github.com/aliabid94) in [PR 1089](https://github.com/gradio-app/gradio/pull/1089) +- fix placeholder prompt by [@pngwn](https://github.com/pngwn) in [PR 1215](https://github.com/gradio-app/gradio/pull/1215) +- ensure webcam video value is propagated correctly by [@pngwn](https://github.com/pngwn) in [PR 1218](https://github.com/gradio-app/gradio/pull/1218) +- Automatic word-break in highlighted text, combine_adjacent support by [@aliabid94](https://github.com/aliabid94) in [PR 1209](https://github.com/gradio-app/gradio/pull/1209) +- async-function-support by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1190](https://github.com/gradio-app/gradio/pull/1190) +- Sharing fix for assets by [@aliabid94](https://github.com/aliabid94) in [PR 1208](https://github.com/gradio-app/gradio/pull/1208) +- Hotfixes for course demos by [@abidlabs](https://github.com/abidlabs) in [PR 1222](https://github.com/gradio-app/gradio/pull/1222) +- Allow Custom CSS by [@aliabid94](https://github.com/aliabid94) in [PR 1170](https://github.com/gradio-app/gradio/pull/1170) +- share-hotfix by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1226](https://github.com/gradio-app/gradio/pull/1226) +- tweaks by [@pngwn](https://github.com/pngwn) in [PR 1229](https://github.com/gradio-app/gradio/pull/1229) +- white space for class concatenation by [@radames](https://github.com/radames) in [PR 1228](https://github.com/gradio-app/gradio/pull/1228) +- Tweaks by [@pngwn](https://github.com/pngwn) in [PR 1230](https://github.com/gradio-app/gradio/pull/1230) +- css tweaks by [@pngwn](https://github.com/pngwn) in [PR 1235](https://github.com/gradio-app/gradio/pull/1235) +- ensure defaults height match for media inputs by [@pngwn](https://github.com/pngwn) in [PR 1236](https://github.com/gradio-app/gradio/pull/1236) +- Default Label label value by [@radames](https://github.com/radames) in [PR 1239](https://github.com/gradio-app/gradio/pull/1239) +- update-shortcut-syntax by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1234](https://github.com/gradio-app/gradio/pull/1234) +- Update version.txt by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1244](https://github.com/gradio-app/gradio/pull/1244) +- Layout bugs by [@pngwn](https://github.com/pngwn) in [PR 1246](https://github.com/gradio-app/gradio/pull/1246) +- Update demo by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1253](https://github.com/gradio-app/gradio/pull/1253) +- Button default name by [@FarukOzderim](https://github.com/FarukOzderim) in [PR 1243](https://github.com/gradio-app/gradio/pull/1243) +- Labels spacing by [@gary149](https://github.com/gary149) in [PR 1254](https://github.com/gradio-app/gradio/pull/1254) +- add global loader for gradio app by [@pngwn](https://github.com/pngwn) in [PR 1251](https://github.com/gradio-app/gradio/pull/1251) +- ui apis for dalle-mini by [@pngwn](https://github.com/pngwn) in [PR 1258](https://github.com/gradio-app/gradio/pull/1258) +- Add precision to Number, backend only by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 1125](https://github.com/gradio-app/gradio/pull/1125) +- Website Design Changes by [@abidlabs](https://github.com/abidlabs) in [PR 1015](https://github.com/gradio-app/gradio/pull/1015) +- Small fixes for multiple demos compatible with 3.0 by [@radames](https://github.com/radames) in [PR 1257](https://github.com/gradio-app/gradio/pull/1257) +- Issue #1160: Model 3D component not destroyed correctly by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 1219](https://github.com/gradio-app/gradio/pull/1219) +- Fixes to components by [@abidlabs](https://github.com/abidlabs) in [PR 1260](https://github.com/gradio-app/gradio/pull/1260) +- layout docs by [@abidlabs](https://github.com/abidlabs) in [PR 1263](https://github.com/gradio-app/gradio/pull/1263) +- Static forms by [@pngwn](https://github.com/pngwn) in [PR 1264](https://github.com/gradio-app/gradio/pull/1264) +- Cdn assets by [@pngwn](https://github.com/pngwn) in [PR 1265](https://github.com/gradio-app/gradio/pull/1265) +- update logo by [@gary149](https://github.com/gary149) in [PR 1266](https://github.com/gradio-app/gradio/pull/1266) +- fix slider by [@aliabid94](https://github.com/aliabid94) in [PR 1268](https://github.com/gradio-app/gradio/pull/1268) +- maybe fix auth in iframes by [@pngwn](https://github.com/pngwn) in [PR 1261](https://github.com/gradio-app/gradio/pull/1261) +- Improves "Getting Started" guide by [@abidlabs](https://github.com/abidlabs) in [PR 1269](https://github.com/gradio-app/gradio/pull/1269) +- Add embedded demos to website by [@aliabid94](https://github.com/aliabid94) in [PR 1270](https://github.com/gradio-app/gradio/pull/1270) +- Label hotfixes by [@abidlabs](https://github.com/abidlabs) in [PR 1281](https://github.com/gradio-app/gradio/pull/1281) +- General tweaks by [@pngwn](https://github.com/pngwn) in [PR 1276](https://github.com/gradio-app/gradio/pull/1276) +- only affect links within the document by [@pngwn](https://github.com/pngwn) in [PR 1282](https://github.com/gradio-app/gradio/pull/1282) +- release 3.0b9 by [@abidlabs](https://github.com/abidlabs) in [PR 1283](https://github.com/gradio-app/gradio/pull/1283) +- Dm by [@pngwn](https://github.com/pngwn) in [PR 1284](https://github.com/gradio-app/gradio/pull/1284) +- Website fixes by [@aliabd](https://github.com/aliabd) in [PR 1286](https://github.com/gradio-app/gradio/pull/1286) +- Create Streamables by [@aliabid94](https://github.com/aliabid94) in [PR 1279](https://github.com/gradio-app/gradio/pull/1279) +- ensure table works on mobile by [@pngwn](https://github.com/pngwn) in [PR 1277](https://github.com/gradio-app/gradio/pull/1277) +- changes by [@aliabid94](https://github.com/aliabid94) in [PR 1287](https://github.com/gradio-app/gradio/pull/1287) +- demo alignment on landing page by [@aliabd](https://github.com/aliabd) in [PR 1288](https://github.com/gradio-app/gradio/pull/1288) +- New meta img by [@aliabd](https://github.com/aliabd) in [PR 1289](https://github.com/gradio-app/gradio/pull/1289) +- updated PyPi version to 3.0 by [@abidlabs](https://github.com/abidlabs) in [PR 1290](https://github.com/gradio-app/gradio/pull/1290) +- Fix site by [@aliabid94](https://github.com/aliabid94) in [PR 1291](https://github.com/gradio-app/gradio/pull/1291) +- Mobile responsive guides by [@aliabd](https://github.com/aliabd) in [PR 1293](https://github.com/gradio-app/gradio/pull/1293) +- Update readme by [@abidlabs](https://github.com/abidlabs) in [PR 1292](https://github.com/gradio-app/gradio/pull/1292) +- gif by [@abidlabs](https://github.com/abidlabs) in [PR 1296](https://github.com/gradio-app/gradio/pull/1296) +- Allow decoding headerless b64 string [@1lint](https://github.com/1lint) in [PR 4031](https://github.com/gradio-app/gradio/pull/4031) + +### Contributors Shoutout: + +- [@JefferyChiang](https://github.com/JefferyChiang) made their first contribution in [PR 1004](https://github.com/gradio-app/gradio/pull/1004) +- [@NimaBoscarino](https://github.com/NimaBoscarino) made their first contribution in [PR 1000](https://github.com/gradio-app/gradio/pull/1000) +- [@ronvoluted](https://github.com/ronvoluted) made their first contribution in [PR 1050](https://github.com/gradio-app/gradio/pull/1050) +- [@radames](https://github.com/radames) made their first contribution in [PR 1074](https://github.com/gradio-app/gradio/pull/1074) +- [@freddyaboulton](https://github.com/freddyaboulton) made their first contribution in [PR 1085](https://github.com/gradio-app/gradio/pull/1085) +- [@liteli1987gmail](https://github.com/liteli1987gmail) & [@chenglu](https://github.com/chenglu) made their first contribution in [PR 4767](https://github.com/gradio-app/gradio/pull/4767) \ No newline at end of file diff --git a/gradio/__init__.py b/gradio/__init__.py new file mode 100644 index 0000000..67c0ffc --- /dev/null +++ b/gradio/__init__.py @@ -0,0 +1,290 @@ +import json + +import gradio._simple_templates +import gradio.image_utils +import gradio.processing_utils +import gradio.templates +from gradio import components, layouts, mcp, themes, validators +from gradio.blocks import Blocks +from gradio.caching import Cache, cache +from gradio.chat_interface import ChatInterface +from gradio.cli import deploy +from gradio.components import ( + HTML, + JSON, + AnnotatedImage, + Annotatedimage, + Audio, + BarPlot, + BrowserState, + Button, + Chatbot, + ChatMessage, + Checkbox, + CheckboxGroup, + Checkboxgroup, + ClearButton, + Code, + ColorPicker, + Component, + DataFrame, + Dataframe, + Dataset, + DateTime, + DeepLinkButton, + Dialogue, + DownloadButton, + Dropdown, + DuplicateButton, + File, + FileExplorer, + Gallery, + Highlight, + HighlightedText, + Highlightedtext, + Image, + ImageEditor, + ImageSlider, + InputHTMLAttributes, + Json, + Label, + LinePlot, + LoginButton, + Markdown, + MessageDict, + Model3D, + MultimodalTextbox, + Navbar, + Number, + ParamViewer, + Plot, + Radio, + ScatterPlot, + Slider, + State, + Text, + Textbox, + Timer, + UploadButton, + Video, + WorkflowCanvas, + component, +) +from gradio.components.audio import WaveformOptions +from gradio.components.image_editor import ( + Brush, + Eraser, + LayerOptions, + WatermarkOptions, + WebcamOptions, +) +from gradio.data_classes import FileData +from gradio.events import ( + CopyData, + DeletedFileData, + DownloadData, + EditData, + EventData, + KeyUpData, + LikeData, + RetryData, + SelectData, + UndoData, + api, + on, +) +from gradio.exceptions import Error +from gradio.external import load, load_chat, load_openapi +from gradio.flagging import ( + CSVLogger, + FlaggingCallback, + SimpleCSVLogger, +) +from gradio.helpers import Info, Progress, Success, Warning, skip, update, validate +from gradio.helpers import create_examples as Examples # noqa: N812 +from gradio.i18n import I18n +from gradio.interface import Interface, TabbedInterface, close_all +from gradio.ipython_ext import load_ipython_extension +from gradio.layouts import ( + Accordion, + Column, + Draggable, + Group, + Row, + Sidebar, + Step, + Tab, + TabItem, + Tabs, + Walkthrough, +) +from gradio.media import get_audio, get_file, get_image, get_model3d, get_video +from gradio.oauth import OAuthProfile, OAuthToken +from gradio.renderable import render +from gradio.route_utils import Header +from gradio.routes import Request, mount_gradio_app +from gradio.server import Server +from gradio.templates import ( + Files, + ImageMask, + List, + Matrix, + Mic, + Microphone, + Numpy, + Paint, + PlayableVideo, + Sketchpad, + TextArea, +) +from gradio.themes import Base as Theme +from gradio.utils import NO_RELOAD, FileSize, get_package_version, set_static_paths +from gradio.workflow import Workflow + +# this is the version: +__version__ = get_package_version() + +__all__ = [ + "Accordion", + "AnnotatedImage", + "Server", + "Annotatedimage", + "Audio", + "BarPlot", + "Blocks", + "BrowserState", + "Brush", + "Button", + "CSVLogger", + "Cache", + "cache", + "ChatInterface", + "ChatMessage", + "Chatbot", + "Checkbox", + "CheckboxGroup", + "Checkboxgroup", + "ClearButton", + "Code", + "ColorPicker", + "Column", + "Component", + "CopyData", + "DataFrame", + "Dataframe", + "Dataset", + "DateTime", + "Dialogue", + "DeletedFileData", + "DownloadButton", + "DownloadData", + "Dropdown", + "DuplicateButton", + "EditData", + "Eraser", + "Error", + "EventData", + "Examples", + "File", + "FileData", + "FileExplorer", + "FileSize", + "Files", + "FlaggingCallback", + "Gallery", + "Group", + "Header", + "HTML", + "Highlight", + "HighlightedText", + "Highlightedtext", + "Image", + "ImageEditor", + "ImageSlider", + "ImageMask", + "Info", + "InputHTMLAttributes", + "Interface", + "JSON", + "Json", + "KeyUpData", + "Label", + "LayerOptions", + "LikeData", + "LinePlot", + "List", + "LoginButton", + "Markdown", + "Matrix", + "MessageDict", + "Mic", + "Microphone", + "Model3D", + "MultimodalTextbox", + "NO_RELOAD", + "Number", + "Numpy", + "OAuthProfile", + "OAuthToken", + "Paint", + "ParamViewer", + "PlayableVideo", + "Plot", + "Progress", + "Radio", + "Request", + "RetryData", + "Row", + "ScatterPlot", + "SelectData", + "Sidebar", + "SimpleCSVLogger", + "Sketchpad", + "Slider", + "State", + "Success", + "Tab", + "TabItem", + "TabbedInterface", + "Tabs", + "Text", + "TextArea", + "Textbox", + "Theme", + "Timer", + "UndoData", + "UploadButton", + "Video", + "Walkthrough", + "Step", + "Warning", + "WaveformOptions", + "WebcamOptions", + "WatermarkOptions", + "__version__", + "close_all", + "deploy", + "get_package_version", + "I18n", + "load", + "load_chat", + "load_ipython_extension", + "load_openapi", + "mount_gradio_app", + "on", + "render", + "set_static_paths", + "skip", + "update", + "DeepLinkButton", + "mcp", + "validate", + "get_audio", + "get_image", + "get_video", + "get_model3d", + "get_file", + "validators", + "Workflow", + "WorkflowCanvas", +] diff --git a/gradio/__main__.py b/gradio/__main__.py new file mode 100644 index 0000000..02e3d6c --- /dev/null +++ b/gradio/__main__.py @@ -0,0 +1,4 @@ +from gradio.cli import cli + +if __name__ == "__main__": + cli() diff --git a/gradio/_simple_templates/__init__.py b/gradio/_simple_templates/__init__.py new file mode 100644 index 0000000..bf5acb3 --- /dev/null +++ b/gradio/_simple_templates/__init__.py @@ -0,0 +1,5 @@ +from .simpledropdown import SimpleDropdown +from .simpleimage import SimpleImage +from .simpletextbox import SimpleTextbox + +__all__ = ["SimpleDropdown", "SimpleTextbox", "SimpleImage"] diff --git a/gradio/_simple_templates/simpledropdown.py b/gradio/_simple_templates/simpledropdown.py new file mode 100644 index 0000000..36d3af0 --- /dev/null +++ b/gradio/_simple_templates/simpledropdown.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import warnings +from collections.abc import Callable, Sequence +from typing import TYPE_CHECKING, Any, Literal + +from gradio.components.base import Component, FormComponent +from gradio.events import Events +from gradio.i18n import I18nData + +if TYPE_CHECKING: + from gradio.components import Timer + + +class SimpleDropdown(FormComponent): + """ + Creates a very simple dropdown listing choices from which entries can be selected. + """ + + EVENTS = [Events.change, Events.input, Events.select] + + def __init__( + self, + choices: list[str | int | float | tuple[str | I18nData, str | int | float]] + | None = None, + *, + value: str | int | float | Callable | 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, + 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", + ): + """ + Parameters: + choices: A list of string options to choose from. An option can also be a tuple of the form (name, value), where name is the displayed name of the dropdown choice and value is the value to be passed to the function, or returned by the function. + value: default value selected in dropdown. If None, no value is selected by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component. + label: the label for this component, displayed above the component if `show_label` is `True` and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component corresponds to. + info: additional component description, appears below the label in smaller font. Supports markdown / HTML syntax. + every: Continuously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer. + inputs: Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change. + inputs: Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change. + show_label: if True, will display label. + scale: relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True. + min_width: minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first. + interactive: if True, choices in this dropdown will be selectable; if False, selection will be disabled. If not provided, this is inferred based on whether the component is used as an input or output. + visible: If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + self.choices = ( + [tuple(c) if isinstance(c, (tuple, list)) else (str(c), c) for c in choices] + if choices + else [] + ) + super().__init__( + label=label, + info=info, + every=every, + inputs=inputs, + show_label=show_label, + scale=scale, + min_width=min_width, + interactive=interactive, + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + value=value, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) + + def api_info(self) -> dict[str, Any]: + return { + "type": "string", + "enum": [c[1] for c in self.choices], + } + + def example_payload(self) -> Any: + return self.choices[0][1] if self.choices else None + + def example_value(self) -> Any: + return self.choices[0][1] if self.choices else None + + def preprocess(self, payload: str | int | float | None) -> str | int | float | None: + """ + Parameters: + payload: the value of the selected dropdown choice + Returns: + Passes the value of the selected dropdown choice as a `str | int | float`. + """ + return payload + + def _warn_if_invalid_choice(self, y): + if y not in [value for _, value in self.choices]: + warnings.warn( + f"The value passed into gr.Dropdown() is not in the list of choices. Please update the list of choices to include: {y}." + ) + + def postprocess(self, value): + """ + Parameters: + value: Expects a `str | int | float` corresponding to the value of the dropdown entry to be selected. + Returns: + Returns the value of the selected dropdown entry. + """ + if value is None: + return None + self._warn_if_invalid_choice(value) + return value + + def process_example(self, value): + return next((c[0] for c in self.choices if c[1] == value), None) diff --git a/gradio/_simple_templates/simpleimage.py b/gradio/_simple_templates/simpleimage.py new file mode 100644 index 0000000..ca5c47e --- /dev/null +++ b/gradio/_simple_templates/simpleimage.py @@ -0,0 +1,121 @@ +"""gr.SimpleImage() component.""" + +from __future__ import annotations + +from collections.abc import Sequence +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal + +from gradio_client import handle_file +from gradio_client.documentation import document + +from gradio.components.base import Component +from gradio.data_classes import FileData +from gradio.events import Events +from gradio.i18n import I18nData + +if TYPE_CHECKING: + from gradio.components import Timer + + +@document() +class SimpleImage(Component): + """ + Creates an image component that can be used to upload images (as an input) or display images (as an output). + """ + + EVENTS = [ + Events.clear, + Events.change, + Events.upload, + ] + + data_model = FileData + + def __init__( + self, + value: 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, + show_download_button: bool = True, + 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", + ): + """ + Parameters: + value: A path or URL for the default value that SimpleImage component is going to take. If a function is provided, the function will be called each time the app loads to set the initial value of this component. + label: the label for this component, displayed above the component if `show_label` is `True` and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component corresponds to. + every: Continuously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer. + inputs: Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change. + show_label: if True, will display label. + show_download_button: If True, will display button to download image. + container: If True, will place the component in a container - providing some extra padding around the border. + scale: relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True. + min_width: minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first. + interactive: if True, will allow users to upload and edit an image; if False, can only be used to display images. If not provided, this is inferred based on whether the component is used as an input or output. + visible: If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + self.show_download_button = show_download_button + super().__init__( + label=label, + every=every, + inputs=inputs, + show_label=show_label, + container=container, + scale=scale, + min_width=min_width, + interactive=interactive, + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + preserved_by_key=preserved_by_key, + value=value, + ) + + def preprocess(self, payload: FileData | None) -> str | None: + """ + Parameters: + payload: A FileData object containing the image data. + Returns: + A `str` containing the path to the image. + """ + if payload is None: + return None + return payload.path + + def postprocess(self, value: str | Path | None) -> FileData | None: + """ + Parameters: + value: Expects a `str` or `pathlib.Path` object containing the path to the image. + Returns: + A FileData object containing the image data. + """ + if value is None: + return None + return FileData(path=str(value), orig_name=Path(value).name) + + def example_payload(self) -> Any: + return handle_file( + "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png" + ) + + def example_value(self) -> Any: + return "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png" diff --git a/gradio/_simple_templates/simpletextbox.py b/gradio/_simple_templates/simpletextbox.py new file mode 100644 index 0000000..ea32af3 --- /dev/null +++ b/gradio/_simple_templates/simpletextbox.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from collections.abc import Callable, Sequence +from typing import TYPE_CHECKING, Any, Literal + +from gradio.components.base import Component, FormComponent +from gradio.events import Events +from gradio.i18n import I18nData + +if TYPE_CHECKING: + from gradio.components import Timer + + +class SimpleTextbox(FormComponent): + """ + Creates a very simple textbox for user to enter string input or display string output. + """ + + EVENTS = [ + Events.change, + Events.input, + Events.submit, + ] + + def __init__( + self, + value: str | Callable | None = None, + *, + placeholder: 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, + scale: int | None = None, + min_width: int = 160, + interactive: bool | None = None, + visible: bool | Literal["hidden"] = True, + rtl: 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", + ): + """ + Parameters: + value: default text to provide in textbox. If a function is provided, the function will be called each time the app loads to set the initial value of this component. + placeholder: placeholder hint to provide behind textbox. + label: the label for this component, displayed above the component if `show_label` is `True` and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component corresponds to. + every: Continuously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer. + inputs: Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change. + show_label: if True, will display label. + scale: relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True. + min_width: minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first. + interactive: if True, will be rendered as an editable textbox; if False, editing will be disabled. If not provided, this is inferred based on whether the component is used as an input or output. + visible: If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM + rtl: If True and `type` is "text", sets the direction of the text to right-to-left (cursor appears on the left of the text). Default is False, which renders cursor on the right. + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + self.placeholder = placeholder + self.rtl = rtl + super().__init__( + label=label, + every=every, + inputs=inputs, + show_label=show_label, + scale=scale, + min_width=min_width, + interactive=interactive, + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + value=value, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) + + def preprocess(self, payload: str | None) -> str | None: + """ + Parameters: + payload: the text entered in the textarea. + Returns: + Passes text value as a {str} into the function. + """ + return None if payload is None else str(payload) + + def postprocess(self, value: str | None) -> str | None: + """ + Parameters: + value: Expects a {str} returned from function and sets textarea value to it. + Returns: + The value to display in the textarea. + """ + return None if value is None else str(value) + + def api_info(self) -> dict[str, Any]: + return {"type": "string"} + + def example_payload(self) -> Any: + return "Hello!!" + + def example_value(self) -> Any: + return "Hello!!" diff --git a/gradio/_vendor/__init__.py b/gradio/_vendor/__init__.py new file mode 100644 index 0000000..00ca497 --- /dev/null +++ b/gradio/_vendor/__init__.py @@ -0,0 +1 @@ +"""Vendored third-party Python dependencies used by Gradio.""" diff --git a/gradio/_vendor/aiofiles/__init__.py b/gradio/_vendor/aiofiles/__init__.py new file mode 100644 index 0000000..5f62158 --- /dev/null +++ b/gradio/_vendor/aiofiles/__init__.py @@ -0,0 +1,23 @@ +"""Utilities for asyncio-friendly file handling.""" + +from . import tempfile +from .threadpool import ( + open, + stderr, + stderr_bytes, + stdin, + stdin_bytes, + stdout, + stdout_bytes, +) + +__all__ = [ + "open", + "tempfile", + "stdin", + "stdout", + "stderr", + "stdin_bytes", + "stdout_bytes", + "stderr_bytes", +] diff --git a/gradio/_vendor/aiofiles/base.py b/gradio/_vendor/aiofiles/base.py new file mode 100644 index 0000000..178bd21 --- /dev/null +++ b/gradio/_vendor/aiofiles/base.py @@ -0,0 +1,70 @@ +"""Various base classes.""" + +from asyncio import get_running_loop +from collections.abc import Awaitable +from contextlib import AbstractAsyncContextManager + + +class AsyncBase: + def __init__(self, file, loop, executor): + self._file = file + self._executor = executor + self._ref_loop = loop + + @property + def _loop(self): + return self._ref_loop or get_running_loop() + + def __aiter__(self): + """We are our own iterator.""" + return self + + def __repr__(self): + return super().__repr__() + " wrapping " + repr(self._file) + + async def __anext__(self): + """Simulate normal file iteration.""" + line = await self.readline() + if line: + return line + else: + raise StopAsyncIteration + + +class AsyncIndirectBase(AsyncBase): + def __init__(self, name, loop, executor, indirect): + self._indirect = indirect + self._name = name + super().__init__(None, loop, executor) + + @property + def _file(self): + return self._indirect() + + @_file.setter + def _file(self, v): + pass # discard writes + + +class AiofilesContextManager(Awaitable, AbstractAsyncContextManager): + """An adjusted async context manager for aiofiles.""" + + __slots__ = ("_coro", "_obj") + + def __init__(self, coro): + self._coro = coro + self._obj = None + + def __await__(self): + if self._obj is None: + self._obj = yield from self._coro.__await__() + return self._obj + + async def __aenter__(self): + return await self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await get_running_loop().run_in_executor( + None, self._obj._file.__exit__, exc_type, exc_val, exc_tb + ) + self._obj = None diff --git a/gradio/_vendor/aiofiles/os.py b/gradio/_vendor/aiofiles/os.py new file mode 100644 index 0000000..92243fa --- /dev/null +++ b/gradio/_vendor/aiofiles/os.py @@ -0,0 +1,58 @@ +"""Async executor versions of file functions from the os module.""" + +import os + +from . import ospath as path +from .ospath import wrap + +__all__ = [ + "path", + "stat", + "rename", + "renames", + "replace", + "remove", + "unlink", + "mkdir", + "makedirs", + "rmdir", + "removedirs", + "symlink", + "readlink", + "listdir", + "scandir", + "access", + "wrap", + "getcwd", +] +if hasattr(os, "link"): + __all__ += ["link"] +if hasattr(os, "sendfile"): + __all__ += ["sendfile"] +if hasattr(os, "statvfs"): + __all__ += ["statvfs"] + + +stat = wrap(os.stat) +rename = wrap(os.rename) +renames = wrap(os.renames) +replace = wrap(os.replace) +remove = wrap(os.remove) +unlink = wrap(os.unlink) +mkdir = wrap(os.mkdir) +makedirs = wrap(os.makedirs) +rmdir = wrap(os.rmdir) +removedirs = wrap(os.removedirs) +symlink = wrap(os.symlink) +readlink = wrap(os.readlink) +listdir = wrap(os.listdir) +scandir = wrap(os.scandir) +access = wrap(os.access) +getcwd = wrap(os.getcwd) + +if hasattr(os, "link"): + link = wrap(os.link) +if hasattr(os, "sendfile"): + sendfile = wrap(os.sendfile) +if hasattr(os, "statvfs"): + statvfs = wrap(os.statvfs) diff --git a/gradio/_vendor/aiofiles/ospath.py b/gradio/_vendor/aiofiles/ospath.py new file mode 100644 index 0000000..387d68d --- /dev/null +++ b/gradio/_vendor/aiofiles/ospath.py @@ -0,0 +1,30 @@ +"""Async executor versions of file functions from the os.path module.""" + +import asyncio +from functools import partial, wraps +from os import path + + +def wrap(func): + @wraps(func) + async def run(*args, loop=None, executor=None, **kwargs): + if loop is None: + loop = asyncio.get_running_loop() + pfunc = partial(func, *args, **kwargs) + return await loop.run_in_executor(executor, pfunc) + + return run + + +exists = wrap(path.exists) +isfile = wrap(path.isfile) +isdir = wrap(path.isdir) +islink = wrap(path.islink) +ismount = wrap(path.ismount) +getsize = wrap(path.getsize) +getmtime = wrap(path.getmtime) +getatime = wrap(path.getatime) +getctime = wrap(path.getctime) +samefile = wrap(path.samefile) +sameopenfile = wrap(path.sameopenfile) +abspath = wrap(path.abspath) diff --git a/gradio/_vendor/aiofiles/tempfile/__init__.py b/gradio/_vendor/aiofiles/tempfile/__init__.py new file mode 100644 index 0000000..b9c0db0 --- /dev/null +++ b/gradio/_vendor/aiofiles/tempfile/__init__.py @@ -0,0 +1,357 @@ +import asyncio +import sys +from functools import partial, singledispatch +from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOBase +from tempfile import NamedTemporaryFile as syncNamedTemporaryFile +from tempfile import SpooledTemporaryFile as syncSpooledTemporaryFile +from tempfile import TemporaryDirectory as syncTemporaryDirectory +from tempfile import TemporaryFile as syncTemporaryFile +from tempfile import _TemporaryFileWrapper as syncTemporaryFileWrapper + +from ..base import AiofilesContextManager +from ..threadpool.binary import AsyncBufferedIOBase, AsyncBufferedReader, AsyncFileIO +from ..threadpool.text import AsyncTextIOWrapper +from .temptypes import AsyncSpooledTemporaryFile, AsyncTemporaryDirectory + +__all__ = [ + "NamedTemporaryFile", + "TemporaryFile", + "SpooledTemporaryFile", + "TemporaryDirectory", +] + + +# ================================================================ +# Public methods for async open and return of temp file/directory +# objects with async interface +# ================================================================ +if sys.version_info >= (3, 12): + + def NamedTemporaryFile( + mode="w+b", + buffering=-1, + encoding=None, + newline=None, + suffix=None, + prefix=None, + dir=None, + delete=True, + delete_on_close=True, + loop=None, + executor=None, + ): + """Async open a named temporary file""" + return AiofilesContextManager( + _temporary_file( + named=True, + mode=mode, + buffering=buffering, + encoding=encoding, + newline=newline, + suffix=suffix, + prefix=prefix, + dir=dir, + delete=delete, + delete_on_close=delete_on_close, + loop=loop, + executor=executor, + ) + ) + +else: + + def NamedTemporaryFile( + mode="w+b", + buffering=-1, + encoding=None, + newline=None, + suffix=None, + prefix=None, + dir=None, + delete=True, + loop=None, + executor=None, + ): + """Async open a named temporary file""" + return AiofilesContextManager( + _temporary_file( + named=True, + mode=mode, + buffering=buffering, + encoding=encoding, + newline=newline, + suffix=suffix, + prefix=prefix, + dir=dir, + delete=delete, + loop=loop, + executor=executor, + ) + ) + + +def TemporaryFile( + mode="w+b", + buffering=-1, + encoding=None, + newline=None, + suffix=None, + prefix=None, + dir=None, + loop=None, + executor=None, +): + """Async open an unnamed temporary file""" + return AiofilesContextManager( + _temporary_file( + named=False, + mode=mode, + buffering=buffering, + encoding=encoding, + newline=newline, + suffix=suffix, + prefix=prefix, + dir=dir, + loop=loop, + executor=executor, + ) + ) + + +def SpooledTemporaryFile( + max_size=0, + mode="w+b", + buffering=-1, + encoding=None, + newline=None, + suffix=None, + prefix=None, + dir=None, + loop=None, + executor=None, +): + """Async open a spooled temporary file""" + return AiofilesContextManager( + _spooled_temporary_file( + max_size=max_size, + mode=mode, + buffering=buffering, + encoding=encoding, + newline=newline, + suffix=suffix, + prefix=prefix, + dir=dir, + loop=loop, + executor=executor, + ) + ) + + +def TemporaryDirectory(suffix=None, prefix=None, dir=None, loop=None, executor=None): + """Async open a temporary directory""" + return AiofilesContextManagerTempDir( + _temporary_directory( + suffix=suffix, prefix=prefix, dir=dir, loop=loop, executor=executor + ) + ) + + +# ========================================================= +# Internal coroutines to open new temp files/directories +# ========================================================= +if sys.version_info >= (3, 12): + + async def _temporary_file( + named=True, + mode="w+b", + buffering=-1, + encoding=None, + newline=None, + suffix=None, + prefix=None, + dir=None, + delete=True, + delete_on_close=True, + loop=None, + executor=None, + max_size=0, + ): + """Async method to open a temporary file with async interface""" + if loop is None: + loop = asyncio.get_running_loop() + + if named: + cb = partial( + syncNamedTemporaryFile, + mode=mode, + buffering=buffering, + encoding=encoding, + newline=newline, + suffix=suffix, + prefix=prefix, + dir=dir, + delete=delete, + delete_on_close=delete_on_close, + ) + else: + cb = partial( + syncTemporaryFile, + mode=mode, + buffering=buffering, + encoding=encoding, + newline=newline, + suffix=suffix, + prefix=prefix, + dir=dir, + ) + + f = await loop.run_in_executor(executor, cb) + + # Wrap based on type of underlying IO object + if type(f) is syncTemporaryFileWrapper: + # _TemporaryFileWrapper was used (named files) + result = wrap(f.file, f, loop=loop, executor=executor) + result._closer = f._closer + return result + else: + # IO object was returned directly without wrapper + return wrap(f, f, loop=loop, executor=executor) + +else: + + async def _temporary_file( + named=True, + mode="w+b", + buffering=-1, + encoding=None, + newline=None, + suffix=None, + prefix=None, + dir=None, + delete=True, + loop=None, + executor=None, + max_size=0, + ): + """Async method to open a temporary file with async interface""" + if loop is None: + loop = asyncio.get_running_loop() + + if named: + cb = partial( + syncNamedTemporaryFile, + mode=mode, + buffering=buffering, + encoding=encoding, + newline=newline, + suffix=suffix, + prefix=prefix, + dir=dir, + delete=delete, + ) + else: + cb = partial( + syncTemporaryFile, + mode=mode, + buffering=buffering, + encoding=encoding, + newline=newline, + suffix=suffix, + prefix=prefix, + dir=dir, + ) + + f = await loop.run_in_executor(executor, cb) + + # Wrap based on type of underlying IO object + if type(f) is syncTemporaryFileWrapper: + # _TemporaryFileWrapper was used (named files) + result = wrap(f.file, f, loop=loop, executor=executor) + # add delete property + result.delete = f.delete + return result + else: + # IO object was returned directly without wrapper + return wrap(f, f, loop=loop, executor=executor) + + +async def _spooled_temporary_file( + max_size=0, + mode="w+b", + buffering=-1, + encoding=None, + newline=None, + suffix=None, + prefix=None, + dir=None, + loop=None, + executor=None, +): + """Open a spooled temporary file with async interface""" + if loop is None: + loop = asyncio.get_running_loop() + + cb = partial( + syncSpooledTemporaryFile, + max_size=max_size, + mode=mode, + buffering=buffering, + encoding=encoding, + newline=newline, + suffix=suffix, + prefix=prefix, + dir=dir, + ) + + f = await loop.run_in_executor(executor, cb) + + # Single interface provided by SpooledTemporaryFile for all modes + return AsyncSpooledTemporaryFile(f, loop=loop, executor=executor) + + +async def _temporary_directory( + suffix=None, prefix=None, dir=None, loop=None, executor=None +): + """Async method to open a temporary directory with async interface""" + if loop is None: + loop = asyncio.get_running_loop() + + cb = partial(syncTemporaryDirectory, suffix, prefix, dir) + f = await loop.run_in_executor(executor, cb) + + return AsyncTemporaryDirectory(f, loop=loop, executor=executor) + + +class AiofilesContextManagerTempDir(AiofilesContextManager): + """With returns the directory location, not the object (matching sync lib)""" + + async def __aenter__(self): + self._obj = await self._coro + return self._obj.name + + +@singledispatch +def wrap(base_io_obj, file, *, loop=None, executor=None): + """Wrap the object with interface based on type of underlying IO""" + raise TypeError(f"Unsupported IO type: {base_io_obj}") + + +@wrap.register(TextIOBase) +def _(base_io_obj, file, *, loop=None, executor=None): + return AsyncTextIOWrapper(file, loop=loop, executor=executor) + + +@wrap.register(BufferedWriter) +def _(base_io_obj, file, *, loop=None, executor=None): + return AsyncBufferedIOBase(file, loop=loop, executor=executor) + + +@wrap.register(BufferedReader) +@wrap.register(BufferedRandom) +def _(base_io_obj, file, *, loop=None, executor=None): + return AsyncBufferedReader(file, loop=loop, executor=executor) + + +@wrap.register(FileIO) +def _(base_io_obj, file, *, loop=None, executor=None): + return AsyncFileIO(file, loop=loop, executor=executor) diff --git a/gradio/_vendor/aiofiles/tempfile/temptypes.py b/gradio/_vendor/aiofiles/tempfile/temptypes.py new file mode 100644 index 0000000..e7d29ca --- /dev/null +++ b/gradio/_vendor/aiofiles/tempfile/temptypes.py @@ -0,0 +1,70 @@ +"""Async wrappers for spooled temp files and temp directory objects""" + +from functools import partial + +from ..base import AsyncBase +from ..threadpool.utils import ( + cond_delegate_to_executor, + delegate_to_executor, + proxy_property_directly, +) + + +@delegate_to_executor("fileno", "rollover") +@cond_delegate_to_executor( + "close", + "flush", + "isatty", + "read", + "readline", + "readlines", + "seek", + "tell", + "truncate", +) +@proxy_property_directly("closed", "encoding", "mode", "name", "newlines") +class AsyncSpooledTemporaryFile(AsyncBase): + """Async wrapper for SpooledTemporaryFile class""" + + async def _check(self): + if self._file._rolled: + return + max_size = self._file._max_size + if max_size and self._file.tell() > max_size: + await self.rollover() + + async def write(self, s): + """Implementation to anticipate rollover""" + if self._file._rolled: + cb = partial(self._file.write, s) + return await self._loop.run_in_executor(self._executor, cb) + else: + file = self._file._file # reference underlying base IO object + rv = file.write(s) + await self._check() + return rv + + async def writelines(self, iterable): + """Implementation to anticipate rollover""" + if self._file._rolled: + cb = partial(self._file.writelines, iterable) + return await self._loop.run_in_executor(self._executor, cb) + else: + file = self._file._file # reference underlying base IO object + rv = file.writelines(iterable) + await self._check() + return rv + + +@delegate_to_executor("cleanup") +@proxy_property_directly("name") +class AsyncTemporaryDirectory: + """Async wrapper for TemporaryDirectory class""" + + def __init__(self, file, loop, executor): + self._file = file + self._loop = loop + self._executor = executor + + async def close(self): + await self.cleanup() diff --git a/gradio/_vendor/aiofiles/threadpool/__init__.py b/gradio/_vendor/aiofiles/threadpool/__init__.py new file mode 100644 index 0000000..3ebbc48 --- /dev/null +++ b/gradio/_vendor/aiofiles/threadpool/__init__.py @@ -0,0 +1,140 @@ +"""Handle files using a thread pool executor.""" + +import asyncio +import sys +from functools import partial, singledispatch +from io import ( + BufferedIOBase, + BufferedRandom, + BufferedReader, + BufferedWriter, + FileIO, + TextIOBase, +) + +from ..base import AiofilesContextManager +from .binary import ( + AsyncBufferedIOBase, + AsyncBufferedReader, + AsyncFileIO, + AsyncIndirectBufferedIOBase, +) +from .text import AsyncTextIndirectIOWrapper, AsyncTextIOWrapper + +sync_open = open + +__all__ = ( + "open", + "stdin", + "stdout", + "stderr", + "stdin_bytes", + "stdout_bytes", + "stderr_bytes", +) + + +def open( + file, + mode="r", + buffering=-1, + encoding=None, + errors=None, + newline=None, + closefd=True, + opener=None, + *, + loop=None, + executor=None, +): + return AiofilesContextManager( + _open( + file, + mode=mode, + buffering=buffering, + encoding=encoding, + errors=errors, + newline=newline, + closefd=closefd, + opener=opener, + loop=loop, + executor=executor, + ) + ) + + +async def _open( + file, + mode="r", + buffering=-1, + encoding=None, + errors=None, + newline=None, + closefd=True, + opener=None, + *, + loop=None, + executor=None, +): + """Open an asyncio file.""" + if loop is None: + loop = asyncio.get_running_loop() + cb = partial( + sync_open, + file, + mode=mode, + buffering=buffering, + encoding=encoding, + errors=errors, + newline=newline, + closefd=closefd, + opener=opener, + ) + f = await loop.run_in_executor(executor, cb) + + return wrap(f, loop=loop, executor=executor) + + +@singledispatch +def wrap(file, *, loop=None, executor=None): + raise TypeError(f"Unsupported io type: {file}.") + + +@wrap.register(TextIOBase) +def _(file, *, loop=None, executor=None): + return AsyncTextIOWrapper(file, loop=loop, executor=executor) + + +@wrap.register(BufferedWriter) +@wrap.register(BufferedIOBase) +def _(file, *, loop=None, executor=None): + return AsyncBufferedIOBase(file, loop=loop, executor=executor) + + +@wrap.register(BufferedReader) +@wrap.register(BufferedRandom) +def _(file, *, loop=None, executor=None): + return AsyncBufferedReader(file, loop=loop, executor=executor) + + +@wrap.register(FileIO) +def _(file, *, loop=None, executor=None): + return AsyncFileIO(file, loop=loop, executor=executor) + + +stdin = AsyncTextIndirectIOWrapper("sys.stdin", None, None, indirect=lambda: sys.stdin) +stdout = AsyncTextIndirectIOWrapper( + "sys.stdout", None, None, indirect=lambda: sys.stdout +) +stderr = AsyncTextIndirectIOWrapper( + "sys.stderr", None, None, indirect=lambda: sys.stderr +) +stdin_bytes = AsyncIndirectBufferedIOBase( + "sys.stdin.buffer", None, None, indirect=lambda: sys.stdin.buffer +) +stdout_bytes = AsyncIndirectBufferedIOBase( + "sys.stdout.buffer", None, None, indirect=lambda: sys.stdout.buffer +) +stderr_bytes = AsyncIndirectBufferedIOBase( + "sys.stderr.buffer", None, None, indirect=lambda: sys.stderr.buffer +) diff --git a/gradio/_vendor/aiofiles/threadpool/binary.py b/gradio/_vendor/aiofiles/threadpool/binary.py new file mode 100644 index 0000000..63fcaff --- /dev/null +++ b/gradio/_vendor/aiofiles/threadpool/binary.py @@ -0,0 +1,104 @@ +from ..base import AsyncBase, AsyncIndirectBase +from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly + + +@delegate_to_executor( + "close", + "flush", + "isatty", + "read", + "read1", + "readinto", + "readline", + "readlines", + "seek", + "seekable", + "tell", + "truncate", + "writable", + "write", + "writelines", +) +@proxy_method_directly("detach", "fileno", "readable") +@proxy_property_directly("closed", "raw", "name", "mode") +class AsyncBufferedIOBase(AsyncBase): + """The asyncio executor version of io.BufferedWriter and BufferedIOBase.""" + + +@delegate_to_executor("peek") +class AsyncBufferedReader(AsyncBufferedIOBase): + """The asyncio executor version of io.BufferedReader and Random.""" + + +@delegate_to_executor( + "close", + "flush", + "isatty", + "read", + "readall", + "readinto", + "readline", + "readlines", + "seek", + "seekable", + "tell", + "truncate", + "writable", + "write", + "writelines", +) +@proxy_method_directly("fileno", "readable") +@proxy_property_directly("closed", "name", "mode") +class AsyncFileIO(AsyncBase): + """The asyncio executor version of io.FileIO.""" + + +@delegate_to_executor( + "close", + "flush", + "isatty", + "read", + "read1", + "readinto", + "readline", + "readlines", + "seek", + "seekable", + "tell", + "truncate", + "writable", + "write", + "writelines", +) +@proxy_method_directly("detach", "fileno", "readable") +@proxy_property_directly("closed", "raw", "name", "mode") +class AsyncIndirectBufferedIOBase(AsyncIndirectBase): + """The indirect asyncio executor version of io.BufferedWriter and BufferedIOBase.""" + + +@delegate_to_executor("peek") +class AsyncIndirectBufferedReader(AsyncIndirectBufferedIOBase): + """The indirect asyncio executor version of io.BufferedReader and Random.""" + + +@delegate_to_executor( + "close", + "flush", + "isatty", + "read", + "readall", + "readinto", + "readline", + "readlines", + "seek", + "seekable", + "tell", + "truncate", + "writable", + "write", + "writelines", +) +@proxy_method_directly("fileno", "readable") +@proxy_property_directly("closed", "name", "mode") +class AsyncIndirectFileIO(AsyncIndirectBase): + """The indirect asyncio executor version of io.FileIO.""" diff --git a/gradio/_vendor/aiofiles/threadpool/text.py b/gradio/_vendor/aiofiles/threadpool/text.py new file mode 100644 index 0000000..0e62590 --- /dev/null +++ b/gradio/_vendor/aiofiles/threadpool/text.py @@ -0,0 +1,64 @@ +from ..base import AsyncBase, AsyncIndirectBase +from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly + + +@delegate_to_executor( + "close", + "flush", + "isatty", + "read", + "readable", + "readline", + "readlines", + "seek", + "seekable", + "tell", + "truncate", + "write", + "writable", + "writelines", +) +@proxy_method_directly("detach", "fileno", "readable") +@proxy_property_directly( + "buffer", + "closed", + "encoding", + "errors", + "line_buffering", + "newlines", + "name", + "mode", +) +class AsyncTextIOWrapper(AsyncBase): + """The asyncio executor version of io.TextIOWrapper.""" + + +@delegate_to_executor( + "close", + "flush", + "isatty", + "read", + "readable", + "readline", + "readlines", + "seek", + "seekable", + "tell", + "truncate", + "write", + "writable", + "writelines", +) +@proxy_method_directly("detach", "fileno", "readable") +@proxy_property_directly( + "buffer", + "closed", + "encoding", + "errors", + "line_buffering", + "newlines", + "name", + "mode", +) +class AsyncTextIndirectIOWrapper(AsyncIndirectBase): + """The indirect asyncio executor version of io.TextIOWrapper.""" diff --git a/gradio/_vendor/aiofiles/threadpool/utils.py b/gradio/_vendor/aiofiles/threadpool/utils.py new file mode 100644 index 0000000..5fd3bb9 --- /dev/null +++ b/gradio/_vendor/aiofiles/threadpool/utils.py @@ -0,0 +1,72 @@ +import functools + + +def delegate_to_executor(*attrs): + def cls_builder(cls): + for attr_name in attrs: + setattr(cls, attr_name, _make_delegate_method(attr_name)) + return cls + + return cls_builder + + +def proxy_method_directly(*attrs): + def cls_builder(cls): + for attr_name in attrs: + setattr(cls, attr_name, _make_proxy_method(attr_name)) + return cls + + return cls_builder + + +def proxy_property_directly(*attrs): + def cls_builder(cls): + for attr_name in attrs: + setattr(cls, attr_name, _make_proxy_property(attr_name)) + return cls + + return cls_builder + + +def cond_delegate_to_executor(*attrs): + def cls_builder(cls): + for attr_name in attrs: + setattr(cls, attr_name, _make_cond_delegate_method(attr_name)) + return cls + + return cls_builder + + +def _make_delegate_method(attr_name): + async def method(self, *args, **kwargs): + cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs) + return await self._loop.run_in_executor(self._executor, cb) + + return method + + +def _make_proxy_method(attr_name): + def method(self, *args, **kwargs): + return getattr(self._file, attr_name)(*args, **kwargs) + + return method + + +def _make_proxy_property(attr_name): + def proxy_property(self): + return getattr(self._file, attr_name) + + return property(proxy_property) + + +def _make_cond_delegate_method(attr_name): + """For spooled temp files, delegate only if rolled to file object""" + + async def method(self, *args, **kwargs): + if self._file._rolled: + cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs) + return await self._loop.run_in_executor(self._executor, cb) + else: + return getattr(self._file, attr_name)(*args, **kwargs) + + return method diff --git a/gradio/_vendor/ffmpy/__init__.py b/gradio/_vendor/ffmpy/__init__.py new file mode 100644 index 0000000..fc682bb --- /dev/null +++ b/gradio/_vendor/ffmpy/__init__.py @@ -0,0 +1,3 @@ +from .ffmpy import FFExecutableNotFoundError, FFmpeg, FFprobe, FFRuntimeError + +__all__ = ["FFmpeg", "FFprobe", "FFExecutableNotFoundError", "FFRuntimeError"] diff --git a/gradio/_vendor/ffmpy/ffmpy.py b/gradio/_vendor/ffmpy/ffmpy.py new file mode 100644 index 0000000..c0f4c79 --- /dev/null +++ b/gradio/_vendor/ffmpy/ffmpy.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +import errno +import itertools +import shlex +import subprocess +from collections.abc import Mapping, Sequence +from typing import IO, Any + +try: + from psutil import Popen # noqa: F401 + + popen: type[subprocess.Popen | Popen] +except ImportError: + popen = subprocess.Popen +else: + popen = Popen + + +class FFmpeg: + """Wrapper for various `FFmpeg `_ related applications (ffmpeg, + ffprobe). + """ + + def __init__( + self, + executable: str = "ffmpeg", + global_options: Sequence[str] | str | None = None, + inputs: Mapping[str, Sequence[str] | str | None] | None = None, + outputs: Mapping[str, Sequence[str] | str | None] | None = None, + ) -> None: + """Initialize FFmpeg command line wrapper. + + Compiles FFmpeg command line from passed arguments (executable path, options, inputs and + outputs). ``inputs`` and ``outputs`` are dictionares containing inputs/outputs as keys and + their respective options as values. One dictionary value (set of options) must be either a + single space separated string, or a list or strings without spaces (i.e. each part of the + option is a separate item of the list, the result of calling ``split()`` on the options + string). If the value is a list, it cannot be mixed, i.e. cannot contain items with spaces. + An exception are complex FFmpeg command lines that contain quotes: the quoted part must be + one string, even if it contains spaces (see *Examples* for more info). + For more info about FFmpeg command line format see `here + `_. + + :param str executable: path to ffmpeg executable; by default the ``ffmpeg`` command will be + searched for in the ``PATH``, but can be overridden with an absolute path to ``ffmpeg`` + executable + :param iterable global_options: global options passed to ``ffmpeg`` executable (e.g. + ``-y``, ``-v`` etc.); can be specified either as a list/tuple/set of strings, or one + space-separated string; by default no global options are passed + :param dict inputs: a dictionary specifying one or more input arguments as keys with their + corresponding options (either as a list of strings or a single space separated string) as + values + :param dict outputs: a dictionary specifying one or more output arguments as keys with their + corresponding options (either as a list of strings or a single space separated string) as + values + """ + self.executable = executable + self._cmd = [executable] + self._cmd += _normalize_options(global_options, split_mixed=True) + + if inputs is not None: + self._cmd += _merge_args_opts(inputs, add_minus_i_option=True) + + if outputs is not None: + self._cmd += _merge_args_opts(outputs) + + self.cmd = subprocess.list2cmdline(self._cmd) + self.process: subprocess.Popen | Popen | None = None + + def __repr__(self) -> str: + return f"<{self.__class__.__name__!r} {self.cmd!r}>" + + def run( + self, + input_data: bytes | None = None, + stdout: IO | int | None = None, + stderr: IO | int | None = None, + env: Mapping[str, str] | None = None, + **kwargs: Any, + ) -> tuple[bytes | None, bytes | None]: + """Execute FFmpeg command line. + + ``input_data`` can contain input for FFmpeg in case ``pipe`` protocol is used for input. + ``stdout`` and ``stderr`` specify where to redirect the ``stdout`` and ``stderr`` of the + process. By default no redirection is done, which means all output goes to running shell + (this mode should normally only be used for debugging purposes). If FFmpeg ``pipe`` protocol + is used for output, ``stdout`` must be redirected to a pipe by passing `subprocess.PIPE` as + ``stdout`` argument. You can pass custom environment to ffmpeg process with ``env``. + + Returns a 2-tuple containing ``stdout`` and ``stderr`` of the process. If there was no + redirection or if the output was redirected to e.g. `os.devnull`, the value returned will + be a tuple of two `None` values, otherwise it will contain the actual ``stdout`` and + ``stderr`` data returned by ffmpeg process. + + More info about ``pipe`` protocol `here `_. + + :param str input_data: input data for FFmpeg to deal with (audio, video etc.) as bytes (e.g. + the result of reading a file in binary mode) + :param stdout: redirect FFmpeg ``stdout`` there (default is `None` which means no + redirection) + :param stderr: redirect FFmpeg ``stderr`` there (default is `None` which means no + redirection) + :param env: custom environment for ffmpeg process + :param kwargs: any other keyword arguments to be forwarded to `subprocess.Popen + `_ + :return: a 2-tuple containing ``stdout`` and ``stderr`` of the process + :rtype: tuple + :raise: `FFRuntimeError` in case FFmpeg command exits with a non-zero code; + `FFExecutableNotFoundError` in case the executable path passed was not valid + """ + try: + self.process = popen( + self._cmd, + stdin=subprocess.PIPE, + stdout=stdout, + stderr=stderr, + env=env, + **kwargs, + ) + except OSError as e: + if e.errno == errno.ENOENT: + raise FFExecutableNotFoundError( + f"Executable '{self.executable}' not found" + ) + else: + raise + + o_stdout, o_stderr = self.process.communicate(input=input_data) + if self.process.returncode != 0: + raise FFRuntimeError(self.cmd, self.process.returncode, o_stdout, o_stderr) + + return o_stdout, o_stderr + + +class FFprobe(FFmpeg): + """Wrapper for `ffprobe `_.""" + + def __init__( + self, + executable: str = "ffprobe", + global_options: Sequence[str] | str | None = None, + inputs: Mapping[str, Sequence[str] | str | None] | None = None, + ) -> None: + """Create an instance of FFprobe. + + Compiles FFprobe command line from passed arguments (executable path, options, inputs). + FFprobe executable by default is taken from ``PATH`` but can be overridden with an + absolute path. For more info about FFprobe command line format see + `here `_. + + :param str executable: absolute path to ffprobe executable + :param iterable global_options: global options passed to ffmpeg executable; can be specified + either as a list/tuple of strings or a space-separated string + :param dict inputs: a dictionary specifying one or more inputs as keys with their + corresponding options as values + """ + super().__init__( + executable=executable, global_options=global_options, inputs=inputs + ) + + +class FFExecutableNotFoundError(Exception): + """Raise when FFmpeg/FFprobe executable was not found.""" + + +class FFRuntimeError(Exception): + """Raise when FFmpeg/FFprobe command line execution returns a non-zero exit code. + + The resulting exception object will contain the attributes relates to command line execution: + ``cmd``, ``exit_code``, ``stdout``, ``stderr``. + """ + + def __init__( + self, + cmd: str, + exit_code: int, + stdout: bytes | str | None, + stderr: bytes | str | None, + ) -> None: + self.cmd = cmd + self.exit_code = exit_code + self.stdout = stdout + self.stderr = stderr + + stdout_display = _safe_decode(stdout) + stderr_display = _safe_decode(stderr) + + message = f"`{self.cmd}` exited with status {exit_code}\n\nSTDOUT:\n{stdout_display}\n\nSTDERR:\n{stderr_display}" + + super().__init__(message) + + +def _merge_args_opts( + args_opts_dict: Mapping[str, Sequence[str] | str | None], + add_minus_i_option: bool = False, +) -> list[str]: + """Merge options with their corresponding arguments. + + Iterates over the dictionary holding arguments (keys) and options (values). Merges each + options string with its corresponding argument. + + :param dict args_opts_dict: a dictionary of arguments and options + :param dict kwargs: *input_option* - if specified prepends ``-i`` to input argument + :return: merged list of strings with arguments and their corresponding options + :rtype: list + """ + merged: list[str] = [] + + for arg, opt in args_opts_dict.items(): + merged += _normalize_options(opt) + + if not arg: + continue + + if add_minus_i_option: + merged.append("-i") + + merged.append(arg) + + return merged + + +def _normalize_options( + options: Sequence[str] | str | None, split_mixed: bool = False +) -> list[str]: + """Normalize options string or list of strings. + + Splits `options` into a list of strings. If `split_mixed` is `True`, splits (flattens) mixed + options (i.e. list of strings with spaces) into separate items. + + :param options: options string or list of strings + :param bool split_mixed: whether to split mixed options into separate items + """ + if options is None: + return [] + elif isinstance(options, str): + return shlex.split(options) + elif split_mixed: + return list(itertools.chain(*[shlex.split(o) for o in options])) + else: + return list(options) + + +def _safe_decode(stream_data: bytes | str | None) -> str: + """Convert FFmpeg output to text for error messages.""" + if stream_data is None: + return "" + if isinstance(stream_data, bytes): + return stream_data.decode(errors="replace") + return stream_data diff --git a/gradio/_vendor/ffmpy/py.typed b/gradio/_vendor/ffmpy/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/gradio/_vendor/licenses/aiofiles.LICENSE b/gradio/_vendor/licenses/aiofiles.LICENSE new file mode 100644 index 0000000..e06d208 --- /dev/null +++ b/gradio/_vendor/licenses/aiofiles.LICENSE @@ -0,0 +1,202 @@ +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. + diff --git a/gradio/_vendor/licenses/aiofiles.NOTICE b/gradio/_vendor/licenses/aiofiles.NOTICE new file mode 100644 index 0000000..d134f28 --- /dev/null +++ b/gradio/_vendor/licenses/aiofiles.NOTICE @@ -0,0 +1,2 @@ +Asyncio support for files +Copyright 2016 Tin Tvrtkovic diff --git a/gradio/_vendor/licenses/ffmpy.LICENSE b/gradio/_vendor/licenses/ffmpy.LICENSE new file mode 100644 index 0000000..eda4fa4 --- /dev/null +++ b/gradio/_vendor/licenses/ffmpy.LICENSE @@ -0,0 +1,18 @@ +Copyright 2016-2024 Andrii Yurchuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/gradio/_workflow_curated_snapshot.json b/gradio/_workflow_curated_snapshot.json new file mode 100644 index 0000000..858b2e1 --- /dev/null +++ b/gradio/_workflow_curated_snapshot.json @@ -0,0 +1,383 @@ +{ + "snapshot_version": 1, + "fetched_at": "2026-06-09T00:00:00Z", + "items": [ + { + "kind": "space", + "id": "black-forest-labs/FLUX.1-schnell", + "task": "text-to-image", + "space_category": "image-generation", + "modality": "image", + "title": "FLUX.1 schnell", + "description": "Fast, high-quality text-to-image", + "added_at": "2026-04-01T00:00:00Z", + "featured": true, + "zero_gpu": true, + "smoke_inputs": { "prompt": "a small red square" }, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "latency_ms": 1800, + "error": null + } + }, + { + "kind": "space", + "id": "stabilityai/stable-diffusion-3.5-large", + "task": "text-to-image", + "space_category": "image-generation", + "modality": "image", + "title": "Stable Diffusion 3.5 Large", + "description": "High-fidelity text-to-image", + "added_at": "2026-04-01T00:00:00Z", + "featured": true, + "zero_gpu": true, + "smoke_inputs": { "prompt": "a small red square" }, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "latency_ms": 3200, + "error": null + } + }, + { + "kind": "space", + "id": "briaai/BRIA-RMBG-2.0", + "task": "image-to-image", + "space_category": "image-editing", + "modality": "image", + "title": "BRIA Background Removal", + "description": "Remove image backgrounds", + "added_at": "2026-04-01T00:00:00Z", + "featured": true, + "zero_gpu": true, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "latency_ms": 1100, + "error": null + } + }, + { + "kind": "space", + "id": "multimodalart/face-to-all", + "task": "image-to-image", + "space_category": "image-editing", + "modality": "image", + "title": "Face to All", + "description": "Stylize a face photo into many art styles", + "added_at": "2026-04-02T00:00:00Z", + "featured": false, + "zero_gpu": true, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "latency_ms": 2400, + "error": null + } + }, + { + "kind": "model", + "id": "Salesforce/blip-image-captioning-large", + "task": "image-to-text", + "modality": "image", + "title": "BLIP Image Captioning", + "description": "Generate captions from images", + "added_at": "2026-04-02T00:00:00Z", + "featured": false, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "model", + "id": "facebook/detr-resnet-50", + "task": "object-detection", + "modality": "image", + "title": "DETR ResNet-50", + "description": "Open-vocabulary object detection", + "added_at": "2026-04-02T00:00:00Z", + "featured": true, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "model", + "id": "facebook/sam-vit-base", + "task": "image-segmentation", + "modality": "image", + "title": "Segment Anything (SAM)", + "description": "Segment any region of an image", + "added_at": "2026-04-02T00:00:00Z", + "featured": true, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "model", + "id": "google/vit-base-patch16-224", + "task": "image-classification", + "modality": "image", + "title": "ViT Base", + "description": "Image classification (ImageNet)", + "added_at": "2026-04-02T00:00:00Z", + "featured": false, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "model", + "id": "depth-anything/Depth-Anything-V2-Small-hf", + "task": "depth-estimation", + "modality": "image", + "title": "Depth Anything V2", + "description": "Monocular depth estimation", + "added_at": "2026-04-02T00:00:00Z", + "featured": true, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "space", + "id": "facebook/MusicGen", + "task": "text-to-audio", + "space_category": "music-generation", + "modality": "audio", + "title": "MusicGen", + "description": "Generate music from a text prompt", + "added_at": "2026-04-02T00:00:00Z", + "featured": false, + "zero_gpu": true, + "smoke_inputs": { "prompt": "calm piano" }, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "latency_ms": 4200, + "error": null + } + }, + { + "kind": "model", + "id": "facebook/bart-large-cnn", + "task": "summarization", + "modality": "text", + "title": "BART CNN Summarizer", + "description": "News-style text summarization", + "added_at": "2026-04-02T00:00:00Z", + "featured": false, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "model", + "id": "Helsinki-NLP/opus-mt-en-fr", + "task": "translation", + "modality": "text", + "title": "Opus EN→FR", + "description": "English-to-French translation", + "added_at": "2026-04-02T00:00:00Z", + "featured": false, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "model", + "id": "deepset/roberta-base-squad2", + "task": "question-answering", + "modality": "text", + "title": "RoBERTa QA", + "description": "Extractive question answering", + "added_at": "2026-04-02T00:00:00Z", + "featured": false, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "space", + "id": "tencent/Hunyuan3D-2", + "task": "image-to-3d", + "space_category": "3d-modeling", + "modality": "3d", + "title": "Hunyuan3D 2", + "description": "Generate 3D meshes from a single image", + "added_at": "2026-04-02T00:00:00Z", + "featured": true, + "zero_gpu": true, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "latency_ms": 5200, + "error": null + } + }, + { + "kind": "space", + "id": "Wan-AI/Wan2.2-TI2V-5B", + "task": "text-to-video", + "space_category": "video-generation", + "modality": "video", + "title": "Wan 2.2 TI2V 5B", + "description": "Text-to-video generation", + "added_at": "2026-04-02T00:00:00Z", + "featured": true, + "zero_gpu": true, + "smoke_inputs": { "prompt": "a flower opening" }, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "latency_ms": 8800, + "error": null + } + }, + { + "kind": "space", + "id": "hf-audio/whisper-large-v3-turbo", + "task": "automatic-speech-recognition", + "space_category": "automatic-speech-recognition", + "modality": "audio", + "title": "Whisper Large v3 Turbo", + "description": "Fast speech-to-text", + "added_at": "2026-04-01T00:00:00Z", + "featured": true, + "zero_gpu": true, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "latency_ms": 900, + "error": null + } + }, + { + "kind": "space", + "id": "coqui/xtts", + "task": "text-to-speech", + "space_category": "speech-synthesis", + "modality": "audio", + "title": "Coqui XTTS", + "description": "Multilingual text-to-speech with voice cloning", + "added_at": "2026-04-01T00:00:00Z", + "featured": true, + "zero_gpu": true, + "smoke_inputs": { "text": "hello" }, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "latency_ms": 2100, + "error": null + } + }, + { + "kind": "model", + "id": "black-forest-labs/FLUX.1-schnell", + "task": "text-to-image", + "modality": "image", + "title": "FLUX.1 schnell", + "description": "Text-to-image", + "added_at": "2026-04-01T00:00:00Z", + "featured": true, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "model", + "id": "stabilityai/stable-diffusion-3.5-large-turbo", + "task": "text-to-image", + "modality": "image", + "title": "SD 3.5 Large Turbo", + "description": "Fast text-to-image", + "added_at": "2026-04-01T00:00:00Z", + "featured": true, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "model", + "id": "Qwen/Qwen2.5-VL-7B-Instruct", + "task": "image-to-text", + "modality": "image", + "title": "Qwen2.5-VL 7B", + "description": "Image understanding & captioning", + "added_at": "2026-04-01T00:00:00Z", + "featured": false, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "model", + "id": "openai/whisper-large-v3-turbo", + "task": "automatic-speech-recognition", + "modality": "audio", + "title": "Whisper Large v3 Turbo", + "description": "Speech-to-text", + "added_at": "2026-04-01T00:00:00Z", + "featured": true, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "model", + "id": "meta-llama/Llama-3.2-3B-Instruct", + "task": "text-generation", + "modality": "text", + "title": "Llama 3.2 3B Instruct", + "description": "Open-weights text generation", + "added_at": "2026-04-01T00:00:00Z", + "featured": true, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + }, + { + "kind": "model", + "id": "Qwen/Qwen2.5-7B-Instruct", + "task": "text-generation", + "modality": "text", + "title": "Qwen 2.5 7B", + "description": "Multilingual text generation", + "added_at": "2026-04-01T00:00:00Z", + "featured": false, + "validation": { + "last_checked": "2026-06-08T03:14:00Z", + "status": "ok", + "error": null + } + } + ] +} diff --git a/gradio/analytics.py b/gradio/analytics.py new file mode 100644 index 0000000..9a57924 --- /dev/null +++ b/gradio/analytics.py @@ -0,0 +1,267 @@ +"""Functions related to analytics and telemetry.""" + +from __future__ import annotations + +import asyncio +import json +import os +import threading +import urllib.parse +import warnings +from typing import Any + +import httpx +from huggingface_hub.utils._telemetry import _send_telemetry_in_thread +from packaging.version import Version + +import gradio +from gradio.utils import core_gradio_components, get_package_version + +# For testability, we import the pyfetch function into this module scope and define a fallback coroutine object to be patched in tests. +try: + from pyodide.http import pyfetch as pyodide_pyfetch # type: ignore +except ImportError: + + async def pyodide_pyfetch(*_args, **_kwargs): + raise NotImplementedError( + "pyodide.http.pyfetch is not available in this environment." + ) + + +ANALYTICS_URL = "https://api.gradio.app/" +PKG_VERSION_URL = "https://api.gradio.app/pkg-version" + + +def get_block_name(class_name) -> str: + """ + This will return "matrix" for Matrix template, and ensures that any component name that is sent from the gradio app is part of the core components list (no false positives for custom components). + """ + return class_name.__name__.lower() + + +def analytics_enabled() -> bool: + """ + Returns: True if analytics are enabled, False otherwise. + """ + return os.getenv("GRADIO_ANALYTICS_ENABLED", "True") == "True" + + +def _do_analytics_request(topic: str, data: dict[str, Any]) -> None: + threading.Thread( + target=_do_normal_analytics_request, + kwargs={ + "topic": topic, + "data": data, + }, + ).start() + + +def _do_normal_analytics_request(topic: str, data: dict[str, Any]) -> None: + try: + _send_telemetry_in_thread( + topic=topic, + library_name="gradio", + library_version=data.get("version"), + user_agent=data, + ) + except Exception: + pass + + +async def _do_wasm_analytics_request(url: str, data: dict[str, Any]) -> None: + # We use urllib.parse.urlencode to encode the data as a form. + # Ref: https://docs.python.org/3/library/urllib.request.html#urllib-examples + body = urllib.parse.urlencode(data).encode("ascii") + headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + + try: + await asyncio.wait_for( + pyodide_pyfetch(url, method="POST", headers=headers, body=body), + timeout=5, + ) + except asyncio.TimeoutError: + pass # do not push analytics if no network + + +def version_check(): + try: + current_pkg_version = get_package_version() + latest_pkg_version = httpx.get(url=PKG_VERSION_URL, timeout=3).json()["version"] + if Version(latest_pkg_version) > Version(current_pkg_version): + warnings.warn( + f"IMPORTANT: You are using gradio version {current_pkg_version}, " + f"however version {latest_pkg_version} is available, please upgrade. \n" + f"--------" + ) + except json.decoder.JSONDecodeError: # type: ignore + warnings.warn("unable to parse version details from package URL.") + except KeyError: + warnings.warn("package URL does not contain version info.") + except Exception: + pass + + +def initiated_analytics(data: dict[str, Any]) -> None: + if not analytics_enabled(): + return + + topic = f"{ANALYTICS_URL}gradio-initiated-analytics/" + _do_analytics_request( + topic=topic, + data=data, + ) + + +def launched_analytics(blocks: gradio.Blocks, data: dict[str, Any]) -> None: + if not analytics_enabled(): + return + + ( + blocks_telemetry, + inputs_telemetry, + outputs_telemetry, + targets_telemetry, + events_telemetry, + ) = ( + [], + [], + [], + [], + [], + ) + + for x in list(blocks.blocks.values()): + blocks_telemetry.append(x.get_block_name()) + for x in blocks.fns.values(): + targets_telemetry = targets_telemetry + [ + # Sometimes the target can be the Blocks object itself, so we need to check if its in blocks.blocks + blocks.blocks[int(y[0])].get_block_name() + for y in x.targets + if y[0] in blocks.blocks + ] + events_telemetry = events_telemetry + [ + y[1] for y in x.targets if y[0] in blocks.blocks + ] + inputs_telemetry = inputs_telemetry + [ + blocks.blocks[int(y)].get_block_name() # type: ignore + for y in x.inputs + if y in blocks.blocks + ] + outputs_telemetry = outputs_telemetry + [ + blocks.blocks[int(y)].get_block_name() # type: ignore + for y in x.outputs + if y in blocks.blocks + ] + + def get_inputs_outputs( + mode: str, + components: list[gradio.components.Component] | None, + fallback: list[str], + ) -> list[str] | None: + if mode == "interface": + return [b.get_block_name() for b in components] if components else None + return fallback + + core_components = [get_block_name(c) for c in core_gradio_components()] + + additional_data = { + "version": get_package_version(), + "is_hosted_notebook": blocks.is_hosted_notebook, + "using_auth": blocks.auth is not None, + "dev_mode": blocks.dev_mode, + "inputs": get_inputs_outputs( + blocks.mode, blocks.input_components, inputs_telemetry + ), + "outputs": get_inputs_outputs( + blocks.mode, blocks.output_components, outputs_telemetry + ), + "targets": targets_telemetry, + "blocks": blocks_telemetry, + "events": events_telemetry, + } + custom_components = [b for b in blocks_telemetry if b not in core_components] + using_custom_component = len(custom_components) > 0 + additional_data["using_custom_component"] = using_custom_component + additional_data["custom_components"] = custom_components + + data.update(additional_data) + + topic = f"{ANALYTICS_URL}gradio-launched-telemetry/" + + _do_analytics_request(topic=topic, data=data) + + +def custom_component_analytics( + command: str, + template: str | None, + upload_pypi: bool | None, + upload_demo: bool | None, + upload_source: bool | None, + generate_docs: bool | None = None, + bump_version: bool | None = None, + npm_install: str | None = None, + python_path: str | None = None, + gradio_path: str | None = None, +) -> None: + data = { + "command": command, + "template": template, + "upload_pypi": upload_pypi, + "upload_demo": upload_demo, + "upload_source": upload_source, + "generate_docs": generate_docs, + "bump_version": bump_version, + "npm_install": npm_install, + "python_path": python_path, + "gradio_path": gradio_path, + } + if not analytics_enabled(): + return + + _do_analytics_request( + topic="gradio/custom-components", + data=data, + ) + + +def vibe_analytics() -> None: + data = { + "command": "vibe", + } + if not analytics_enabled(): + return + + _do_analytics_request( + topic="gradio/vibe", + data=data, + ) + + +def integration_analytics(data: dict[str, Any]) -> None: + if not analytics_enabled(): + return + + topic = f"{ANALYTICS_URL}gradio-integration-analytics/" + _do_analytics_request( + topic=topic, + data=data, + ) + + +def error_analytics(message: str) -> None: + """ + Send error analytics if there is network + Parameters: + message: Details about error + """ + if not analytics_enabled(): + return + + data = {"error": message} + topic = f"{ANALYTICS_URL}gradio-error-analytics/" + _do_analytics_request( + topic=topic, + data=data, + ) diff --git a/gradio/block_function.py b/gradio/block_function.py new file mode 100644 index 0000000..f782e54 --- /dev/null +++ b/gradio/block_function.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import inspect +from collections.abc import Callable, Sequence +from typing import TYPE_CHECKING, Literal + +from . import utils + +try: + import spaces # type: ignore +except Exception: + spaces = None # type: ignore[assignment] + + +if TYPE_CHECKING: # Only import for type checking (is False at runtime). + from gradio.components.base import Component + from gradio.renderable import Renderable + + from .blocks import BlockContext + + +class BlockFunction: + def __init__( + self, + fn: Callable | None, + inputs: Sequence[Component | BlockContext], + outputs: Sequence[Component | BlockContext], + preprocess: bool, + postprocess: bool, + inputs_as_dict: bool, + targets: list[tuple[int | None, str]], + _id: int, + batch: bool = False, + max_batch_size: int = 4, + concurrency_limit: int | None | Literal["default"] = "default", + concurrency_id: str | None = None, + tracks_progress: bool = False, + api_name: str | None = None, + api_description: str | None | Literal[False] = None, + js: str | Literal[True] | None = None, + show_progress: Literal["full", "minimal", "hidden"] = "full", + show_progress_on: Sequence[Component] | None = None, + cancels: list[int] | None = None, + collects_event_data: bool = False, + trigger_after: int | None = None, + trigger_only_on_success: bool = False, + trigger_only_on_failure: bool = False, + trigger_mode: Literal["always_last", "once", "multiple"] = "once", + queue: bool = True, + scroll_to_output: bool = False, + api_visibility: Literal["public", "private", "undocumented"] = "public", + renderable: Renderable | None = None, + rendered_in: Renderable | None = None, + render_iteration: int | None = None, + is_cancel_function: bool = False, + connection: Literal["stream", "sse"] = "sse", + time_limit: float | None = None, + stream_every: float = 0.5, + event_specific_args: list[str] | None = None, + component_prop_inputs: list[int] | None = None, + page: str = "", + js_implementation: str | None = None, + key: str | int | tuple[int | str, ...] | None = None, + validator: Callable | None = None, + ): + self.fn = fn + self._id = _id + self.inputs = inputs + self.outputs = outputs + self.preprocess = preprocess + self.postprocess = postprocess + self.tracks_progress = tracks_progress + self.concurrency_limit: int | None | Literal["default"] = concurrency_limit + self.concurrency_id = concurrency_id or str(id(fn)) + self.batch = batch + self.max_batch_size = max_batch_size + self.total_runtime = 0 + self.total_runs = 0 + self.inputs_as_dict = inputs_as_dict + self.targets = targets + self.name = getattr(fn, "__name__", "fn") if fn is not None else None + self.api_name = api_name + self.api_description = api_description + self.js = js + self.show_progress = show_progress + self.show_progress_on = show_progress_on + self.cancels = cancels or [] + self.collects_event_data = collects_event_data + self.trigger_after = trigger_after + self.trigger_only_on_success = trigger_only_on_success + self.trigger_only_on_failure = trigger_only_on_failure + self.trigger_mode = trigger_mode + self.queue = False if fn is None else queue + self.scroll_to_output = False if utils.get_space() else scroll_to_output + self.api_visibility = api_visibility + self.types_generator = inspect.isgeneratorfunction( + self.fn + ) or inspect.isasyncgenfunction(self.fn) + self.renderable = renderable + self.rendered_in = rendered_in + self.render_iteration = render_iteration + self.page = page + self.validator = validator + if js_implementation: + self.fn.__js_implementation__ = js_implementation # type: ignore + + # We need to keep track of which events are cancel events + # so that the client can call the /cancel route directly + self.is_cancel_function = is_cancel_function + self.time_limit = time_limit + self.stream_every = stream_every + self.connection = connection + self.event_specific_args = event_specific_args + self.component_prop_inputs = component_prop_inputs or [] + self.key = key + + self.spaces_auto_wrap() + + def spaces_auto_wrap(self): + if spaces is None: + return + if utils.get_space() is None: + return + self.fn = spaces.gradio_auto_wrap(self.fn) + + def __str__(self): + return str( + { + "fn": self.name, + "preprocess": self.preprocess, + "postprocess": self.postprocess, + } + ) + + def __repr__(self): + return str(self) + + def get_config(self): + return { + "id": self._id, + "targets": self.targets, + "inputs": [block._id for block in self.inputs], + "outputs": [block._id for block in self.outputs], + "backend_fn": self.fn is not None, + "js": self.js, + "queue": self.queue, + "api_name": self.api_name, + "api_description": self.api_description, + "scroll_to_output": self.scroll_to_output, + "show_progress": self.show_progress, + "show_progress_on": None + if self.show_progress_on is None + else [block._id for block in self.show_progress_on], + "batch": self.batch, + "max_batch_size": self.max_batch_size, + "cancels": self.cancels, + "types": { + "generator": self.types_generator, + "cancel": self.is_cancel_function, + }, + "collects_event_data": self.collects_event_data, + "trigger_after": self.trigger_after, + "trigger_only_on_success": self.trigger_only_on_success, + "trigger_only_on_failure": self.trigger_only_on_failure, + "trigger_mode": self.trigger_mode, + "api_visibility": self.api_visibility, + "rendered_in": self.rendered_in._id if self.rendered_in else None, + "render_id": self.renderable._id if self.renderable else None, + "connection": self.connection, + "time_limit": self.time_limit, + "stream_every": self.stream_every, + "event_specific_args": self.event_specific_args, + "component_prop_inputs": self.component_prop_inputs, + "js_implementation": getattr(self.fn, "__js_implementation__", None), + } diff --git a/gradio/blocks.py b/gradio/blocks.py new file mode 100644 index 0000000..786d05a --- /dev/null +++ b/gradio/blocks.py @@ -0,0 +1,3591 @@ +from __future__ import annotations + +import copy +import dataclasses +import hashlib +import inspect +import json +import os +import random +import re +import secrets +import string +import sys +import threading +import time +import warnings +import weakref +import webbrowser +from collections import defaultdict +from collections.abc import AsyncIterator, Callable, Coroutine, Sequence, Set +from pathlib import Path +from types import ModuleType, SimpleNamespace +from typing import TYPE_CHECKING, Any, Literal, Union, cast +from urllib.parse import urlparse, urlunparse + +import anyio +import fastapi +import httpx +from anyio import CapacityLimiter +from gradio_client import utils as client_utils +from gradio_client.documentation import document +from groovy import transpile + +from gradio import ( + analytics, + components, + networking, + processing_utils, + queueing, + utils, +) +from gradio.block_function import BlockFunction +from gradio.blocks_events import BLOCKS_EVENTS, BlocksEvents, BlocksMeta +from gradio.caching import TrackManualCacheUsage, used_manual_cache +from gradio.context import ( + Context, + LocalContext, + get_blocks_context, + get_render_context, + set_render_context, +) +from gradio.data_classes import ( + APIEndpointInfo, + APIInfo, + BlocksConfigDict, + DeveloperPath, + FileData, + GradioModel, + GradioRootModel, + Layout, +) +from gradio.events import ( + EventData, + EventListener, + EventListenerMethod, +) +from gradio.exceptions import ( + ChecksumMismatchError, + ComponentProcessingError, + DuplicateBlockError, + Error, + InvalidApiNameError, + InvalidComponentError, + ShareCertificateWriteError, +) +from gradio.helpers import create_tracker, skip, special_args +from gradio.i18n import I18n, I18nData +from gradio.node_server import start_node_server +from gradio.route_utils import API_PREFIX, MediaStream, slugify +from gradio.routes import INTERNAL_ROUTES, VERSION, App, Request +from gradio.state_holder import SessionState, StateHolder +from gradio.themes import ThemeClass as Theme +from gradio.tunneling import ( + BINARY_FILENAME, + BINARY_FOLDER, + BINARY_PATH, + BINARY_URL, + CURRENT_TUNNELS, +) +from gradio.utils import ( + TupleNoPrint, + check_function_inputs_match, + component_or_layout_class, + get_cancelled_fn_indices, + get_node_path, + get_package_version, + get_upload_folder, +) + +try: + import spaces # type: ignore +except Exception: + spaces = None # type: ignore[assignment] + + +if TYPE_CHECKING: # Only import for type checking (is False at runtime). + from gradio.components.base import Component + from gradio.mcp import GradioMCPServer + from gradio.renderable import Renderable + + +class Block: + def __init__( + self, + *, + 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", + visible: bool | Literal["hidden"] = True, + proxy_url: str | None = None, + ): + if getattr(self, "_is_initialized", False): + return + self._is_initialized = True + key_to_id_map = LocalContext.key_to_id_map.get(None) + if key is not None and key_to_id_map and key in key_to_id_map: + self.is_render_replacement = True + self._id = key_to_id_map[key] + else: + self.is_render_replacement = False + self._id = Context.id + Context.id += 1 + if key is not None and key_to_id_map is not None: + key_to_id_map[key] = self._id + self.visible = visible + self.elem_id = elem_id + self.elem_classes = ( + [elem_classes] if isinstance(elem_classes, str) else elem_classes + ) or [] + self.proxy_url = proxy_url + self.share_token = secrets.token_urlsafe(32) + self.parent: BlockContext | None = None + self.rendered_in: Renderable | None = None + self.page: str + self.is_rendered: bool = False + self._constructor_args: list[dict] + self.state_session_capacity = 10000 + self.temp_files: set[str] = set() + self.GRADIO_CACHE = get_upload_folder() + self.key = key + self.preserved_by_key = ( + [preserved_by_key] + if isinstance(preserved_by_key, str) + else (preserved_by_key or []) + ) + self.mcp_server_obj = None + + # Keep tracks of files that should not be deleted when the delete_cache parameter is set + # These files are the default value of the component and files that are used in examples + self.keep_in_cache = set() + self.has_launched = False + + if render: + self.render() + + def unique_key(self) -> int | None: + if self.key is None: + return None + return hash((self.rendered_in._id if self.rendered_in else None, self.key)) + + @property + def stateful(self) -> bool: + return False + + @property + def skip_api(self) -> bool: + return False + + @property + def constructor_args(self) -> dict[str, Any]: + """Get the arguments passed to the component's initializer. + + Only set classes whose metaclass is ComponentMeta + """ + # the _constructor_args list is appended based on the mro of the class + # so the first entry is for the bottom of the hierarchy + return self._constructor_args[0] if self._constructor_args else {} + + @property + def events( + self, + ) -> list[EventListener]: + return getattr(self, "EVENTS", []) + + def render(self): + """ + Adds self into appropriate BlockContext + """ + root_context = get_blocks_context() + render_context = get_render_context() + self.rendered_in = LocalContext.renderable.get(None) + if ( + root_context is not None + and self._id in root_context.blocks + and not self.is_render_replacement + ): + raise DuplicateBlockError( + f"A block with id: {self._id} has already been rendered in the current Blocks." + ) + if render_context is not None: + if root_context: + self.page = root_context.root_block.current_page + render_context.add(self) + self.parent = render_context + if root_context is not None: + root_context.blocks[self._id] = self + self.is_rendered = True + if isinstance(self, components.Component): + root_context.root_block.temp_file_sets.append(self.temp_files) + return self + + def unrender(self): + """ + Removes self from BlockContext if it has been rendered (otherwise does nothing). + Removes self from the layout and collection of blocks, but does not delete any event triggers. + """ + root_context = get_blocks_context() + if hasattr(self, "parent") and self.parent is not None: + try: + self.parent.children.remove(self) + self.parent = None + except ValueError: + pass + if root_context is not None: + try: + del root_context.blocks[self._id] + self.is_rendered = False + except KeyError: + pass + return self + + def get_block_name(self) -> str: + """ + Gets block's class name. If it is template component it gets the parent's class name. + This is used to identify the Svelte file to use in the frontend. Override this method + if a component should use a different Svelte file than the default naming convention. + """ + return ( + self.__class__.__base__.__name__.lower() # type: ignore + if hasattr(self, "is_template") + else self.__class__.__name__.lower() + ) + + def get_block_class(self) -> str: + """ + Gets block's class name. If it is template component it gets the parent's class name. + Very similar to the get_block_name method, but this method is used to reconstruct a + Gradio app that is loaded from a Space using gr.load(). This should generally + NOT be overridden. + """ + return ( + self.__class__.__base__.__name__.lower() # type: ignore + if hasattr(self, "is_template") + else self.__class__.__name__.lower() + ) + + def get_expected_parent(self) -> type[BlockContext] | None: + return None + + def breaks_grouping(self) -> bool: + """ + Whether this component breaks FormComponent grouping chains. + Components that return False will not reset the pseudo_parent + when encountered during fill_expected_parents grouping. + """ + return True + + def get_config(self, cls: type[Block] | None = None) -> dict[str, Any]: + config = {} + if cls is None: + cls = self.__class__ + signature = inspect.signature(cls.__init__) + for parameter in signature.parameters.values(): + if hasattr(self, parameter.name): + value = getattr(self, parameter.name) + if dataclasses.is_dataclass(value): + value = dataclasses.asdict(value) # type: ignore + elif isinstance(value, Block): + block_instance = value + value = block_instance.get_config() + value["id"] = block_instance._id + elif isinstance(value, (list, tuple)): + serialized_list = [] + for item in value: + if isinstance(item, Block): + item_config = item.get_config() + item_config["id"] = item._id + serialized_list.append(item_config) + else: + serialized_list.append(item) + value = serialized_list + config[parameter.name] = value + for e in self.events: + to_add = e.config_data() + if to_add: + config = {**to_add, **config} + config.pop("render", None) + config = { + **config, + "proxy_url": getattr(self, "proxy_url", None), + "name": self.get_block_class(), + } + for event_attribute in ["_selectable", "_undoable", "_retryable", "likeable"]: + if (attributable := getattr(self, event_attribute, None)) is not None: + config[event_attribute] = attributable + return config + + @classmethod + def recover_kwargs( + cls, props: dict[str, Any], additional_keys: list[str] | None = None + ): + """ + Recovers kwargs from a dict of props. + """ + additional_keys = additional_keys or [] + signature = inspect.signature(cls.__init__) + kwargs = {} + for parameter in signature.parameters.values(): + if parameter.name in props and parameter.name not in additional_keys: + kwargs[parameter.name] = props[parameter.name] + return kwargs + + async def async_move_resource_to_block_cache( + self, url_or_file_path: str | Path | None + ) -> str | None: + """Moves a file or downloads a file from a url to a block's cache directory, adds + to to the block's temp_files, and returns the path to the file in cache. This + ensures that the file is accessible to the Block and can be served to users. + + This async version of the function is used when this is being called within + a FastAPI route, as this is not blocking. + """ + if url_or_file_path is None: + return None + if isinstance(url_or_file_path, Path): + url_or_file_path = str(url_or_file_path) + + if client_utils.is_http_url_like(url_or_file_path): + temp_file_path = await processing_utils.async_ssrf_protected_download( + url_or_file_path, cache_dir=self.GRADIO_CACHE + ) + + self.temp_files.add(temp_file_path) + else: + url_or_file_path = str(utils.abspath(url_or_file_path)) + if not utils.is_in_or_equal(url_or_file_path, self.GRADIO_CACHE): + try: + temp_file_path = processing_utils.save_file_to_cache( + url_or_file_path, cache_dir=self.GRADIO_CACHE + ) + except FileNotFoundError: + # This can happen if when using gr.load() and the file is on a remote Space + # but the file is not the `value` of the component. For example, if the file + # is the `avatar_image` of the `Chatbot` component. In this case, we skip + # copying the file to the cache and just use the remote file path. + return url_or_file_path + else: + temp_file_path = url_or_file_path + self.temp_files.add(temp_file_path) + + return temp_file_path + + def move_resource_to_block_cache( + self, url_or_file_path: str | Path | None + ) -> str | None: + """Moves a file or downloads a file from a url to a block's cache directory, adds + to to the block's temp_files, and returns the path to the file in cache. This + ensures that the file is accessible to the Block and can be served to users. + + This sync version of the function is used when this is being called outside of + a FastAPI route, e.g. when examples are being cached. + """ + if url_or_file_path is None: + return None + if isinstance(url_or_file_path, Path): + url_or_file_path = str(url_or_file_path) + + if client_utils.is_http_url_like(url_or_file_path): + temp_file_path = processing_utils.save_url_to_cache( + url_or_file_path, cache_dir=self.GRADIO_CACHE + ) + + self.temp_files.add(temp_file_path) + else: + url_or_file_path = str(utils.abspath(url_or_file_path)) + if not utils.is_in_or_equal(url_or_file_path, self.GRADIO_CACHE): + try: + temp_file_path = processing_utils.save_file_to_cache( + url_or_file_path, cache_dir=self.GRADIO_CACHE + ) + except FileNotFoundError: + # This can happen if when using gr.load() and the file is on a remote Space + # but the file is not the `value` of the component. For example, if the file + # is the `avatar_image` of the `Chatbot` component. In this case, we skip + # copying the file to the cache and just use the remote file path. + return url_or_file_path + else: + temp_file_path = url_or_file_path + self.temp_files.add(temp_file_path) + + return temp_file_path + + def serve_static_file( + self, url_or_file_path: str | Path | dict | None + ) -> dict | None: + """If a file is a local file, moves it to the block's cache directory and returns + a FileData-type dictionary corresponding to the file. If the file is a URL, returns a + FileData-type dictionary corresponding to the URL. This ensures that the file is + accessible in the frontend and can be served to users. + + Examples: + >>> block.serve_static_file("https://gradio.app/logo.png") -> {"path": "https://gradio.app/logo.png", "url": "https://gradio.app/logo.png"} + >>> block.serve_static_file("logo.png") -> {"path": "logo.png", "url": "/file=logo.png"} + >>> block.serve_static_file({"path": "logo.png", "url": "/file=logo.png"}) -> {"path": "logo.png", "url": "/file=logo.png"} + """ + if url_or_file_path is None: + return None + if isinstance(url_or_file_path, dict): + return url_or_file_path + if isinstance(url_or_file_path, Path): + url_or_file_path = str(url_or_file_path) + if client_utils.is_http_url_like(url_or_file_path): + return FileData( + path=str(url_or_file_path), url=str(url_or_file_path) + ).model_dump() + else: + data = {"path": url_or_file_path, "meta": {"_type": "gradio.FileData"}} + try: + return processing_utils.move_files_to_cache(data, self) + except AttributeError: # Can be raised if this function is called before the Block is fully initialized. + return data + + @classmethod + def get_component_class_id(cls) -> str: + try: + module_path = inspect.getfile(cls) + except OSError: + module_path = cls.__module__ + module_hash = hashlib.sha256( + f"{cls.__name__}_{module_path}".encode() + ).hexdigest() + return module_hash + + @property + def component_class_id(self): + return self.get_component_class_id() + + def postprocess(self, value): + return value + + +class BlockContext(Block): + def __init__( + self, + elem_id: str | None = None, + elem_classes: list[str] | str | None = None, + visible: bool | Literal["hidden"] = True, + render: bool = True, + key: int | str | tuple[int | str, ...] | None = None, + preserved_by_key: list[str] | str | None = None, + ): + """ + Parameters: + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. + visible: If False, this will be hidden but included in the Blocks config file (its visibility can later be updated). + render: If False, this will not be included in the Blocks config file at all. + """ + self.children: list[Block] = [] + Block.__init__( + self, + elem_id=elem_id, + elem_classes=elem_classes, + visible=visible, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) + + TEMPLATE_DIR = DeveloperPath("./templates/") + FRONTEND_DIR = "../../frontend/" + + @property + def skip_api(self): + return True + + def add_child(self, child: Block): + self.children.append(child) + + def __enter__(self): + render_context = get_render_context() + self.parent = render_context + set_render_context(self) + return self + + def add(self, child: Block): + child.parent = self + self.children.append(child) + + def fill_expected_parents(self): + root_context = get_blocks_context() + children = [] + pseudo_parent = None + for child in self.children: + expected_parent = child.get_expected_parent() + if not expected_parent or isinstance(self, expected_parent): + if child.breaks_grouping(): + pseudo_parent = None + children.append(child) + else: + if pseudo_parent is not None and isinstance( + pseudo_parent, expected_parent + ): + pseudo_parent.add_child(child) + else: + key = None + if child.key is not None: + if isinstance(child.key, tuple): + key = child.key + ("_parent",) + else: + key = (child.key, "_parent") + pseudo_parent = expected_parent(render=False, key=key) + pseudo_parent.parent = self + children.append(pseudo_parent) + pseudo_parent.add_child(child) + pseudo_parent.page = child.page + if root_context: + root_context.blocks[pseudo_parent._id] = pseudo_parent + child.parent = pseudo_parent + self.children = children + + def __exit__(self, exc_type: type[BaseException] | None = None, *args): + set_render_context(self.parent) + if exc_type is not None: + return + if getattr(self, "allow_expected_parents", True): + self.fill_expected_parents() + + +def postprocess_update_dict( + block: Component | BlockContext, update_dict: dict, postprocess: bool = True +): + """ + Converts a dictionary of updates into a format that can be sent to the frontend to update the component. + E.g. {"value": "2", "visible": True, "invalid_arg": "hello"} + Into -> {"__type__": "update", "value": 2.0, "visible": True} + Parameters: + block: The Block that is being updated with this update dictionary. + update_dict: The original update dictionary + postprocess: Whether to postprocess the "value" key of the update dictionary. + """ + from gradio.components import HTML + + value = update_dict.pop("value", components._Keywords.NO_VALUE) + + props = None + if isinstance(block, HTML): + sig = inspect.signature(HTML.__init__) + constructor_params = set(sig.parameters.keys()) + props = {k: v for k, v in update_dict.items() if k not in constructor_params} + update_dict = {k: getattr(block, k) for k in update_dict if hasattr(block, k)} + if props: + update_dict["props"] = props + + if value is not components._Keywords.NO_VALUE: + if postprocess: + update_dict["value"] = block.postprocess(value) + if isinstance(update_dict["value"], (GradioModel, GradioRootModel)): + update_dict["value"] = update_dict["value"].model_dump() + else: + update_dict["value"] = value + update_dict["__type__"] = "update" + return update_dict + + +def convert_component_dict_to_list( + outputs_ids: list[int], predictions: dict +) -> list | dict: + """ + Converts a dictionary of component updates into a list of updates in the order of + the outputs_ids and including every output component. Leaves other types of dictionaries unchanged. + E.g. {"textbox": "hello", "number": {"__type__": "generic_update", "value": "2"}} + Into -> ["hello", {"__type__": "generic_update"}, {"__type__": "generic_update", "value": "2"}] + """ + keys_are_blocks = [isinstance(key, Block) for key in predictions] + if all(keys_are_blocks): + reordered_predictions = [skip() for _ in outputs_ids] + for component, value in predictions.items(): + if component._id not in outputs_ids: + raise ValueError( + f"Returned component {component} not specified as output of function." + ) + output_index = outputs_ids.index(component._id) + reordered_predictions[output_index] = value + predictions = utils.resolve_singleton(reordered_predictions) + elif any(keys_are_blocks): + raise ValueError( + "Returned dictionary included some keys as Components. Either all keys must be Components to assign Component values, or return a List of values to assign output values in order." + ) + return predictions + + +def _find_free_port(host: str, start: int, try_count: int = 100) -> int: + """Find an available port by scanning from *start*.""" + import socket + + for port in range(start, start + try_count): + try: + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host if host != "0.0.0.0" else "127.0.0.1", port)) + s.close() + return port + except OSError: + continue + raise OSError(f"Cannot find empty port in range: {start}-{start + try_count - 1}.") + + +class BlocksConfig: + def __init__(self, root_block: Blocks): + self._id: int = 0 + self.root_block = root_block + self.blocks: dict[int, Component | Block] = {} + self.fns: dict[int, BlockFunction] = {} + self.fn_id: int = 0 + + def set_event_trigger( + self, + targets: Sequence[EventListenerMethod], + fn: Callable | None, + inputs: ( + Component + | BlockContext + | Sequence[Component | BlockContext] + | Set[Component | BlockContext] + | None + ), + outputs: ( + Component + | BlockContext + | Sequence[Component | BlockContext] + | Set[Component | BlockContext] + | None + ), + preprocess: bool = True, + postprocess: bool = True, + scroll_to_output: bool = False, + show_progress: Literal["full", "minimal", "hidden"] = "full", + show_progress_on: Component | Sequence[Component] | None = None, + api_name: str | None = None, + api_description: str | None | Literal[False] = None, + js: str | Literal[True] | None = None, + no_target: bool = False, + queue: bool = True, + batch: bool = False, + max_batch_size: int = 4, + cancels: list[int] | None = None, + collects_event_data: bool | None = None, + trigger_after: int | None = None, + trigger_only_on_success: bool = False, + trigger_only_on_failure: bool = False, + trigger_mode: Literal["once", "multiple", "always_last"] | None = "once", + concurrency_limit: int | None | Literal["default"] = "default", + concurrency_id: str | None = None, + api_visibility: Literal["public", "private", "undocumented"] = "public", + renderable: Renderable | None = None, + is_cancel_function: bool = False, + connection: Literal["stream", "sse"] = "sse", + time_limit: float | None = None, + stream_every: float = 0.5, + event_specific_args: list[str] | None = None, + js_implementation: str | None = None, + key: str | int | tuple[int | str, ...] | None = None, + validator: Callable | None = None, + component_prop_inputs: list[int] | None = None, + ) -> tuple[BlockFunction, int]: + """ + Adds an event to the component's dependencies. + Parameters: + targets: a list of EventListenerMethod objects that define the event trigger + fn: the function to run when the event is triggered + inputs: the list of input components whose values will be passed to the function + outputs: the list of output components whose values will be updated by the function + preprocess: whether to run the preprocess methods of the input components before running the function + postprocess: whether to run the postprocess methods of the output components after running the function + scroll_to_output: whether to scroll to output of dependency on trigger + show_progress: how to show the progress animation while event is running: "full" shows a spinner which covers the output component area as well as a runtime display in the upper right corner, "minimal" only shows the runtime display, "hidden" shows no progress animation at all + show_progress_on: Component or list of components to show the progress animation on. If None, will show the progress animation on all of the output components. + api_name: defines how the endpoint appears in the API docs. Can be a string or None. If set to a string, the endpoint will be exposed in the API docs with the given name. If None (default), the name of the function will be used as the API endpoint. + api_description: Description of the API endpoint. Can be a string, None, or False. If set to a string, the endpoint will be exposed in the API docs with the given description. If None, the function's docstring will be used as the API endpoint description. If False, then no description will be displayed in the API docs. + js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values that will be passed as inputs to the Python function (`fn`) + no_target: if True, sets "targets" to [], used for the Blocks.load() event and .then() events + queue: If True, will place the request on the queue, if the queue has been enabled. If False, will not put this event on the queue, even if the queue has been enabled. If None, will use the queue setting of the gradio app. + batch: whether this function takes in a batch of inputs + max_batch_size: the maximum batch size to send to the function + cancels: a list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method. + collects_event_data: whether to collect event data for this event + trigger_after: if set, this event will be triggered after 'trigger_after' function index + trigger_only_on_success: if True, this event will only be triggered if the previous event was successful (only applies if `trigger_after` is set) + trigger_only_on_failure: if True, this event will only be triggered if the previous event failed i.e. raised an exception (only applies if `trigger_after` is set) + trigger_mode: If "once" (default for all events except `.change()`) would not allow any submissions while an event is pending. If set to "multiple", unlimited submissions are allowed while pending, and "always_last" (default for `.change()` and `.key_up()` events) would allow a second submission after the pending event is complete. + concurrency_limit: If set, this is the maximum number of this event that can be running simultaneously. Can be set to None to mean no concurrency_limit (any number of this event can be running simultaneously). Set to "default" to use the default concurrency limit (defined by the `default_concurrency_limit` parameter in `queue()`, which itself is 1 by default). + concurrency_id: If set, this is the id of the concurrency group. Events with the same concurrency_id will be limited by the lowest set concurrency_limit. + api_visibility: controls the visibility and accessibility of this endpoint. Can be "public" (shown in API docs and callable by clients), "private" (hidden from API docs and not callable by the Gradio client libraries), or "undocumented" (hidden from API docs but callable by clients and via gr.load). If fn is None, api_visibility will automatically be set to "private". + is_cancel_function: whether this event cancels another running event. + connection: The connection format, either "sse" or "stream". + time_limit: The time limit for the function to run. Parameter only used for the `.stream()` event. + stream_every: The latency (in seconds) at which stream chunks are sent to the backend. Defaults to 0.5 seconds. Parameter only used for the `.stream()` event. + validator: a function that takes in the inputs and can optionally return a gr.validate() object for each input. + Returns: dependency information, dependency index + """ + # Support for singular parameter + _targets = [ + ( + target.block._id if not no_target and target.block else None, + target.event_name, + ) + for target in targets + ] + if isinstance(inputs, Set): + inputs_as_dict = True + inputs = sorted(inputs, key=lambda x: x._id) + else: + inputs_as_dict = False + if inputs is None: + inputs = [] + elif not isinstance(inputs, Sequence): + inputs = [inputs] + + if isinstance(outputs, Set): + outputs = sorted(outputs, key=lambda x: x._id) + elif outputs is None: + outputs = [] + elif not isinstance(outputs, Sequence): + outputs = [outputs] + if show_progress_on and not isinstance(show_progress_on, Sequence): + show_progress_on = [show_progress_on] + + if fn is not None and not cancels: + check_function_inputs_match(fn, inputs, inputs_as_dict) + + if _targets and trigger_mode is None: + if _targets[0][1] in ["change", "key_up"]: + trigger_mode = "always_last" + elif _targets[0][1] in ["stream"]: + trigger_mode = "multiple" + if trigger_mode is None: + trigger_mode = "once" + elif trigger_mode not in ["once", "multiple", "always_last"]: + raise ValueError( + f"Invalid value for parameter `trigger_mode`: {trigger_mode}. Please choose from: {['once', 'multiple', 'always_last']}" + ) + + fn_to_analyze = renderable.fn if renderable else fn + _, progress_index, event_data_index, component_prop_indices = ( + special_args(fn_to_analyze) if fn_to_analyze else (None, None, None, []) + ) + if component_prop_inputs is None: + component_prop_inputs = component_prop_indices or [] + + # If api_name is None or empty string, use the function name + if api_name is None or isinstance(api_name, str) and api_name.strip() == "": + if fn is not None: + if not hasattr(fn, "__name__"): + if hasattr(fn, "__class__") and hasattr(fn.__class__, "__name__"): + name = fn.__class__.__name__ + else: + name = "unnamed" + else: + name = fn.__name__ + api_name = "".join( + [ + s + for s in str(name) + if s not in set(string.punctuation) - {"-", "_"} + ] + ) + elif js is not None: + api_name = "js_fn" + api_visibility = "private" + else: + api_name = "unnamed" + api_visibility = "private" + elif api_name is False: + api_name = "false" + api_visibility = "private" + + api_name = utils.append_unique_suffix( + api_name, + [fn.api_name for fn in self.fns.values() if isinstance(fn.api_name, str)], + ) + + # The `api_visibility` parameter is "private" if: (1) the user explicitly sets it (2) fn is None (there's no backend function) + # or (3) the function is a js function + + if collects_event_data is None: + collects_event_data = event_data_index is not None + + rendered_in = LocalContext.renderable.get(None) + + if js is True and inputs: + raise ValueError( + "Cannot create event: events with js=True cannot have inputs." + ) + + reuse_id = False + fn_id = self.fn_id + render_iteration = rendered_in.render_iteration if rendered_in else None + + if rendered_in and key is not None: + for existing_fn in self.fns.values(): + if existing_fn.key == key: + reuse_id = True + fn_id = existing_fn._id + break + + block_fn = BlockFunction( + fn, + inputs, # type: ignore + outputs, # type: ignore + preprocess, + postprocess, + _id=fn_id, + inputs_as_dict=inputs_as_dict, + targets=_targets, + batch=batch, + max_batch_size=max_batch_size, + concurrency_limit=concurrency_limit, + concurrency_id=concurrency_id, + tracks_progress=progress_index is not None, + api_name=api_name, + api_description=api_description, + js=js, + show_progress=show_progress, + show_progress_on=show_progress_on # type: ignore + if isinstance(show_progress_on, (list, tuple)) or show_progress_on is None + else [show_progress_on], + cancels=cancels, + collects_event_data=collects_event_data, + trigger_after=trigger_after, + trigger_only_on_success=trigger_only_on_success, + trigger_only_on_failure=trigger_only_on_failure, + trigger_mode=trigger_mode, + queue=queue, + scroll_to_output=scroll_to_output, + api_visibility=api_visibility, + renderable=renderable, + rendered_in=rendered_in, + render_iteration=render_iteration, + is_cancel_function=is_cancel_function, + connection=connection, + time_limit=time_limit, + stream_every=stream_every, + event_specific_args=event_specific_args, + component_prop_inputs=component_prop_inputs, + page=self.root_block.current_page, + js_implementation=js_implementation, + key=key, + validator=validator, + ) + + self.fns[fn_id] = block_fn + if not reuse_id: + self.fn_id += 1 + return block_fn, block_fn._id + + @staticmethod + def config_for_block( + _id: int, + rendered_ids: list[int], + block: Block | Component, + renderable: Renderable | None = None, + ) -> dict: + if renderable and _id not in rendered_ids: + return {} + props = block.get_config() if hasattr(block, "get_config") else {} + + skip_none_deletion = [] + if ( + renderable and block.key + ): # Nones are important for replacing a value in a keyed component + skip_none_deletion = [ + prop for prop, val in block.constructor_args.items() if val is None + ] + utils.delete_none(props, skip_props=skip_none_deletion) + + block_config = { + "id": _id, + "type": block.get_block_name(), + "props": props, + "skip_api": block.skip_api, + "component_class_id": getattr(block, "component_class_id", None), + "key": block.unique_key(), + } + if renderable: + block_config["renderable"] = renderable._id + if block.rendered_in is not None: + block_config["rendered_in"] = block.rendered_in._id + if not block.skip_api: + block_config["api_info"] = block.api_info() # type: ignore + if hasattr(block, "api_info_as_input"): + block_config["api_info_as_input"] = block.api_info_as_input() # type: ignore + else: + block_config["api_info_as_input"] = block.api_info() # type: ignore + if hasattr(block, "api_info_as_output"): + block_config["api_info_as_output"] = block.api_info_as_output() # type: ignore + else: + block_config["api_info_as_output"] = block.api_info() # type: ignore + block_config["example_inputs"] = block.example_inputs() # type: ignore + + return block_config + + def get_config(self, renderable: Renderable | None = None): + config = { + "page": {}, + "components": [], + "dependencies": [], + } + + for page_tuple in self.root_block.pages: + page = page_tuple[0] + if page not in config["page"]: + config["page"][page] = { # type: ignore + "layout": {"id": self.root_block._id, "children": []}, + "components": [], + "dependencies": [], + } + + rendered_ids = [] + + def get_layout(block: Block) -> Layout: + rendered_ids.append(block._id) + if not isinstance(block, BlockContext): + return {"id": block._id} + children_layout = [] + for child in block.children: + layout = get_layout(child) + children_layout.append(layout) + return {"id": block._id, "children": children_layout} + + if renderable: + root_block = self.blocks[renderable.container_id] + else: + root_block = self.root_block + layout = get_layout(root_block) + config["layout"] = layout + + for root_child in layout.get("children", []): + if isinstance(root_child, dict) and root_child["id"] in self.blocks: + block = self.blocks[root_child["id"]] + config["page"][block.page]["layout"]["children"].append(root_child) # type: ignore + + blocks_items = list( + self.blocks.items() + ) # freeze as list to prevent concurrent re-renders from changing the dict during loop, see https://github.com/gradio-app/gradio/issues/9991 + for _id, block in blocks_items: + block_config = self.config_for_block(_id, rendered_ids, block, renderable) + if not block_config: + continue + config["components"].append(block_config) # type: ignore + config["page"][block.page]["components"].append(block._id) # type: ignore + + dependencies = [] + for fn in self.fns.values(): + if renderable is None or fn.rendered_in == renderable: + dependency_config = fn.get_config() + dependencies.append(dependency_config) + config["page"][fn.page]["dependencies"].append(dependency_config["id"]) # type: ignore + + config["dependencies"] = dependencies + return config + + def __copy__(self): + new = BlocksConfig(self.root_block) + new.blocks = copy.copy(self.blocks) + new.fns = copy.copy(self.fns) + new.fn_id = self.fn_id + return new + + def attach_load_events(self, rendered_in: Renderable | None = None): + """Add a load event for every component whose initial value requires a function call to set.""" + for component in self.blocks.values(): + if rendered_in is not None and component.rendered_in != rendered_in: + continue + if ( + isinstance(component, components.Component) + and component.load_event_to_attach + ): + load_fn, triggers, inputs = component.load_event_to_attach + has_target = len(triggers) > 0 + triggers += [(self.root_block, "load")] + # Use set_event_trigger to avoid ambiguity between load class/instance method + + dep = self.set_event_trigger( + [EventListenerMethod(*trigger) for trigger in triggers], + load_fn, + inputs, + component, + no_target=False, + show_progress="hidden" if has_target else "full", + )[0] + component.load_event = dep.get_config() + + +@document("launch", "queue", "integrate", "load", "unload") +class Blocks(BlockContext, BlocksEvents, metaclass=BlocksMeta): + """ + Blocks is Gradio's low-level API that allows you to create more custom web + applications and demos than Interfaces (yet still entirely in Python). + + + Compared to the Interface class, Blocks offers more flexibility and control over: + (1) the layout of components (2) the events that + trigger the execution of functions (3) data flows (e.g. inputs can trigger outputs, + which can trigger the next level of outputs). Blocks also offers ways to group + together related demos such as with tabs. + + + The basic usage of Blocks is as follows: create a Blocks object, then use it as a + context (with the "with" statement), and then define layouts, components, or events + within the Blocks context. Finally, call the launch() method to launch the demo. + + Example: + import gradio as gr + def update(name): + return f"Welcome to Gradio, {name}!" + + with gr.Blocks() as demo: + gr.Markdown("Start typing below and then click **Run** to see the output.") + with gr.Row(): + inp = gr.Textbox(placeholder="What is your name?") + out = gr.Textbox() + btn = gr.Button("Run") + btn.click(fn=update, inputs=inp, outputs=out) + + demo.launch() + Demos: blocks_hello, blocks_flipper, blocks_kinematics + Guides: blocks-and-event-listeners, controlling-layout, state-in-blocks, more-blocks-features, using-blocks-like-functions + """ + + # stores references to all currently existing Blocks instances + instances: weakref.WeakSet = weakref.WeakSet() + + @classmethod + def get_instances(cls) -> list[Blocks]: + """ + :return: list of all current instances. + """ + return list(Blocks.instances) + + def __init__( + self, + analytics_enabled: bool | None = None, + mode: str = "blocks", + title: str | I18nData = "Gradio", + fill_height: bool = False, + fill_width: bool = False, + delete_cache: tuple[int, int] | None = None, + **kwargs, + ): + """ + Parameters: + analytics_enabled: Whether to allow basic telemetry. If None, will use GRADIO_ANALYTICS_ENABLED environment variable or default to True. + mode: A human-friendly name for the kind of Blocks or Interface being created. Used internally for analytics. + title: The tab title to display when this is opened in a browser window. + fill_height: Whether to vertically expand top-level child components to the height of the window. If True, expansion occurs when the scale value of the child components >= 1. + fill_width: Whether to horizontally expand to fill container fully. If False, centers and constrains app to a maximum width. Only applies if this is the outermost `Blocks` in your Gradio app. + delete_cache: A tuple corresponding [frequency, age] both expressed in number of seconds. Every `frequency` seconds, the temporary files created by this Blocks instance will be deleted if more than `age` seconds have passed since the file was created. For example, setting this to (86400, 86400) will delete temporary files every day. The cache will be deleted entirely when the server restarts. If None, no cache deletion will occur. + """ + + self.limiter = None + self.encrypt = False + self.mcp_server_obj: None | GradioMCPServer = None + self.mcp_error: None | str = None + self.share = False + self.enable_queue = True + self.max_threads = 40 + self.pending_streams = defaultdict(dict) + self.pending_diff_streams = defaultdict(dict) + self.show_error = True + self.fill_height = fill_height + self.fill_width = fill_width + self.delete_cache = delete_cache + self.extra_startup_events: list[Callable[..., Coroutine[Any, Any, Any]]] = [] + self.renderables: list[Renderable] = [] + self.state_holder: StateHolder + self.custom_mount_path: str | None = None + self.pwa = False + self.mcp_server = False + + # For analytics_enabled and allow_flagging: (1) first check for + # parameter, (2) check for env variable, (3) default to True/"manual" + self.analytics_enabled = ( + analytics_enabled + if analytics_enabled is not None + else analytics.analytics_enabled() + ) + if self.analytics_enabled: + t = threading.Thread(target=analytics.version_check) + t.start() + else: + os.environ["HF_HUB_DISABLE_TELEMETRY"] = "True" + self.enable_monitoring: bool | None = None + + deprecated_params = ["theme", "css", "css_paths", "js", "head", "head_paths"] + deprecated_kwargs = {k: kwargs.pop(k) for k in deprecated_params if k in kwargs} + if deprecated_kwargs: + param_list = ", ".join(deprecated_kwargs.keys()) + warnings.warn( + f"The parameters have been moved from the Blocks constructor to the launch() method in Gradio 6.0: {param_list}. " + f"Please pass these parameters to launch() instead.", + UserWarning, + stacklevel=2, + ) + self._deprecated_theme = deprecated_kwargs.get("theme") + self._deprecated_css = deprecated_kwargs.get("css") + self._deprecated_css_paths = deprecated_kwargs.get("css_paths") + self._deprecated_js = deprecated_kwargs.get("js") + self._deprecated_head = deprecated_kwargs.get("head") + self._deprecated_head_paths = deprecated_kwargs.get("head_paths") + + self.default_config = BlocksConfig(self) + super().__init__(render=False, **kwargs) + + self.mode = mode + self.is_running = False + self.local_url = None + self.share_url = None + self.width = None + self.height = None + self.api_open = utils.get_space() is None + + self.space_id = utils.get_space() + self.favicon_path = None + self.auth = None + self.dev_mode = bool(os.getenv("GRADIO_WATCH_DIRS", "")) + self.vibe_mode = bool(os.getenv("GRADIO_VIBE_MODE", "")) + self.app_id = random.getrandbits(64) + self.upload_file_set = set() + self.temp_file_sets = [self.upload_file_set] + self.title = title + + # Only used when an Interface is loaded from a config + self.predict = None + self.input_components = None + self.output_components = None + self.__name__ = None # type: ignore + self.api_mode = None + + self.progress_tracking = None + self.ssl_verify = True + self.allowed_paths = [] + self.blocked_paths = [] + self.root_path = os.environ.get("GRADIO_ROOT_PATH", "") + self.proxy_urls = set() + + self.pages: list[tuple[str, str, bool]] = [("", "Home", True)] + self.current_page = "" + + self.css = None + self.js = None + self.head = None + self.theme = None # type: ignore + self.head_paths = None + + if self.analytics_enabled: + data = { + "mode": self.mode, + "version": get_package_version(), + } + analytics.initiated_analytics(data) + + Blocks.instances.add(self) + + self.queue() + + @property + def blocks(self) -> dict[int, Component | Block]: + return self.default_config.blocks + + @blocks.setter + def blocks(self, value: dict[int, Component | Block]): + self.default_config.blocks = value + + @property + def fns(self) -> dict[int, BlockFunction]: + return self.default_config.fns + + def get_component(self, id: int) -> Component | BlockContext: + comp = self.blocks[id] + if not isinstance(comp, (components.Component, BlockContext)): + raise TypeError(f"Block with id {id} is not a Component or BlockContext") + return comp + + @property + def _is_running_in_reload_thread(self): + from gradio.cli.commands.reload import reload_thread + + return getattr(reload_thread, "running_reload", False) + + @classmethod + def from_config( + cls, + config: BlocksConfigDict, + fns: list[Callable], + proxy_url: str, + ) -> Blocks: + """ + Factory method that creates a Blocks from a config and list of functions. Used + internally by the gradio.external.load() method. + + Parameters: + config: a dictionary containing the configuration of the Blocks. + fns: a list of functions that are used in the Blocks. Must be in the same order as the dependencies in the config. + proxy_url: an external url to use as a root URL when serving files for components in the Blocks. + """ + config = copy.deepcopy(config) + components_config = config["components"] + original_mapping: dict[int, Block] = {} + proxy_urls: set[str] = set() + if httpx.URL(proxy_url).host.endswith(".hf.space"): + proxy_urls.add(proxy_url) + + def get_block_instance(id: int) -> Block: + for block_config in components_config: + if block_config["id"] == id: + break + else: + raise ValueError(f"Cannot find block with id {id}") + cls = component_or_layout_class(block_config["props"]["name"]) + + # If a Gradio app B is loaded into a Gradio app A, and B itself loads a + # Gradio app C, then the proxy_urls of the components in A need to be the + # URL of C, not B. The else clause below handles this case. + if block_config["props"].get("proxy_url") is None: + block_config["props"]["proxy_url"] = f"{proxy_url}/" + postprocessed_value = block_config["props"].pop("value", None) + + constructor_args = cls.recover_kwargs(block_config["props"]) + block = cls(**constructor_args) + if postprocessed_value is not None: + block.value = postprocessed_value # type: ignore + + block_proxy_url = block_config["props"]["proxy_url"] + block.proxy_url = block_proxy_url + # Only add proxy URLs that point to known Hugging Face Space + # hosts to prevent SSRF via malicious configs. + if httpx.URL(block_proxy_url).host.endswith(".hf.space"): + proxy_urls.add(block_proxy_url) + if ( + _selectable := block_config["props"].pop("_selectable", None) + ) is not None: + block._selectable = _selectable # type: ignore + + return block + + def iterate_over_children(children_list): + for child_config in children_list: + id = child_config["id"] + block = get_block_instance(id) + + original_mapping[id] = block + + children = child_config.get("children") + if children is not None: + if not isinstance(block, BlockContext): + raise ValueError( + f"Invalid config, Block with id {id} has children but is not a BlockContext." + ) + with block: + iterate_over_children(children) + + derived_fields = ["types"] + + with Blocks(theme=config.get("theme", None)) as blocks: + # ID 0 should be the root Blocks component + original_mapping[0] = root_block = Context.root_block or blocks + + if "layout" in config: + iterate_over_children(config["layout"].get("children", [])) + + first_dependency = None + + # add the event triggers + if "dependencies" not in config: + raise ValueError( + "This config is missing the 'dependencies' field and cannot be loaded." + ) + for dependency, fn in zip(config["dependencies"], fns, strict=False): + # We used to add a "fake_event" to the config to cache examples + # without removing it. This was causing bugs in calling gr.load + # We fixed the issue by removing "fake_event" from the config in examples.py + # but we still need to skip these events when loading the config to support + # older demos + if "trigger" in dependency and dependency["trigger"] == "fake_event": + continue + for field in derived_fields: + dependency.pop(field, None) + + # older versions had a separate trigger field, but now it is part of the + # targets field + _targets = dependency.pop("targets") + trigger = dependency.pop("trigger", None) + is_then_event = False + + # This assumes that you cannot combine multiple .then() events in a single + # gr.on() event, which is true for now. If this changes, we will need to + # update this code. + if not isinstance(_targets[0], int) and _targets[0][1] in [ + "then", + "success", + ]: + if len(_targets) != 1: + raise ValueError( + "This logic assumes that .then() events are not combined with other events in a single gr.on() event" + ) + is_then_event = True + + dependency.pop("backend_fn") + dependency.pop("documentation", None) + dependency["inputs"] = [ + original_mapping[i] for i in dependency["inputs"] + ] + dependency["outputs"] = [ + original_mapping[o] for o in dependency["outputs"] + ] + dependency.pop("status_tracker", None) + dependency.pop("zerogpu", None) + dependency.pop("id", None) + dependency.pop("rendered_in", None) + dependency.pop("render_id", None) + dependency.pop("every", None) + + # Older versions of Gradio had a show_api field, which has been replaced + # by api_visibility. + api_visibility = dependency.pop("api_visibility", None) + if api_visibility is None: + show_api = dependency.pop("show_api", None) + if dependency["api_name"] is False: + api_visibility = "private" + elif show_api is True: + api_visibility = "public" + else: + api_visibility = "undocumented" + dependency["api_visibility"] = api_visibility + + dependency["preprocess"] = False + dependency["postprocess"] = False + if is_then_event: + targets = [EventListenerMethod(None, "then")] + dependency["trigger_after"] = dependency.pop("trigger_after") + dependency["trigger_only_on_success"] = dependency.pop( + "trigger_only_on_success" + ) + dependency["trigger_only_on_failure"] = dependency.pop( + "trigger_only_on_failure", False + ) + dependency["no_target"] = True + else: + targets = [ + EventListenerMethod( + t.__self__ if t.has_trigger else None, + t.event_name, # type: ignore + ) + for t in Blocks.get_event_targets( + original_mapping, _targets, trigger + ) + ] + dependency = root_block.default_config.set_event_trigger( # type: ignore + targets=targets, fn=fn, **dependency + )[0] + if first_dependency is None: + first_dependency = dependency + + # Allows some use of Interface-specific methods with loaded Spaces + if first_dependency and get_blocks_context(): + blocks.predict = [fns[0]] + blocks.input_components = first_dependency.inputs + blocks.output_components = first_dependency.outputs + blocks.__name__ = "Interface" + blocks.api_mode = True + blocks.proxy_urls = proxy_urls + return blocks + + def __str__(self): + return self.__repr__() + + def __repr__(self): + num_backend_fns = len([d for d in self.fns.values() if d.fn]) + repr = f"Gradio Blocks instance: {num_backend_fns} backend functions" + repr += f"\n{'-' * len(repr)}" + for d, dependency in self.fns.items(): + if dependency.fn: + repr += f"\nfn_index={d}" + repr += "\n inputs:" + for block in dependency.inputs: + block = self.blocks[block._id] + repr += f"\n |-{block}" + repr += "\n outputs:" + for block in dependency.outputs: + block = self.blocks[block._id] + repr += f"\n |-{block}" + return repr + + @property + def expects_oauth(self): + """Return whether the app expects user to authenticate via OAuth.""" + return any( + isinstance(block, components.LoginButton) for block in self.blocks.values() + ) + + def unload(self, fn: Callable[..., Any]) -> None: + """This listener is triggered when the user closes or refreshes the tab, ending the user session. + It is useful for cleaning up resources when the app is closed. + Parameters: + fn: Callable function to run to clear resources. The function should not take any arguments and the output is not used. + Example: + import gradio as gr + with gr.Blocks() as demo: + gr.Markdown("# When you close the tab, hello will be printed to the console") + demo.unload(lambda: print("hello")) + demo.launch() + """ + self.default_config.set_event_trigger( + targets=[EventListenerMethod(None, "unload")], + fn=fn, + inputs=None, + outputs=None, + preprocess=False, + postprocess=False, + show_progress="hidden", + api_name=None, + js=None, + no_target=True, + batch=False, + max_batch_size=4, + cancels=None, + collects_event_data=None, + trigger_after=None, + trigger_only_on_success=False, + trigger_only_on_failure=False, + trigger_mode="once", + concurrency_limit="default", + concurrency_id=None, + api_visibility="private", + ) + + def render(self): + root_context = get_blocks_context() + if root_context is not None and Context.root_block is not None: + if self._id in root_context.blocks: + raise DuplicateBlockError( + f"A block with id: {self._id} has already been rendered in the current Blocks." + ) + overlapping_ids = set(root_context.blocks).intersection(self.blocks) + for id in overlapping_ids: + # State components are allowed to be reused between Blocks + if not isinstance(self.blocks[id], components.State): + raise DuplicateBlockError( + "At least one block in this Blocks has already been rendered." + ) + + for block in self.blocks.values(): + block.page = Context.root_block.current_page + root_context.blocks.update(self.blocks) + dependency_offset = max(root_context.fns.keys(), default=-1) + 1 + existing_api_names = [ + dep.api_name + for dep in root_context.fns.values() + if isinstance(dep.api_name, str) + ] + for dependency in self.fns.values(): + dependency.page = Context.root_block.current_page + dependency._id += dependency_offset + # Any event -- e.g. Blocks.load() -- that is triggered by this Blocks + # should now be triggered by the root Blocks instead. + for target in dependency.targets: + if target[0] == self._id: + target = (Context.root_block._id, target[1]) + api_name = dependency.api_name + if isinstance(api_name, str): + api_name_ = utils.append_unique_suffix( + api_name, + existing_api_names, + ) + if api_name != api_name_: + dependency.api_name = api_name_ + dependency.cancels = [c + dependency_offset for c in dependency.cancels] + if dependency.trigger_after is not None: + dependency.trigger_after += dependency_offset + # Recreate the cancel function so that it has the latest + # dependency fn indices. This is necessary to properly cancel + # events in the backend + if dependency.cancels: + updated_cancels = [ + root_context.fns[i].get_config() for i in dependency.cancels + ] + dependency.cancels = get_cancelled_fn_indices(updated_cancels) + root_context.fns[dependency._id] = dependency + root_context.fn_id = max(root_context.fns.keys(), default=-1) + 1 + Context.root_block.temp_file_sets.extend(self.temp_file_sets) + Context.root_block.proxy_urls.update(self.proxy_urls) + Context.root_block.extra_startup_events.extend(self.extra_startup_events) + + render_context = get_render_context() + if render_context is not None: + render_context.children.extend(self.children) + return self + + def is_callable(self, fn_index: int = 0) -> bool: + """Checks if a particular Blocks function is callable (i.e. not stateful or a generator).""" + block_fn = self.fns[fn_index] + dependency = self.fns[fn_index] + + if inspect.isasyncgenfunction(block_fn.fn): + return False + if inspect.isgeneratorfunction(block_fn.fn): + return False + if any(block.stateful for block in dependency.inputs): + return False + return not any(block.stateful for block in dependency.outputs) + + def __call__(self, *inputs, fn_index: int = 0, api_name: str | None = None): + """ + Allows Blocks objects to be called as functions. Supply the parameters to the + function as positional arguments. To choose which function to call, use the + fn_index parameter, which must be a keyword argument. + + Parameters: + *inputs: the parameters to pass to the function + fn_index: the index of the function to call (defaults to 0, which for Interfaces, is the default prediction function) + api_name: The api_name of the dependency to call. Will take precedence over fn_index. + """ + if api_name is not None: + inferred_fn_index = next( + (i for i, d in self.fns.items() if d.api_name == api_name), + None, + ) + if inferred_fn_index is None: + raise InvalidApiNameError( + f"Cannot find a function with api_name {api_name}" + ) + fn_index = inferred_fn_index + if not (self.is_callable(fn_index)): + raise ValueError( + "This function is not callable because it is either stateful or is a generator. Please use the .launch() method instead to create an interactive user interface." + ) + + inputs = list(inputs) + processed_inputs = self.serialize_data(fn_index, inputs) + fn = self.fns[fn_index] + if fn.batch: + processed_inputs = [[inp] for inp in processed_inputs] + + outputs = client_utils.synchronize_async( + self.process_api, + block_fn=fn, + inputs=processed_inputs, + request=None, + state={}, + explicit_call=True, + ) + outputs = outputs["data"] + + if fn.batch: + outputs = [out[0] for out in outputs] + + outputs = self.deserialize_data(fn_index, outputs) + processed_outputs = utils.resolve_singleton(outputs) + + return processed_outputs + + async def call_function( + self, + block_fn: BlockFunction | int, + processed_input: list[Any], + iterator: AsyncIterator[Any] | None = None, + requests: Request | list[Request] | None = None, + event_id: str | None = None, + event_data: EventData | None = None, + in_event_listener: bool = False, + state: SessionState | None = None, + ): + """ + Calls function with given index and preprocessed input, and measures process time. + Parameters: + fn_index: index of function to call + processed_input: preprocessed input to pass to function + iterator: iterator to use if function is a generator + requests: requests to pass to function + event_id: id of event in queue + event_data: data associated with event trigger + """ + if isinstance(block_fn, int): + block_fn = self.fns[block_fn] + if not block_fn.fn: + raise IndexError("function has no backend method.") + is_generating = False + request = requests[0] if isinstance(requests, list) else requests + start = time.time() + + fn = utils.get_function_with_locals( + fn=block_fn.fn, + blocks=self, + event_id=event_id, + in_event_listener=in_event_listener, + request=request, # type: ignore + state=state, + ) + + if iterator is None: # If not a generator function that has already run + if block_fn.inputs_as_dict: + processed_input = [ + dict(zip(block_fn.inputs, processed_input, strict=False)) + ] + + fn_to_analyze = ( + block_fn.renderable.fn if block_fn.renderable else block_fn.fn + ) + component_props = {} + for idx in block_fn.component_prop_inputs: + if idx < len(processed_input) and isinstance( + processed_input[idx], dict + ): + component_props[idx] = processed_input[idx] + + processed_input, progress_index, _, _ = special_args( + fn_to_analyze, + processed_input, + request, # type: ignore + event_data, # type: ignore + component_props=component_props, + ) + progress_tracker = ( + processed_input[progress_index] if progress_index is not None else None + ) + + if progress_tracker is not None and progress_index is not None: + progress_tracker, fn = create_tracker(fn, progress_tracker.track_tqdm) + processed_input[progress_index] = progress_tracker + + if inspect.iscoroutinefunction(fn): + prediction = await fn(*processed_input) + else: + prediction = await anyio.to_thread.run_sync( # type: ignore + fn, *processed_input, limiter=self.limiter + ) + else: + prediction = None + + if inspect.isgeneratorfunction(fn) or inspect.isasyncgenfunction(fn): + try: + if iterator is None: + iterator = cast(AsyncIterator[Any], prediction) + if inspect.isgenerator(iterator): + iterator = utils.SyncToAsyncIterator(iterator, self.limiter) + prediction = await utils.async_iteration(iterator) + is_generating = True + except StopAsyncIteration: + n_outputs = len(block_fn.outputs) + prediction = ( + components._Keywords.FINISHED_ITERATING + if n_outputs == 1 + else (components._Keywords.FINISHED_ITERATING,) * n_outputs + ) + iterator = None + + duration = time.time() - start + + return { + "prediction": prediction, + "duration": duration, + "is_generating": is_generating, + "iterator": iterator, + } + + def serialize_data(self, fn_index: int, inputs: list[Any]) -> list[Any]: + dependency = self.fns[fn_index] + processed_input = [] + + def format_file(s): + return FileData(path=s).model_dump() + + for i, block in enumerate(dependency.inputs): + if not isinstance(block, components.Component): + raise InvalidComponentError( + f"{block.__class__} Component not a valid input component." + ) + api_info = block.api_info() + if client_utils.value_is_file(api_info): + serialized_input = client_utils.traverse( + inputs[i], + format_file, + lambda s: ( + client_utils.is_filepath(s) or client_utils.is_http_url_like(s) + ), + ) + else: + serialized_input = inputs[i] + processed_input.append(serialized_input) + + return processed_input + + def deserialize_data(self, fn_index: int, outputs: list[Any]) -> list[Any]: + dependency = self.fns[fn_index] + predictions = [] + + for o, block in enumerate(dependency.outputs): + if not isinstance(block, components.Component): + raise InvalidComponentError( + f"{block.__class__} Component not a valid output component." + ) + + deserialized = client_utils.traverse( + outputs[o], lambda s: s["path"], client_utils.is_file_obj + ) + predictions.append(deserialized) + + return predictions + + def validate_inputs(self, block_fn: BlockFunction, inputs: list[Any]): + dep_inputs = block_fn.inputs + + # This handles incorrect inputs when args are changed by a JS function + # Only check not enough args case, ignore extra arguments (for now) + # TODO: make this stricter? + if len(inputs) < len(dep_inputs): + name = ( + f" ({block_fn.name})" + if block_fn.name and block_fn.name != "" + else "" + ) + + wanted_args = [] + received_args = [] + for block in dep_inputs: + wanted_args.append(str(block)) + for inp in inputs: + v = f'"{inp}"' if isinstance(inp, str) else str(inp) + received_args.append(v) + + wanted = ", ".join(wanted_args) + received = ", ".join(received_args) + + # JS func didn't pass enough arguments + raise ValueError( + f"""An event handler{name} didn't receive enough input values (needed: {len(dep_inputs)}, got: {len(inputs)}). +Check if the event handler calls a Javascript function, and make sure its return value is correct. +Wanted inputs: + [{wanted}] +Received inputs: + [{received}]""" + ) + + @staticmethod + def _format_processing_error( + block_fn: BlockFunction, + index: int, + block: Block, + value: Any, + is_input: bool, + original_error: Exception, + ) -> str: + name = ( + f' (named "{block_fn.name}")' + if block_fn.name and block_fn.name != "" + else "" + ) + blocks = block_fn.inputs if is_input else block_fn.outputs + components_list = ", ".join(b.get_block_name() for b in blocks) + + value_repr = repr(value) + if len(value_repr) > 200: + value_repr = value_repr[:200] + "..." + + kind = "input" if is_input else "output" + stage = "preprocess" if is_input else "postprocess" + verb = "passed to" if is_input else "returned from" + + method = getattr(block, "preprocess" if is_input else "postprocess", None) + expected_type = None + if method is not None: + try: + params = list(inspect.signature(method).parameters.values()) + if params and params[0].annotation is not inspect.Parameter.empty: + annotation = params[0].annotation + expected_type = ( + annotation + if isinstance(annotation, str) + else getattr(annotation, "__name__", str(annotation)) + ) + except (ValueError, TypeError): + expected_type = None + + expected_clause = ( + f"Expected a `{expected_type}`, but the" if expected_type else "The" + ) + return ( + f"Could not {stage} {kind} component at index {index} " + f"(a `{block.get_block_name()}` component) of the event handler{name} " + f"with {kind} components: [{components_list}].\n\n" + f"{expected_clause} value {verb} the event handler was: {value_repr} " + f"(of type `{type(value).__name__}`).\n\n" + f"Original error: {type(original_error).__name__}: {original_error}" + ) + + async def preprocess_data( + self, + block_fn: BlockFunction, + inputs: list[Any], + state: SessionState | None, + explicit_call: bool = False, + ): + state = state or SessionState(self) + + self.validate_inputs(block_fn, inputs) + + processed_input = [] + for i, block in enumerate(block_fn.inputs): + if not isinstance(block, components.Component): + raise InvalidComponentError( + f"{block.__class__} Component not a valid input component." + ) + if block.stateful: + processed_input.append(state[block._id]) + else: + if block._id in state: + block = state[block._id] + + is_prop_input = i in block_fn.component_prop_inputs + if is_prop_input: + processing_utils.check_all_files_in_cache(inputs[i]) + value_to_process = ( + inputs[i].get("value", None) if is_prop_input else inputs[i] + ) + + from gradio.profiling import trace_phase + + async with trace_phase("preprocess_move_to_cache"): + inputs_cached = await processing_utils.async_move_files_to_cache( + value_to_process, + block, + check_in_upload_folder=not explicit_call, + ) + if getattr(block, "data_model", None) and inputs_cached is not None: + data_model = cast( + Union[GradioModel, GradioRootModel], block.data_model + ) + inputs_cached = data_model.model_validate( + inputs_cached, context={"validate_meta": True} + ) + if isinstance(inputs_cached, (GradioModel, GradioRootModel)): + inputs_serialized = inputs_cached.model_dump() + else: + inputs_serialized = inputs_cached + + if block._id not in state: + state[block._id] = block + state._update_value_in_config(block._id, inputs_serialized) + + if block_fn.preprocess: + try: + processed_value = await anyio.to_thread.run_sync( + block.preprocess, inputs_cached, limiter=self.limiter + ) + except Error: + raise + except Exception as err: + raise ComponentProcessingError( + self._format_processing_error( + block_fn, + i, + block, + value_to_process, + is_input=True, + original_error=err, + ) + ) from err + else: + processed_value = inputs_serialized + + if is_prop_input: + inputs[i]["value"] = processed_value + processed_input.append(inputs[i]) + else: + processed_input.append(processed_value) + return processed_input + + def validate_outputs(self, block_fn: BlockFunction, predictions: Any | list[Any]): + dep_outputs = block_fn.outputs + + if not isinstance(predictions, (list, tuple)): + predictions = [predictions] + + if len(predictions) != len(dep_outputs): + name = ( + f" ({block_fn.name})" + if block_fn.name and block_fn.name != "" + else "" + ) + + wanted_args = [] + received_args = [] + for block in dep_outputs: + wanted_args.append(str(block.get_block_class())) + for pred in predictions: + v = f'"{pred}"' if isinstance(pred, str) else str(pred) + received_args.append(v) + + wanted = ", ".join(wanted_args) + received = ", ".join(received_args) + + if len(predictions) < len(dep_outputs): + raise ValueError( + f"""A function{name} didn't return enough output values (needed: {len(dep_outputs)}, returned: {len(predictions)}). + Output components: + [{wanted}] + Output values returned: + [{received}]""" + ) + else: + if len(predictions) == 1 and predictions[0] is None: + # do not throw error if the function did not return anything + # https://github.com/gradio-app/gradio/issues/9742 + return + warnings.warn( + f"""A function{name} returned too many output values (needed: {len(dep_outputs)}, returned: {len(predictions)}). Ignoring extra values. + Output components: + [{wanted}] + Output values returned: + [{received}]""" + ) + + async def postprocess_data( + self, + block_fn: BlockFunction, + predictions: list | dict, + state: SessionState | None, + ) -> list[Any]: + from gradio.profiling import trace_phase + + state = state or SessionState(self) + if ( + isinstance(predictions, dict) + and predictions == skip() + and len(block_fn.outputs) > 1 + ): + # For developer convenience, if a function returns a single skip() with multiple outputs, + # we will skip updating all outputs. + predictions = [skip()] * len(block_fn.outputs) + if isinstance(predictions, dict) and len(predictions) > 0: + predictions = convert_component_dict_to_list( + [block._id for block in block_fn.outputs], predictions + ) + + if len(block_fn.outputs) == 1 and not block_fn.batch: + predictions = [ + predictions, + ] + + self.validate_outputs(block_fn, predictions) # type: ignore + + output = [] + for i, block in enumerate(block_fn.outputs): + try: + if predictions[i] is components._Keywords.FINISHED_ITERATING: + output.append(None) + continue + except (IndexError, KeyError) as err: + raise ValueError( + "Number of output components does not match number " + f"of values returned from from function {block_fn.name}" + ) from err + + if block.stateful: + prediction_value = predictions[i] + if utils.is_prop_update(prediction_value): + # Support gr.update(value=...) for State components + if "value" in prediction_value: + state[block._id] = prediction_value["value"] + else: + state[block._id] = prediction_value + output.append(None) + else: + prediction_value = predictions[i] + if utils.is_prop_update( + prediction_value + ): # if update is passed directly (deprecated), remove Nones + prediction_value = utils.delete_none( + prediction_value, skip_value=True + ) + + if isinstance(prediction_value, Block): + prediction_value = prediction_value.constructor_args.copy() + prediction_value["__type__"] = "update" + elif isinstance(prediction_value, SimpleNamespace) and getattr( + prediction_value, "_is_component_update", False + ): + prediction_value = vars(prediction_value).copy() + keys = inspect.signature(block.__class__.__init__).parameters.keys() + prediction_value = { + k: v for k, v in prediction_value.items() if k in keys + } + prediction_value["__type__"] = "update" + if utils.is_prop_update(prediction_value): + kwargs = state[block._id].constructor_args.copy() + kwargs.update(prediction_value) + kwargs.pop("value", None) + kwargs.pop("__type__") + kwargs["render"] = False + + state[block._id] = block.__class__(**kwargs) + state._update_config(block._id) + prediction_value = postprocess_update_dict( + block=state[block._id], + update_dict=prediction_value, + postprocess=block_fn.postprocess, + ) + if "value" in prediction_value: + state._update_value_in_config( + block._id, prediction_value.get("value") + ) + elif block_fn.postprocess: + if not isinstance(block, components.Component): + raise InvalidComponentError( + f"{block.__class__} Component not a valid output component." + ) + if block._id in state: + block = state[block._id] + try: + prediction_value = await anyio.to_thread.run_sync( + block.postprocess, prediction_value, limiter=self.limiter + ) + except Error: + raise + except Exception as err: + raise ComponentProcessingError( + self._format_processing_error( + block_fn, + i, + block, + predictions[i], + is_input=False, + original_error=err, + ) + ) from err + if isinstance(prediction_value, (GradioModel, GradioRootModel)): + prediction_value_serialized = prediction_value.model_dump() + else: + prediction_value_serialized = prediction_value + async with trace_phase("postprocess_update_state_in_config"): + prediction_value_serialized = ( + await processing_utils.async_move_files_to_cache( + prediction_value_serialized, + block, + postprocess=True, + ) + ) + if block._id not in state: + state[block._id] = block + state._update_value_in_config( + block._id, prediction_value_serialized + ) + elif not block_fn.postprocess: + if block._id not in state: + state[block._id] = block + state._update_value_in_config(block._id, prediction_value) + async with trace_phase("postprocess_move_to_cache"): + outputs_cached = await processing_utils.async_move_files_to_cache( + prediction_value, + block, + postprocess=True, + ) + output.append(outputs_cached) + + return output + + async def handle_streaming_outputs( + self, + block_fn: BlockFunction, + data: list, + session_hash: str | None, + run: int | None, + root_path: str | None = None, + final: bool = False, + ) -> list: + if session_hash is None or run is None: + return data + if run not in self.pending_streams[session_hash]: + self.pending_streams[session_hash][run] = {} + stream_run: dict[int, MediaStream] = self.pending_streams[session_hash][run] + + for i, block in enumerate(block_fn.outputs): + output_id = block._id + if ( + isinstance(block, components.StreamingOutput) + and block.streaming + and not utils.is_prop_update(data[i]) + ): + if final: + stream_run[output_id].end_stream() + first_chunk = output_id not in stream_run + binary_data, output_data = await block.stream_output( + data[i], + f"{session_hash}/{run}/{output_id}/playlist.m3u8", + first_chunk, + ) + if first_chunk: + desired_output_format = None + if orig_name := output_data.get("orig_name"): + desired_output_format = Path(orig_name).suffix[1:] + stream_run[output_id] = MediaStream( + desired_output_format=desired_output_format + ) + stream_run[output_id] + + await stream_run[output_id].add_segment(binary_data) + output_data = await processing_utils.async_move_files_to_cache( + output_data, + block, + postprocess=True, + ) + if root_path is not None: + output_data = processing_utils.add_root_url( + output_data, root_path, None + ) + data[i] = output_data + + return data + + def handle_streaming_diffs( + self, + block_fn: BlockFunction, + data: list, + session_hash: str | None, + run: int | None, + final: bool, + simple_format: bool = False, + ) -> list: + if session_hash is None or run is None: + return data + first_run = run not in self.pending_diff_streams[session_hash] + if first_run: + self.pending_diff_streams[session_hash][run] = [None] * len(data) + last_diffs = self.pending_diff_streams[session_hash][run] + + for i in range(len(block_fn.outputs)): + if final: + data[i] = last_diffs[i] + continue + + if first_run: + last_diffs[i] = data[i] + else: + prev_chunk = last_diffs[i] + last_diffs[i] = data[i] + if not simple_format: + data[i] = utils.diff(prev_chunk, data[i]) + + if final: + del self.pending_diff_streams[session_hash][run] + + return data + + async def process_api( + self, + block_fn: BlockFunction | int, + inputs: list[Any], + state: SessionState | None = None, + request: Request | list[Request] | None = None, + iterator: AsyncIterator | None = None, + session_hash: str | None = None, + event_id: str | None = None, + event_data: EventData | None = None, + in_event_listener: bool = True, + simple_format: bool = False, + explicit_call: bool = False, + root_path: str | None = None, + ) -> dict[str, Any]: + """ + Processes API calls from the frontend. First preprocesses the data, + then runs the relevant function, then postprocesses the output. + + Parameters: + fn_index: Index of function to run. + inputs: input data received from the frontend + state: data stored from stateful components for session (key is input block id) + request: the gr.Request object containing information about the network request (e.g. IP address, headers, query parameters, username) + iterators: the in-progress iterators for each generator function (key is function index) + event_id: id of event that triggered this API call + event_data: data associated with the event trigger itself + in_event_listener: whether this API call is being made in response to an event listener + explicit_call: whether this call is being made directly by calling the Blocks function, instead of through an event listener or API route + root_path: if provided, the root path of the server. All file URLs will be prefixed with this path. + + Returns a dictionary with the following keys: + - "data": the output data from the function + - "is_generating": whether the function is generating output + - "iterator": the iterator for the function + - "duration": the duration of the function call + - "average_duration": the average duration of the function call + - "render_config": the render config for the function + """ + if isinstance(block_fn, int): + block_fn = self.fns[block_fn] + batch = block_fn.batch + state_ids_to_track, hashed_values = self.get_state_ids_to_track(block_fn, state) + changed_state_ids = [] + LocalContext.blocks.set(self) + + if batch: + max_batch_size = block_fn.max_batch_size + batch_sizes = [len(inp) for inp in inputs] + batch_size = batch_sizes[0] + manual_cache_used = False + if inspect.isasyncgenfunction(block_fn.fn) or inspect.isgeneratorfunction( + block_fn.fn + ): + raise ValueError("Gradio does not support generators in batch mode.") + if not all(x == batch_size for x in batch_sizes): + raise ValueError( + f"All inputs to a batch function must have the same length but instead have sizes: {batch_sizes}." + ) + if batch_size > max_batch_size: + raise ValueError( + f"Batch size ({batch_size}) exceeds the max_batch_size for this function ({max_batch_size})" + ) + inputs = [ + await self.preprocess_data(block_fn, list(i), state, explicit_call) + for i in zip(*inputs, strict=False) + ] + with TrackManualCacheUsage(): + result = await self.call_function( + block_fn, + list(zip(*inputs, strict=False)), + None, + request, + event_id, + event_data, + in_event_listener, + state, + ) + manual_cache_used = used_manual_cache() + preds = result["prediction"] + data = [ + await self.postprocess_data(block_fn, list(o), state) + for o in zip(*preds, strict=False) + ] + if root_path is not None: + data = processing_utils.add_root_url(data, root_path, None) # type: ignore + data = list(zip(*data, strict=False)) + is_generating, iterator = None, None + else: + from gradio.profiling import trace_phase + + old_iterator = iterator + manual_cache_used = False + if old_iterator: + inputs = [] + else: + async with trace_phase("preprocess"): + inputs = await self.preprocess_data( + block_fn, inputs, state, explicit_call + ) + was_generating = old_iterator is not None + with TrackManualCacheUsage(): + async with trace_phase("fn_call"): + result = await self.call_function( + block_fn, + inputs, + old_iterator, + request, + event_id, + event_data, + in_event_listener, + state, + ) + manual_cache_used = used_manual_cache() + + async with trace_phase("postprocess"): + data = await self.postprocess_data( + block_fn, result["prediction"], state + ) + if state: + changed_state_ids = [ + state_id + for hash_value, state_id in zip( + hashed_values, state_ids_to_track, strict=False + ) + if hash_value != utils.deep_hash(state[state_id]) + ] + + if root_path is not None: + data = processing_utils.add_root_url(data, root_path, None) + is_generating, iterator = result["is_generating"], result["iterator"] + if is_generating or was_generating: + run = id(old_iterator) if was_generating else id(iterator) + async with trace_phase("streaming_diff"): + data = await self.handle_streaming_outputs( + block_fn, + data, + session_hash=session_hash, + run=run, + root_path=root_path, + final=not is_generating, + ) + data = self.handle_streaming_diffs( + block_fn, + data, + session_hash=session_hash, + run=run, + final=not is_generating, + simple_format=simple_format, + ) + + if not manual_cache_used: + block_fn.total_runtime += result["duration"] + block_fn.total_runs += 1 + output = { + "data": data, + "is_generating": is_generating, + "iterator": iterator, + "duration": result["duration"], + "average_duration": ( + block_fn.total_runtime / block_fn.total_runs + if block_fn.total_runs > 0 + else None + ), + "render_config": None, + "changed_state_ids": changed_state_ids, + "used_cache": "partial" if manual_cache_used else None, + } + if block_fn.renderable and state: + output["render_config"] = state.blocks_config.get_config( + block_fn.renderable + ) + output["render_config"]["render_id"] = block_fn.renderable._id + if root_path is not None: + output["render_config"] = processing_utils.add_root_url( + output["render_config"], root_path, None + ) + + return output + + def get_state_ids_to_track( + self, block_fn: BlockFunction, state: SessionState | None + ) -> tuple[list[int], list]: + if state is None: + return [], [] + state_ids_to_track = [] + hashed_values = [] + for block in block_fn.outputs: + if block.stateful and any( + (block._id, "change") in fn.targets + for fn in state.blocks_config.fns.values() + ): + value = state[block._id] + state_ids_to_track.append(block._id) + hashed_values.append(utils.deep_hash(value)) + return state_ids_to_track, hashed_values + + def create_limiter(self): + self.limiter = ( + None + if self.max_threads == 40 + else CapacityLimiter(total_tokens=self.max_threads) + ) + + def get_config(self): # type: ignore[override] + return {"type": "column"} + + def get_config_file(self) -> BlocksConfigDict: + config: BlocksConfigDict = { + "version": VERSION, + "api_prefix": API_PREFIX, + "mode": self.mode, + "app_id": self.app_id, + "dev_mode": self.dev_mode, + "vibe_mode": self.vibe_mode, + "analytics_enabled": self.analytics_enabled, + "components": [], + "css": self.css, + "connect_heartbeat": False, + "js": self.js, + "head": self.head, + "title": self.title or "Gradio", + "space_id": self.space_id, + "enable_queue": True, # launch attributes + "show_error": getattr(self, "show_error", False), + "footer_links": getattr(self, "footer_links", []), + "is_colab": utils.colab_check(), + "max_file_size": getattr(self, "max_file_size", None), + "stylesheets": getattr(self, "stylesheets", []), + "theme": self.theme.name if self.theme is not None else None, + "protocol": "sse_v3", + "body_css": { # type: ignore + "body_background_fill": self.theme._get_computed_value( + "body_background_fill" + ), + "body_text_color": self.theme._get_computed_value("body_text_color"), + "body_background_fill_dark": self.theme._get_computed_value( + "body_background_fill_dark" + ), + "body_text_color_dark": self.theme._get_computed_value( + "body_text_color_dark" + ), + } + if getattr(self, "theme", None) is not None + else None, + "fill_height": self.fill_height, + "fill_width": self.fill_width, + "theme_hash": getattr(self, "theme_hash", None), # type: ignore + "pwa": self.pwa, + "pages": self.pages, # type: ignore + "page": {}, + "mcp_server": self.mcp_server, + "i18n_translations": ( + getattr(self.i18n_instance, "translations_dict", None) + if getattr(self, "i18n_instance", None) is not None + else None + ), + } + config.update(self.default_config.get_config()) # type: ignore + config["connect_heartbeat"] = utils.connect_heartbeat( + config, self.blocks.values(), self.fns.values() + ) + return config + + def transpile_to_js(self, quiet: bool = False): + fns_to_transpile = [ + fn.fn for fn in self.fns.values() if fn.fn and fn.js is True + ] + num_to_transpile = len(fns_to_transpile) + if not quiet and num_to_transpile > 0: + print("********************************************") + print("* Trying to transpile functions from Python -> JS for performance\n") + for index, fn in enumerate(fns_to_transpile): + if not quiet: + print(f"* ({index + 1}/{num_to_transpile}) {fn.__name__}: ", end="") # type: ignore + if getattr(fn, "__js_implementation__", None) is None: # type: ignore + try: + fn.__js_implementation__ = transpile(fn, validate=True) # type: ignore + if not quiet: + print("✅") + except Exception as e: + if not quiet: + print("❌", e, end="\n\n") + elif not quiet: + print("✅") + if not quiet and num_to_transpile > 0: + print("********************************************\n") + + def __enter__(self): + render_context = get_render_context() + if render_context is None: + Context.root_block = self + self.parent = render_context + set_render_context(self) + self.exited = False + return self + + def __exit__(self, exc_type: type[BaseException] | None = None, *args): + if exc_type is not None: + set_render_context(None) + Context.root_block = None + return + super().fill_expected_parents() + set_render_context(self.parent) + # Configure the load events before root_block is reset + self.default_config.attach_load_events() + if self.parent is None: + Context.root_block = None + else: + self.parent.children.extend(self.children) + self.config = self.get_config_file() + self.app = App.create_app(self, mcp_server=False) + self.progress_tracking = any( + block_fn.tracks_progress for block_fn in self.fns.values() + ) + self.page = "" + self.validate_navbar_settings() + self.exited = True + + def clear(self): + """Resets the layout of the Blocks object.""" + self.default_config.blocks = {} + self.default_config.fns = {} + self.children = [] + return self + + @document() + def queue( + self, + status_update_rate: float | Literal["auto"] = "auto", + api_open: bool | None = None, + max_size: int | None = None, + *, + default_concurrency_limit: int | None | Literal["not_set"] = "not_set", + ): + """ + By enabling the queue you can control when users know their position in the queue, and set a limit on maximum number of events allowed. + Parameters: + status_update_rate: If "auto", Queue will send status estimations to all clients whenever a job is finished. Otherwise Queue will send status at regular intervals set by this parameter as the number of seconds. + api_open: If True, the REST routes of the backend will be open, allowing requests made directly to those endpoints to skip the queue. + max_size: The maximum number of events the queue will store at any given moment. If the queue is full, new events will not be added and a user will receive a message saying that the queue is full. If None, the queue size will be unlimited. + default_concurrency_limit: The default value of `concurrency_limit` to use for event listeners that don't specify a value. Can be set by environment variable GRADIO_DEFAULT_CONCURRENCY_LIMIT. Defaults to 1 if not set otherwise. + Example: (Blocks) + with gr.Blocks() as demo: + button = gr.Button(label="Generate Image") + button.click(fn=image_generator, inputs=gr.Textbox(), outputs=gr.Image()) + demo.queue(max_size=10) + demo.launch() + Example: (Interface) + demo = gr.Interface(image_generator, gr.Textbox(), gr.Image()) + demo.queue(max_size=20) + demo.launch() + """ + if api_open is not None: + self.api_open = api_open + if utils.is_zero_gpu_space(): + max_size = 1 if max_size is None else max_size + self._queue = queueing.Queue( + live_updates=status_update_rate == "auto", + concurrency_count=self.max_threads, + update_intervals=status_update_rate if status_update_rate != "auto" else 1, + max_size=max_size, + blocks=self, + default_concurrency_limit=default_concurrency_limit, + ) + self.app = App.create_app(self, mcp_server=False) + return self + + def validate_queue_settings(self): + for dep in self.fns.values(): + for i in dep.cancels: + if not self.fns[i].queue: + raise ValueError( + "Queue needs to be enabled! " + "You may get this error by either 1) passing a function that uses the yield keyword " + "into an interface without enabling the queue or 2) defining an event that cancels " + "another event without enabling the queue. Both can be solved by calling .queue() " + "before .launch()" + ) + + def validate_navbar_settings(self): + """Validates that only one Navbar component exists per page.""" + from gradio.components.navbar import Navbar + + navbar_pages = set() + for block in self.blocks.values(): + if isinstance(block, Navbar): + if block.page in navbar_pages: + raise ValueError( + f"Only one gr.Navbar component can exist per page. " + f"Found multiple Navbar components on page '{block.page or 'Home'}'. " + "Please remove the extra Navbar components." + ) + navbar_pages.add(block.page) + + def _set_html_css_theme_variables(self): + self.theme_css = self.theme._get_theme_css() + self.stylesheets = self.theme._stylesheets + theme_hasher = hashlib.sha256() + theme_hasher.update(self.theme_css.encode("utf-8")) + self.theme_hash = theme_hasher.hexdigest() + css_paths = utils.none_or_singleton_to_list(self.css_paths) + for css_path in css_paths or []: + self.css = self.css or "" + with open(css_path, encoding="utf-8") as css_file: + self.css += "\n" + css_file.read() + + head_paths = utils.none_or_singleton_to_list(self.head_paths) + for head_path in head_paths or []: + self.head = self.head or "" + with open(head_path, encoding="utf-8") as head_file: + self.head += "\n" + head_file.read() + + def _resolve_ssr_mode( + self, ssr_mode: bool | None, disable_when_multi_page: bool = True + ) -> bool: + if disable_when_multi_page and len(self.config.get("pages", [])) > 1: + warnings.warn( + "SSR mode is not supported with multi-page apps when mounting on a FastAPI app. Disabling SSR mode.", + UserWarning, + ) + return False + return ( + ssr_mode + if ssr_mode is not None + else os.getenv("GRADIO_SSR_MODE", "False").lower() == "true" + ) + + def launch( + self, + inline: bool | None = None, + inbrowser: bool = False, + share: bool | None = None, + debug: bool = False, + max_threads: int = 40, + auth: ( + Callable[[str, str], bool] | tuple[str, str] | list[tuple[str, str]] | None + ) = None, + auth_message: str | None = None, + prevent_thread_lock: bool = False, + show_error: bool = False, + server_name: str | None = None, + server_port: int | None = None, + *, + height: int = 500, + width: int | str = "100%", + favicon_path: str | Path | None = None, + ssl_keyfile: str | None = None, + ssl_certfile: str | None = None, + ssl_keyfile_password: str | None = None, + ssl_verify: bool = True, + quiet: bool = False, + footer_links: list[Literal["api", "gradio", "settings"] | dict[str, str]] + | None = None, + allowed_paths: list[str] | None = None, + blocked_paths: list[str] | None = None, + root_path: str | None = None, + app_kwargs: dict[str, Any] | None = None, + state_session_capacity: int = 10000, + share_server_address: str | None = None, + share_server_protocol: Literal["http", "https"] | None = None, + share_server_tls_certificate: str | None = None, + auth_dependency: Callable[[fastapi.Request], str | None] | None = None, + max_file_size: str | int | None = None, + enable_monitoring: bool | None = None, + strict_cors: bool = True, + node_server_name: str | None = None, + node_port: int | None = None, + ssr_mode: bool | None = None, + pwa: bool | None = None, + mcp_server: bool | None = None, + num_workers: int | None = None, + _app: App | None = None, + _frontend: bool = True, + i18n: I18n | None = None, + theme: Theme | str | None = None, + css: str | None = None, + css_paths: str | Path | Sequence[str | Path] | None = None, + js: str | Literal[True] | None = None, + head: str | None = None, + head_paths: str | Path | Sequence[str | Path] | None = None, + ) -> tuple[App, str, str]: + """ + Launches a simple web server that serves the demo. Can also be used to create a + public link used by anyone to access the demo from their browser by setting share=True. + Parameters: + inline: whether to display in the gradio app inline in an iframe. Defaults to True in python notebooks; False otherwise. + inbrowser: whether to automatically launch the gradio app in a new tab on the default browser. + share: whether to create a publicly shareable link for the gradio app. Creates an SSH tunnel to make your UI accessible from anywhere. If not provided, it is set to False by default every time, except when running in Google Colab. When localhost is not accessible (e.g. Google Colab), setting share=False is not supported. Can be set by environment variable GRADIO_SHARE=True. + debug: if True, blocks the main thread from running. If running in Google Colab, this is needed to print the errors in the cell output. + auth: If provided, username and password (or list of username-password tuples) required to access app. Can also provide function that takes username and password and returns True if valid login. + auth_message: If provided, HTML message provided on login page. + prevent_thread_lock: By default, the gradio app blocks the main thread while the server is running. If set to True, the gradio app will not block and the gradio server will terminate as soon as the script finishes. + show_error: If True, any errors in the gradio app will be displayed in an alert modal and printed in the browser console log. They will also be displayed in the alert modal of downstream apps that gr.load() this app. + server_port: will start gradio app on this port (if available). Can be set by environment variable GRADIO_SERVER_PORT. If None, will search for an available port starting at 7860. + server_name: to make app accessible on local network, set this to "0.0.0.0". Can be set by environment variable GRADIO_SERVER_NAME. If None, will use "127.0.0.1". + max_threads: the maximum number of total threads that the Gradio app can generate in parallel. The default is inherited from the starlette library (currently 40). + width: The width in pixels of the iframe element containing the gradio app (used if inline=True) + height: The height in pixels of the iframe element containing the gradio app (used if inline=True) + favicon_path: If a path to a file (.png, .gif, or .ico) is provided, it will be used as the favicon for the web page. + ssl_keyfile: If a path to a file is provided, will use this as the private key file to create a local server running on https. + ssl_certfile: If a path to a file is provided, will use this as the signed certificate for https. Needs to be provided if ssl_keyfile is provided. + ssl_keyfile_password: If a password is provided, will use this with the ssl certificate for https. + ssl_verify: If False, skips certificate validation which allows self-signed certificates to be used. + quiet: If True, suppresses most print statements. + footer_links: The links to display in the footer of the app. Accepts a list, where each element of the list must be one of "api", "gradio", or "settings" corresponding to the API docs, "built with Gradio", and settings pages respectively. If None, all three links will be shown in the footer. An empty list means that no footer is shown. + allowed_paths: List of complete filepaths or parent directories that gradio is allowed to serve. Must be absolute paths. Warning: if you provide directories, any files in these directories or their subdirectories are accessible to all users of your app. Can be set by comma separated environment variable GRADIO_ALLOWED_PATHS. These files are generally assumed to be secure and will be displayed in the browser when possible. + blocked_paths: List of complete filepaths or parent directories that gradio is not allowed to serve (i.e. users of your app are not allowed to access). Must be absolute paths. Warning: takes precedence over `allowed_paths` and all other directories exposed by Gradio by default. Can be set by comma separated environment variable GRADIO_BLOCKED_PATHS. + root_path: The root path (or "mount point") of the application, if it's not served from the root ("/") of the domain. Often used when the application is behind a reverse proxy that forwards requests to the application. For example, if the application is served at "https://example.com/myapp", the `root_path` should be set to "/myapp". A full URL beginning with http:// or https:// can be provided, which will be used as the root path in its entirety. Can be set by environment variable GRADIO_ROOT_PATH. Defaults to "". + app_kwargs: Additional keyword arguments to pass to the underlying FastAPI app as a dictionary of parameter keys and argument values. For example, `{"docs_url": "/docs"}` + state_session_capacity: The maximum number of sessions whose information to store in memory. If the number of sessions exceeds this number, the oldest sessions will be removed. Reduce capacity to reduce memory usage when using gradio.State or returning updated components from functions. Defaults to 10000. + share_server_address: Use this to specify a custom FRP server and port for sharing Gradio apps (only applies if share=True). If not provided, will use the default FRP server at https://gradio.live. See https://github.com/huggingface/frp for more information. + share_server_protocol: Use this to specify the protocol to use for the share links. Defaults to "https", unless a custom share_server_address is provided, in which case it defaults to "http". If you are using a custom share_server_address and want to use https, you must set this to "https". + share_server_tls_certificate: The path to a TLS certificate file to use when connecting to a custom share server. This parameter is not used with the default FRP server at https://gradio.live. Otherwise, you must provide a valid TLS certificate file (e.g. a "cert.pem") relative to the current working directory, or the connection will not use TLS encryption, which is insecure. + auth_dependency: A function that takes a FastAPI request and returns a string user ID or None. If the function returns None for a specific request, that user is not authorized to access the app (they will see a 401 Unauthorized response). To be used with external authentication systems like OAuth. Cannot be used with `auth`. + max_file_size: The maximum file size in bytes that can be uploaded. Can be a string of the form "", where value is any positive integer and unit is one of "b", "kb", "mb", "gb", "tb". If None, no limit is set. + enable_monitoring: Enables traffic monitoring of the app through the /monitoring endpoint. By default is None, which enables this endpoint. If explicitly True, will also print the monitoring URL to the console. If False, will disable monitoring altogether. + strict_cors: If True, prevents external domains from making requests to a Gradio server running on localhost. If False, allows requests to localhost that originate from localhost but also, crucially, from "null". This parameter should normally be True to prevent CSRF attacks but may need to be False when embedding a *locally-running Gradio app* using web components. + ssr_mode: If True, the Gradio app will be rendered using server-side rendering mode, which is typically more performant and provides better SEO, but this requires Node 20+ to be installed on the system. If False, the app will be rendered using client-side rendering mode. If None, will use GRADIO_SSR_MODE environment variable or default to False. + pwa: If True, the Gradio app will be set up as an installable PWA (Progressive Web App). If set to None (default behavior), then the PWA feature will be enabled if this Gradio app is launched on Spaces, but not otherwise. + i18n: An I18n instance containing custom translations, which are used to translate strings in our components (e.g. the labels of components or Markdown strings). This feature can only be used to translate static text in the frontend, not values in the backend. + mcp_server: If True, the Gradio app will be set up as an MCP server and documented functions will be added as MCP tools. If None (default behavior), then the GRADIO_MCP_SERVER environment variable will be used to determine if the MCP server should be enabled. + num_workers: Number of background workers to launch in the background to serve file I/O and static assets. This offloads traffic from the main server and reduces latency. Only has an effect if ssr mode is set. + theme: A Theme object or a string representing a theme. If a string, will look for a built-in theme with that name (e.g. "soft" or "default"), or will attempt to load a theme from the Hugging Face Hub (e.g. "gradio/monochrome"). If None, will use the Default theme. + css: Custom css as a code string. This css will be included in the demo webpage. + css_paths: Custom css as a pathlib.Path to a css file or a list of such paths. This css files will be read, concatenated, and included in the demo webpage. If the `css` parameter is also set, the css from `css` will be included first. + js: Custom js as a code string. The js code will automatically be executed when the page loads. For more flexibility, use the head parameter to insert js inside '`); `head` content " + "is injected and loaded before `js_on_load` runs. Then, if needed, put code " + "that uses those libraries in the `js_on_load` parameter, which executes when " + "the component renders. See " + "https://gradio.app/guides/custom-HTML-components for details.", + UserWarning, + stacklevel=2, + ) + + +@document() +class HTML(BlockContext, Component): + """ + Creates a component with arbitrary HTML. Can include CSS and JavaScript to create highly customized and interactive components. + Example: + ```python + import gradio as gr + + with gr.Blocks() as demo: + basic_html = gr.HTML("

This is a basic HTML component

") + + button_set = gr.HTML(["Option 1", "Option 2", "Option 3"], + html_template="{{#each value}}{{/each}}", + css_template="button { margin: 5px; padding: 10px; }", + js_on_load="element.querySelectorAll('.option-btn').forEach(btn => { btn.addEventListener('click', () => { trigger('click', {option: btn.innerText}); }); });") + clicked_option = gr.Textbox(label="Clicked Option") + def on_button_click(evt: gr.EventData): + return evt.option + button_set.click(on_button_click, outputs=clicked_option) + + demo.launch() + ``` + Demos: super_html + Guides: custom-HTML-components, custom-CSS-and-JS + """ + + EVENTS = all_events + + def __init__( + self, + 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, + ): + """ + Parameters: + value: The HTML content in the ${value} tag in the html_template. For example, if html_template="

${value}

" and value="Hello, world!", the component will render as `"

Hello, world!

"`. + label: The label for this component. Is used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to. + html_template: A string representing the HTML template for this component as a JS template string and Handlebars template. The `${value}` tag will be replaced with the `value` parameter, and all other tags will be filled in with the values from `props`. This element can have children when used in a `with gr.HTML(...):` context, and the children will be rendered to replace `@children` substring, which cannot be nested inside any HTML tags. + css_template: A string representing the CSS template for this component as a JS template string and Handlebars template. The CSS will be automatically scoped to this component, and rules outside a block will target the component's root element. The `${value}` tag will be replaced with the `value` parameter, and all other tags will be filled in with the values from `props`. + js_on_load: A string representing the JavaScript code that will be executed when the component is loaded. The `element` variable refers to the HTML element of this component, and can be used to access children such as `element.querySelector()`. The `trigger` function can be used to trigger events, such as `trigger('click')`. The value and other props can be edited through `props`, e.g. `props.value = "new value"` which will re-render the HTML template. If `server_functions` is provided, a `server` object is also available in `js_on_load`, where each function is accessible as an async method, e.g. `server.list_files(path).then(files => ...)` or `const files = await server.list_files(path)`. The `upload` async function can be used to upload a JavaScript `File` object to the Gradio server, returning a dictionary with `path` (the server-side file path) and `url` (the public URL to access the file), e.g. `const { path, url } = await upload(file)`. The `watch` function can be used to observe prop changes when the component is an output to a Python event listener: `watch('value', () => { ... })` runs the callback after the template re-renders whenever `value` changes, or `watch(['value', 'color'], () => { ... })` to watch multiple props.`. + apply_default_css: If True, default Gradio CSS styles will be applied to the HTML component. + head: A raw HTML string to inject into the document `` before `js_on_load` runs. Typically used for `", r.text + ) # some basic regex to extract the config + try: + config = json.loads(result.group(1)) # type: ignore + except AttributeError as ae: + raise ValueError(f"Could not load the Space: {space_name}") from ae + elif config_request.status_code == 200: + config = config_request.json() + else: + raise ValueError( + f"Could not load the Space: {space_name} because the config could not be fetched." + ) + if "allow_flagging" in config: # Create an Interface for Gradio 2.x Spaces + return from_spaces_interface( + space_name, config, alias, token, iframe_url, **kwargs + ) + else: # Create a Blocks for Gradio 3.x Spaces + if kwargs: + warnings.warn( + "You cannot override parameters for this Space by passing in kwargs. " + "Instead, please load the Space as a function and use it to create a " + "Blocks or Interface locally. You may find this Guide helpful: " + "https://gradio.app/using_blocks_like_functions/" + ) + return from_spaces_blocks(space=space_name, token=token) + + +def make_event_data_fn(client, endpoint): + """Create a function that accepts EventData. + The event_data_fn has to be created in this closure so that the value of endpoint + is correctly captured.""" + helper = client.new_helper(endpoint.fn_index) + + def event_data_fn(event_data: gr.EventData, *args): + fn = endpoint.make_end_to_end_fn(helper) + return fn(*args, event_data=event_data._data) + + return event_data_fn + + +def from_spaces_blocks(space: str, token: str | None) -> Blocks: + client = Client( + space, + token=token, + download_files=False, + _skip_components=False, + ) + # We set deserialize to False to avoid downloading output files from the server. + # Instead, we serve them as URLs using the /proxy/ endpoint directly from the server. + + if client.app_version < version.Version("4.0.0b14"): + raise GradioVersionIncompatibleError( + f"Gradio version 4.x cannot load spaces with versions less than 4.x ({client.app_version})." + "Please downgrade to version 3 to load this space." + ) + + # Use end_to_end_fn here to properly upload/download all files + predict_fns = [] + for endpoint in client.endpoints.values(): + if not isinstance(endpoint, Endpoint): + raise TypeError( + f"Expected endpoint to be an Endpoint, but got {type(endpoint)}" + ) + if endpoint.backend_fn: + dep_config = next( + ( + d + for d in client.config["dependencies"] + if d.get("id") == endpoint.fn_index + ), + {}, + ) + if dep_config.get("collects_event_data"): + event_data_fn = make_event_data_fn(client, endpoint) + predict_fns.append(event_data_fn) + else: + helper = client.new_helper(endpoint.fn_index) + fn = endpoint.make_end_to_end_fn(helper) + predict_fns.append(fn) + else: + predict_fns.append(None) + blocks = gr.Blocks.from_config(client.config, predict_fns, client.src) # type: ignore + with blocks: + # Reset the session_hash when page loads + blocks.load(lambda: client.reset_session(), None, None) + return blocks + + +def from_spaces_interface( + model_name: str, + config: dict, + alias: str | None, + token: str | None, + iframe_url: str, + **kwargs, +) -> Interface: + config = external_utils.streamline_spaces_interface(config) + api_url = f"{iframe_url}/api/predict/" + headers = {"Content-Type": "application/json"} + if token not in [False, None]: + headers["Authorization"] = f"Bearer {token}" + + # The function should call the API with preprocessed data + def fn(*data): + data = json.dumps({"data": data}) + response = httpx.post(api_url, headers=headers, data=data) # type: ignore + result = json.loads(response.content.decode("utf-8")) + if "error" in result and "429" in result["error"]: + raise TooManyRequestsError("Too many requests to the Hugging Face API") + try: + output = result["data"] + except KeyError as ke: + raise KeyError( + f"Could not find 'data' key in response from external Space. Response received: {result}" + ) from ke + if ( + len(config["outputs"]) == 1 + ): # if the fn is supposed to return a single value, pop it + output = output[0] + if ( + len(config["outputs"]) == 1 and isinstance(output, list) + ): # Needed to support Output.Image() returning bounding boxes as well (TODO: handle different versions of gradio since they have slightly different APIs) + output = output[0] + return output + + fn.__name__ = alias if (alias is not None) else model_name + config["fn"] = fn + + kwargs = dict(config, **kwargs) + kwargs["_api_mode"] = True + + fn = kwargs.pop("fn", None) + inputs = kwargs.pop("inputs", None) + outputs = kwargs.pop("outputs", None) + + interface = gr.Interface(fn, inputs, outputs, **kwargs) + return interface + + +TEXT_FILE_EXTENSIONS = ( + ".doc", + ".docx", + ".rtf", + ".epub", + ".odt", + ".odp", + ".pptx", + ".txt", + ".md", + ".py", + ".ipynb", + ".js", + ".jsx", + ".html", + ".css", + ".java", + ".cs", + ".php", + ".c", + ".cc", + ".cpp", + ".cxx", + ".cts", + ".h", + ".hh", + ".hpp", + ".rs", + ".R", + ".Rmd", + ".swift", + ".go", + ".rb", + ".kt", + ".kts", + ".ts", + ".tsx", + ".m", + ".mm", + ".mts", + ".scala", + ".dart", + ".lua", + ".pl", + ".pm", + ".t", + ".sh", + ".bash", + ".zsh", + ".bat", + ".coffee", + ".csv", + ".log", + ".ini", + ".cfg", + ".config", + ".json", + ".proto", + ".yaml", + ".yml", + ".toml", + ".sql", +) +IMAGE_FILE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".gif", ".webp") + + +def format_conversation( + history: list[NormalizedMessageDict], new_message: str | MultimodalValue +) -> list[dict]: + conversation = [] + for message in history: + new_content = [] + for content in message["content"]: + if "type" not in content: + raise ValueError( + f"Invalid message format: {message['content']}. Each element must have a type key." + ) + elif content["type"] == "file": + new_content.append( + { + "type": "image_url", + "image_url": { + "url": encode_url_or_file_to_base64(content["file"]["path"]) # type: ignore + }, + } + ) + else: + new_content.append(content) + message["content"] = new_content + conversation.append(message) + if isinstance(new_message, str): + text = new_message + files = [] + else: + text = new_message.get("text", None) + files = new_message.get("files", []) + image_files, text_encoded = [], [] + for file in files: + if file.lower().endswith(TEXT_FILE_EXTENSIONS): + text_encoded.append(file) + else: + image_files.append(file) + + for image in image_files: + conversation.append( + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": encode_url_or_file_to_base64(image)}, + } + ], + } + ) + if text or text_encoded: + text = text or "" + text += "\n".join( + [ + f"\n## {Path(file).name}\n{Path(file).read_text()}" + for file in text_encoded + ] + ) + conversation.append( + {"role": "user", "content": [{"type": "text", "text": text}]} + ) + return conversation + + +@document() +def load_chat( + base_url: str, + model: str, + token: str | None = None, + *, + file_types: Literal["text_encoded", "image"] + | list[Literal["text_encoded", "image"]] + | None = "text_encoded", + system_message: str | None = None, + streaming: bool = True, + **kwargs, +) -> ChatInterface: + """ + Load a chat interface from an OpenAI API chat compatible endpoint. + Parameters: + base_url: The base URL of the endpoint, e.g. "http://localhost:11434/v1/" + model: The name of the model you are loading, e.g. "llama3.2" + token: The API token or a placeholder string if you are using a local model, e.g. "ollama" + file_types: The file types allowed to be uploaded by the user. "text_encoded" allows uploading any text-encoded file (which is simply appended to the prompt), and "image" adds image upload support. Set to None to disable file uploads. + system_message: The system message to use for the conversation, if any. + streaming: Whether the response should be streamed. + kwargs: Additional keyword arguments to pass into ChatInterface for customization. + Example: + import gradio as gr + gr.load_chat( + "http://localhost:11434/v1/", + model="qwen2.5", + token="***", + file_types=["text_encoded", "image"], + system_message="You are a silly assistant.", + ).launch() + """ + try: + from openai import OpenAI + except ImportError as e: + raise ImportError( + "To use OpenAI API Client, you must install the `openai` package. You can install it with `pip install openai`." + ) from e + from gradio.chat_interface import ChatInterface + + client = OpenAI(api_key=token or "***", base_url=base_url) + start_message = ( + [{"role": "system", "content": system_message}] if system_message else [] + ) + file_types = utils.none_or_singleton_to_list(file_types) + + def open_api(message: str | MultimodalValue, history: list | None) -> str | None: + history = history or start_message + conversation = format_conversation(history, message) # type: ignore + return ( + client.chat.completions.create( + model=model, + messages=conversation, # type: ignore + ) + .choices[0] + .message.content + ) + + def open_api_stream( + message: str | MultimodalValue, history: list | None + ) -> Generator[str, None, None]: + history = history or start_message + conversation = format_conversation(history, message) # type: ignore + stream = client.chat.completions.create( + model=model, + messages=conversation, # type: ignore + stream=True, + ) + response = "" + for chunk in stream: + if chunk.choices and chunk.choices[0].delta.content is not None: + response += chunk.choices[0].delta.content + yield response + + supported_extensions = [] + for file_type in file_types: + if file_type == "text_encoded": + supported_extensions += TEXT_FILE_EXTENSIONS + elif file_type == "image": + supported_extensions += IMAGE_FILE_EXTENSIONS + else: + raise ValueError( + f"Invalid file type: {file_type}. Must be 'text_encoded' or 'image'." + ) + + if "chatbot" not in kwargs: + from gradio.components import Chatbot + + kwargs["chatbot"] = Chatbot(scale=1, allow_tags=True) + + textbox_arg = kwargs.pop("textbox", None) + if textbox_arg is not None: + textbox = textbox_arg + else: + textbox = ( + gr.MultimodalTextbox(file_types=supported_extensions) + if file_types + else None + ) + + return ChatInterface( + open_api_stream if streaming else open_api, + multimodal=bool(file_types), + textbox=textbox, + **kwargs, + ) + + +@document() +def load_openapi( + openapi_spec: str | dict, + base_url: str, + *, + paths: list[str] | None = None, + exclude_paths: list[str] | None = None, + methods: list[Literal["get", "post", "put", "patch", "delete"]] | None = None, + auth_token: str | None = None, + **interface_kwargs, +) -> Blocks: + """ + Load a Gradio app from an OpenAPI v3 specification. + + Parameters: + openapi_spec: URL, file path, or dictionary containing the OpenAPI specification (v3, JSON format only) + base_url: Base URL for the API endpoints, e.g. "https://api.example.com/v1". This is used to construct the full URL for each endpoint. + paths: Optional list of specific API paths to create Gradio endpoints from. Supports regex patterns, e.g. ["/api/v1/books", ".*user.*"]. If None, all paths in the OpenAPI spec will be included. + exclude_paths: Optional list of API paths to exclude from the Gradio endpoints. Supports regex patterns and takes precedence over `paths`. For example, [".*internal.*"] will exclude all paths containing "internal". + methods: Optional list of HTTP methods to include in the Gradio endpoints. If None, all methods will be included. + auth_token: Optional authentication token to be sent as a Bearer token in the Authorization header for all API requests. + interface_kwargs: Additional keyword arguments to pass to each generated gr.Interface instance (e.g., title, description, article, examples_per_page, etc.) + Returns: + A Gradio Blocks app with endpoints generated from the OpenAPI spec + """ + if isinstance(openapi_spec, dict): + spec = openapi_spec + elif isinstance(openapi_spec, str): + if is_http_url_like(openapi_spec): + response = httpx.get(openapi_spec) + response.raise_for_status() + content = response.text + else: + with open(openapi_spec) as f: + content = f.read() + + try: + spec = json.loads(content) + except json.JSONDecodeError as e: + raise ValueError("openapi_spec must be a JSON string or dictionary") from e + else: + raise ValueError("openapi_spec must be a string (URL/file path) or dictionary") + + api_paths = spec.get("paths", {}) + + if paths is not None or exclude_paths is not None: + filtered_paths = {} + for path in api_paths: + if exclude_paths: + excluded = False + for exclude_pattern in exclude_paths: + if re.match(exclude_pattern, path): + excluded = True + break + if excluded: + continue + + if paths is not None: + for pattern in paths: + if re.match(pattern, path): + filtered_paths[path] = api_paths[path] + break + else: + filtered_paths[path] = api_paths[path] + + api_paths = filtered_paths + + if not api_paths: + raise ValueError("No valid paths found in the OpenAPI specification") + else: + print(f"* Loaded {len(api_paths)} paths from the OpenAPI specification") + + valid_api_paths = [] + for path, path_item in api_paths.items(): + for method, operation in path_item.items(): + if methods and method.lower() not in [m.lower() for m in methods]: + continue + valid_api_paths.append((path, method, operation)) + + with gr.Blocks( + title=spec.get("info", {}).get("title", "OpenAPI Interface") + ) as demo: + with gr.Sidebar(): + gr.Markdown(f"## {spec.get('info', {}).get('title', 'OpenAPI Interface')}") + gr.Markdown(spec.get("info", {}).get("description", "")) + gr.Markdown("### API Endpoints") + api_path_str = "
" + gr.Markdown(api_path_str) + + for path, method, operation in valid_api_paths: + components_list = [] + + for param in operation.get("parameters", []): + component = external_utils.component_from_parameter_schema(param) + components_list.append(component) + + request_body = operation.get("requestBody") + if request_body: + body_component = external_utils.component_from_request_body_schema( + request_body, spec + ) + if body_component: + components_list.append(body_component) + + endpoint_fn = external_utils.create_endpoint_fn( + path, method, operation, base_url, auth_token + ) + endpoint_fn.__name__ = ( + f"{method}_{path.replace('/', '_').replace('{', '').replace('}', '')}" + ) + + gr.Markdown( + f"

" + f"{external_utils.method_box(method)} {path}

" + ) + if operation.get("summary"): + gr.Markdown(operation["summary"]) + + inputs = components_list if components_list else [] + output = gr.JSON(label="Response") + + gr.Interface( + fn=endpoint_fn, + inputs=inputs, + outputs=output, + **interface_kwargs, + ) + + return demo diff --git a/gradio/external_utils.py b/gradio/external_utils.py new file mode 100644 index 0000000..15bcb3e --- /dev/null +++ b/gradio/external_utils.py @@ -0,0 +1,616 @@ +"""Utility function for gradio/external.py, designed for internal use.""" + +from __future__ import annotations + +import base64 +import inspect +import json +import math +import re +import warnings + +import httpx +import yaml +from gradio_client.utils import encode_url_or_file_to_base64 +from huggingface_hub import HfApi, ImageClassificationOutputElement, InferenceClient +from huggingface_hub.errors import HFValidationError + +from gradio import components +from gradio.exceptions import Error, TooManyRequestsError + + +def get_model_info(model_name, token=None): + hf_api = HfApi(token=token) + print(f"Fetching model from: https://huggingface.co/{model_name}") + try: + model_info = hf_api.model_info(model_name) + except HFValidationError as e: + if ":fastest" in model_name or ":cheapest" in model_name: + raise ValueError( + "To use :cheapest or :fastest, upgrade huggingface_hub to huggingface_hub>=1.0" + ) from e + raise + pipeline = model_info.pipeline_tag + tags = model_info.tags + return pipeline, tags + + +################## +# Helper functions for processing tabular data +################## + + +def get_tabular_examples(model_name: str) -> dict[str, list[float]]: + readme = httpx.get(f"https://huggingface.co/{model_name}/resolve/main/README.md") + if readme.status_code != 200: + warnings.warn(f"Cannot load examples from README for {model_name}", UserWarning) + example_data = {} + else: + yaml_regex = re.search( + "(?:^|[\r\n])---[\n\r]+([\\S\\s]*?)[\n\r]+---([\n\r]|$)", readme.text + ) + if yaml_regex is None: + example_data = {} + else: + example_yaml = next( + yaml.safe_load_all(readme.text[: yaml_regex.span()[-1]]) + ) + example_data = example_yaml.get("widget", {}).get("structuredData", {}) + if not example_data: + raise ValueError( + f"No example data found in README.md of {model_name} - Cannot build gradio demo. " + "See the README.md here: https://huggingface.co/scikit-learn/tabular-playground/blob/main/README.md " + "for a reference on how to provide example data to your model." + ) + # replace nan with string NaN for inference Endpoints + for data in example_data.values(): + for i, val in enumerate(data): + if isinstance(val, float) and math.isnan(val): + data[i] = "NaN" + return example_data + + +def cols_to_rows( + example_data: dict[str, list[float | str] | None], +) -> tuple[list[str], list[list[float]]]: + headers = list(example_data.keys()) + n_rows = max(len(example_data[header] or []) for header in headers) + data = [] + for row_index in range(n_rows): + row_data = [] + for header in headers: + col = example_data[header] or [] + if row_index >= len(col): + row_data.append("NaN") + else: + row_data.append(col[row_index]) + data.append(row_data) + return headers, data + + +def rows_to_cols(incoming_data: dict) -> dict[str, dict[str, dict[str, list[str]]]]: + data_column_wise = {} + for i, header in enumerate(incoming_data["headers"]): + data_column_wise[header] = [str(row[i]) for row in incoming_data["data"]] + return {"inputs": {"data": data_column_wise}} + + +################## +# Helper functions for processing other kinds of data +################## + + +def postprocess_label(scores: list[ImageClassificationOutputElement]) -> dict: + return {c.label: c.score for c in scores} + + +def postprocess_mask_tokens(scores: list[dict[str, str | float]]) -> dict: + return {c["token_str"]: c["score"] for c in scores} + + +def postprocess_question_answering(answer: dict) -> tuple[str, dict]: + return answer["answer"], {answer["answer"]: answer["score"]} + + +def postprocess_visual_question_answering(scores: list[dict[str, str | float]]) -> dict: + return {c["answer"]: c["score"] for c in scores} + + +def zero_shot_classification_wrapper(client: InferenceClient): + def zero_shot_classification_inner(input: str, labels: str, multi_label: bool): + return client.zero_shot_classification( + input, labels.split(","), multi_label=multi_label + ) + + return zero_shot_classification_inner + + +def sentence_similarity_wrapper(client: InferenceClient): + def sentence_similarity_inner(input: str, sentences: str): + return client.sentence_similarity(input, sentences.split("\n")) + + return sentence_similarity_inner + + +def text_generation_wrapper(client: InferenceClient): + def text_generation_inner(input: str): + return input + client.text_generation(input) + + return text_generation_inner + + +def conversational_wrapper(client: InferenceClient): + def chat_fn(message, history): + if not history: + history = [] + history.append({"role": "user", "content": message}) + try: + out = "" + for chunk in client.chat_completion(messages=history, stream=True): + out += chunk.choices[0].delta.content or "" if chunk.choices else "" + yield out + except Exception as e: + handle_hf_error(e) + + return chat_fn + + +def encode_to_base64(r: httpx.Response) -> str: + # Handles the different ways HF API returns the prediction + base64_repr = base64.b64encode(r.content).decode("utf-8") + data_prefix = ";base64," + # Case 1: base64 representation already includes data prefix + if data_prefix in base64_repr: + return base64_repr + else: + content_type = r.headers.get("content-type") + # Case 2: the data prefix is a key in the response + if content_type == "application/json": + try: + data = r.json()[0] + content_type = data["content-type"] + base64_repr = data["blob"] + except KeyError as ke: + raise ValueError( + "Cannot determine content type returned by external API." + ) from ke + # Case 3: the data prefix is included in the response headers + else: + pass + new_base64 = f"data:{content_type};base64,{base64_repr}" + return new_base64 + + +def format_ner_list(input_string: str, ner_groups: list[dict[str, str | int]]): + if len(ner_groups) == 0: + return [(input_string, None)] + + output = [] + end = 0 + prev_end = 0 + + for group in ner_groups: + entity, start, end = group["entity_group"], group["start"], group["end"] + output.append((input_string[prev_end:start], None)) + output.append((input_string[start:end], entity)) + prev_end = end + + output.append((input_string[end:], None)) + return output + + +def token_classification_wrapper(client: InferenceClient): + def token_classification_inner(input: str): + ner_list = client.token_classification(input) + return format_ner_list(input, ner_list) # type: ignore + + return token_classification_inner + + +def object_detection_wrapper(client: InferenceClient): + def object_detection_inner(input: str): + annotations = client.object_detection(input) + formatted_annotations = [ + ( + ( + a["box"]["xmin"], + a["box"]["ymin"], + a["box"]["xmax"], + a["box"]["ymax"], + ), + a["label"], + ) + for a in annotations + ] + return (input, formatted_annotations) + + return object_detection_inner + + +def image_text_to_text_wrapper(client: InferenceClient): + def chat_fn(image, text): + messages = [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": encode_url_or_file_to_base64(image)}, + }, + {"type": "text", "text": text}, + ], + } + ] + + try: + response = client.chat_completion(messages=messages, stream=False) + return response.choices[0].message.content + except Exception as e: + # Fallback to image_to_text for models that don't support chat_completion + try: + # Try image_to_text (standard image captioning) + result = client.image_to_text(image) + return f"Image description: {result}\n\nUser question: {text}\n\nNote: This model doesn't support question-answering about images, only image captioning." + except Exception: + handle_hf_error(e) + + return chat_fn + + +def chatbot_preprocess(text, state): + if not state: + return text, [], [] + return ( + text, + state["conversation"]["generated_responses"], + state["conversation"]["past_user_inputs"], + ) + + +def chatbot_postprocess(response): + chatbot_history = list( + zip( + response["conversation"]["past_user_inputs"], + response["conversation"]["generated_responses"], + strict=False, + ) + ) + return chatbot_history, response + + +def tabular_wrapper(client: InferenceClient, pipeline: str): + # This wrapper is needed to handle an issue in the InfereneClient where the model name is not + # automatically loaded when using the tabular_classification and tabular_regression methods. + # See: https://github.com/huggingface/huggingface_hub/issues/2015 + def tabular_inner(data): + if pipeline not in ("tabular_classification", "tabular_regression"): + raise TypeError(f"pipeline type {pipeline!r} not supported") + assert client.model # noqa: S101 + if pipeline == "tabular_classification": + return client.tabular_classification(data, model=client.model) + else: + return client.tabular_regression(data, model=client.model) + + return tabular_inner + + +################## +# Helper function for cleaning up an Interface loaded from HF Spaces +################## + + +def streamline_spaces_interface(config: dict) -> dict: + """Streamlines the interface config dictionary to remove unnecessary keys.""" + config["inputs"] = [ + components.get_component_instance(component) + for component in config["input_components"] + ] + config["outputs"] = [ + components.get_component_instance(component) + for component in config["output_components"] + ] + parameters = { + "article", + "description", + "flagging_options", + "inputs", + "outputs", + "title", + } + config = {k: config[k] for k in parameters} + return config + + +def handle_hf_error(e: Exception): + if "429" in str(e): + raise TooManyRequestsError() from e + elif "401" in str(e) or "You must provide an api_key" in str(e): + raise Error("Unauthorized, please make sure you are signed in.") from e + elif isinstance(e, StopIteration): + raise Error( + "This model is not supported by any Hugging Face Inference Provider. " + "Please check the supported models at https://huggingface.co/docs/inference-providers." + ) from e + else: + raise Error(str(e) or f"An error occurred: {type(e).__name__}") from e + + +def create_endpoint_fn( + endpoint_path: str, + endpoint_method: str, + endpoint_operation: dict, + base_url: str, + auth_token: str | None = None, +): + # Get request body info for docstring generation + request_body = endpoint_operation.get("requestBody", {}) + + def endpoint_fn(*args): + url = f"{base_url.rstrip('/')}{endpoint_path}" + + headers = {"Content-Type": "application/json"} + if auth_token: + headers["Authorization"] = f"Bearer {auth_token}" + + params = {} + body_data = {} + + operation_params = endpoint_operation.get("parameters", []) + request_body = endpoint_operation.get("requestBody", {}) + + param_index = 0 + for param in operation_params: + if param_index < len(args): + if param.get("in") == "query": + params[param["name"]] = args[param_index] + elif param.get("in") == "path": + url = url.replace(f"{{{param['name']}}}", str(args[param_index])) + param_index += 1 + + is_file_upload = False + if request_body and param_index < len(args): + content = request_body.get("content", {}) + for content_type in content: + if content_type in ["application/octet-stream", "multipart/form-data"]: + is_file_upload = True + break + + if request_body and param_index < len(args): + if is_file_upload: + file_data = args[param_index] + if file_data: + headers["Content-Type"] = "application/octet-stream" + body_data = file_data + else: + body_data = b"" + else: + body_data = json.loads(args[param_index]) + try: + if endpoint_method.lower() == "get": + response = httpx.get(url, params=params, headers=headers) + elif endpoint_method.lower() == "post": + response = httpx.post( + url, + params=params, + content=body_data if is_file_upload else None, + json=body_data if not is_file_upload else None, + headers=headers, + ) + elif endpoint_method.lower() == "put": + response = httpx.put( + url, + params=params, + content=body_data if is_file_upload else None, + json=body_data if not is_file_upload else None, + headers=headers, + ) + elif endpoint_method.lower() == "patch": + response = httpx.patch( + url, + params=params, + content=body_data if is_file_upload else None, + json=body_data if not is_file_upload else None, + headers=headers, + ) + elif endpoint_method.lower() == "delete": + response = httpx.delete(url, params=params, headers=headers) + else: + raise ValueError(f"Unsupported HTTP method: {endpoint_method}") + + if response.status_code in [200, 201, 202, 204]: + return response.json() + else: + return { + "__status__": "error", + "status_code": response.status_code, + "message": response.text, + } + except Exception as e: + return f"Error: {str(e)}" + + summary = endpoint_operation.get("summary", "") + description = endpoint_operation.get("description", "") + + param_docs = [] + param_names = [] + + for param in endpoint_operation.get("parameters", []): + param_name = param.get("name", "") + param_desc = param.get("description", "") + param_schema = param.get("schema", {}) + param_enum = param_schema.get("enum", []) + if param_enum: + param_desc += f" (Choices: {', '.join(param_enum)})" + param_names.append(param_name) + param_docs.append(f" {param_name}: {param_desc}") + + if request_body: + body_desc = request_body.get("description", "URL of file") + param_docs.append(f" request_body: {body_desc}") + param_names.append("request_body") + + docstring_parts = [] + if description or summary: + docstring_parts.append(description or summary) + if param_docs: + docstring_parts.append("Parameters:") + docstring_parts.extend(param_docs) + + endpoint_fn.__doc__ = "\n".join(docstring_parts) + + if param_names: + sig_params = [] + for name in param_names: + sig_params.append( + inspect.Parameter( + name=name, kind=inspect.Parameter.POSITIONAL_OR_KEYWORD + ) + ) + sig_params.append( + inspect.Parameter(name="args", kind=inspect.Parameter.VAR_POSITIONAL) + ) + + new_sig = inspect.Signature(parameters=sig_params) + + endpoint_fn.__signature__ = new_sig # type: ignore + + return endpoint_fn + + +def component_from_parameter_schema(param_info: dict) -> components.Component: + import gradio as gr + + param_name = param_info.get("name") + param_description = param_info.get("description") + + param_schema = param_info.get("schema", {}) + param_type = param_schema.get("type") + enum_values = param_schema.get("enum") + default_value = param_schema.get("default") + + if enum_values is not None: + component = gr.Dropdown( + choices=enum_values, + label=param_name, + value=default_value, + allow_custom_value=False, + info=param_description, + ) + elif param_type in ("number", "integer"): + component = gr.Number( + label=param_name, + value=default_value, + info=param_description, + ) + elif param_type == "boolean": + component = gr.Checkbox( + label=param_name, + value=default_value, + info=param_description, + ) + elif param_type == "array": + component = gr.Textbox( + label=f"{param_name} (JSON array)", + value="[]", + info=param_description, + ) + else: + component = gr.Textbox( + label=param_name, + value=default_value, + info=param_description, + ) + + return component + + +def resolve_schema_ref(schema: dict, spec: dict) -> dict: + """Resolve schema references in OpenAPI spec.""" + if "$ref" in schema: + ref_path = schema["$ref"] + if ref_path.startswith("#/components/schemas/"): + schema_name = ref_path.split("/")[-1] + return spec.get("components", {}).get("schemas", {}).get(schema_name, {}) + elif ref_path.startswith("#/"): + path_parts = ref_path.split("/")[1:] + current = spec + for part in path_parts: + current = current.get(part, {}) + return current + return schema + + +def component_from_request_body_schema( + request_body: dict, spec: dict +) -> components.Component | None: + """Create a Gradio component from an OpenAPI request body schema.""" + import gradio as gr + + if not request_body: + return None + + content = request_body.get("content", {}) + description = request_body.get("description", "Request Body") + + for content_type, content_schema in content.items(): + if content_type in ["application/octet-stream", "multipart/form-data"]: + schema = resolve_schema_ref(content_schema.get("schema", {}), spec) + if schema.get("type") == "string" and schema.get("format") == "binary": + return gr.File(label="File") + + json_content = content.get("application/json", {}) + if not json_content: + for content_type, content_schema in content.items(): + if content_type.startswith("application/"): + json_content = content_schema + break + + if not json_content: + return None + + schema = resolve_schema_ref(json_content.get("schema", {}), spec) + + default_value = schema.get("example", {}) + if not default_value and schema.get("type") == "object": + properties = schema.get("properties", {}) + default_value = {} + for prop_name, prop_schema in properties.items(): + prop_schema = resolve_schema_ref(prop_schema, spec) + prop_type = prop_schema.get("type") + if prop_type == "string": + default_value[prop_name] = prop_schema.get("example", "") + elif prop_type in ("number", "integer"): + default_value[prop_name] = prop_schema.get("example", 0) + elif prop_type == "boolean": + default_value[prop_name] = prop_schema.get("example", False) + elif prop_type == "array": + default_value[prop_name] = prop_schema.get("example", []) + elif prop_type == "object": + default_value[prop_name] = prop_schema.get("example", {}) + + component = gr.Textbox( + label="Request Body", + value=json.dumps(default_value, indent=2), + info=description, + ) + + return component + + +def method_box(method: str) -> str: + color_map = { + "GET": "#61affe", + "POST": "#49cc90", + "PUT": "#fca130", + "DELETE": "#f93e3e", + "PATCH": "#50e3c2", + } + color = color_map.get(method.upper(), "#999") + return ( + f"{method.upper()}" + ) diff --git a/gradio/flagging.py b/gradio/flagging.py new file mode 100644 index 0000000..07c74c0 --- /dev/null +++ b/gradio/flagging.py @@ -0,0 +1,426 @@ +from __future__ import annotations + +import csv +import datetime +import json +import os +import re +import time +from abc import ABC, abstractmethod +from collections.abc import Sequence +from multiprocessing import Lock +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from gradio_client import utils as client_utils +from gradio_client.documentation import document + +import gradio as gr +from gradio import utils +from gradio.events import LikeData + +if TYPE_CHECKING: + from gradio.components import Component + + +class FlaggingCallback(ABC): + """ + An abstract class for defining the methods that any FlaggingCallback should have. + """ + + @abstractmethod + def setup(self, components: Sequence[Component], flagging_dir: str): + """ + This method should be overridden and ensure that everything is set up correctly for flag(). + This method gets called once at the beginning of the Interface.launch() method. + Parameters: + components: Set of components that will provide flagged data. + flagging_dir: A string, typically containing the path to the directory where the flagging file should be stored (provided as an argument to Interface.__init__()). + """ + pass + + @abstractmethod + def flag( + self, + flag_data: list[Any], + flag_option: str | None = None, + username: str | None = None, + ) -> int: + """ + This method should be overridden by the FlaggingCallback subclass and may contain optional additional arguments. + This gets called every time the button is pressed. + Parameters: + interface: The Interface object that is being used to launch the flagging interface. + flag_data: The data to be flagged. + flag_option (optional): In the case that flagging_options are provided, the flag option that is being used. + username (optional): The username of the user that is flagging the data, if logged in. + Returns: + (int) The total number of samples that have been flagged. + """ + pass + + +@document() +class SimpleCSVLogger(FlaggingCallback): + """ + A simplified implementation of the FlaggingCallback abstract class + provided for illustrative purposes. Each flagged sample (both the input and output data) + is logged to a CSV file on the machine running the gradio app. + Example: + import gradio as gr + def image_classifier(inp): + return {'cat': 0.3, 'dog': 0.7} + demo = gr.Interface(fn=image_classifier, inputs="image", outputs="label", + flagging_callback=SimpleCSVLogger()) + """ + + def __init__(self): + pass + + def setup(self, components: Sequence[Component], flagging_dir: str | Path): + self.components = components + self.flagging_dir = flagging_dir + os.makedirs(flagging_dir, exist_ok=True) + + def flag( + self, + flag_data: list[Any], + flag_option: str | None = None, # noqa: ARG002 + username: str | None = None, # noqa: ARG002 + ) -> int: + flagging_dir = self.flagging_dir + log_filepath = Path(flagging_dir) / "log.csv" + + csv_data = [] + for component, sample in zip(self.components, flag_data, strict=False): + save_dir = Path( + flagging_dir + ) / client_utils.strip_invalid_filename_characters( + str(component.label) if component.label is not None else "" + ) + save_dir.mkdir(exist_ok=True) + csv_data.append( + component.flag( + sample, + save_dir, + ) + ) + + with open(log_filepath, "a", encoding="utf-8", newline="") as csvfile: + writer = csv.writer(csvfile) + writer.writerow(utils.sanitize_list_for_csv(csv_data)) + + with open(log_filepath, encoding="utf-8") as csvfile: + line_count = len(list(csv.reader(csvfile))) - 1 + return line_count + + +@document() +class ClassicCSVLogger(FlaggingCallback): + """ + The classic implementation of the FlaggingCallback abstract class in gradio<5.0. Each flagged + sample (both the input and output data) is logged to a CSV file with headers on the machine running the gradio app. + Example: + import gradio as gr + def image_classifier(inp): + return {'cat': 0.3, 'dog': 0.7} + demo = gr.Interface(fn=image_classifier, inputs="image", outputs="label", + flagging_callback=ClassicCSVLogger()) + Guides: using-flagging + """ + + def __init__(self, simplify_file_data: bool = True): + self.simplify_file_data = simplify_file_data + + def setup( + self, + components: Sequence[Component], + flagging_dir: str | Path, + ): + self.components = components + self.flagging_dir = flagging_dir + os.makedirs(flagging_dir, exist_ok=True) + + def flag( + self, + flag_data: list[Any], + flag_option: str | None = None, + username: str | None = None, + ) -> int: + flagging_dir = self.flagging_dir + log_filepath = Path(flagging_dir) / "log.csv" + is_new = not Path(log_filepath).exists() + headers = [ + getattr(component, "label", None) or f"component {idx}" + for idx, component in enumerate(self.components) + ] + [ + "flag", + "username", + "timestamp", + ] + + csv_data = [] + for idx, (component, sample) in enumerate( + zip(self.components, flag_data, strict=False) + ): + save_dir = Path( + flagging_dir + ) / client_utils.strip_invalid_filename_characters( + str(getattr(component, "label", None) or f"component {idx}") + ) + if utils.is_prop_update(sample): + csv_data.append(str(sample)) + else: + data = ( + component.flag(sample, flag_dir=save_dir) + if sample is not None + else "" + ) + if self.simplify_file_data: + data = utils.simplify_file_data_in_str(data) + csv_data.append(data) + csv_data.append(flag_option) + csv_data.append(username if username is not None else "") + csv_data.append(str(datetime.datetime.now())) + + with open(log_filepath, "a", newline="", encoding="utf-8") as csvfile: + writer = csv.writer(csvfile) + if is_new: + writer.writerow(utils.sanitize_list_for_csv(headers)) + writer.writerow(utils.sanitize_list_for_csv(csv_data)) + + with open(log_filepath, encoding="utf-8") as csvfile: + line_count = len(list(csv.reader(csvfile))) - 1 + return line_count + + +@document() +class CSVLogger(FlaggingCallback): + """ + The default implementation of the FlaggingCallback abstract class in gradio>=5.0. Each flagged + sample (both the input and output data) is logged to a CSV file with headers on the machine running + the gradio app. Unlike ClassicCSVLogger, this implementation is concurrent-safe and it creates a new + dataset file every time the headers of the CSV (derived from the labels of the components) change. It also + only creates columns for "username" and "flag" if the flag_option and username are provided, respectively. + + Example: + import gradio as gr + def image_classifier(inp): + return {'cat': 0.3, 'dog': 0.7} + demo = gr.Interface(fn=image_classifier, inputs="image", outputs="label", + flagging_callback=CSVLogger()) + Guides: using-flagging + """ + + def __init__( + self, + simplify_file_data: bool = True, + verbose: bool = True, + dataset_file_name: str | None = None, + ): + """ + Parameters: + simplify_file_data: If True, the file data will be simplified before being written to the CSV file. If CSVLogger is being used to cache examples, this is set to False to preserve the original FileData class + verbose: If True, prints messages to the console about the dataset file creation + dataset_file_name: The name of the dataset file to be created (should end in ".csv"). If None, the dataset file will be named "dataset1.csv" or the next available number. + """ + self.simplify_file_data = simplify_file_data + self.verbose = verbose + self.dataset_file_name = dataset_file_name + self.lock = Lock() + + def setup( + self, + components: Sequence[Component], + flagging_dir: str | Path, + ): + self.components = components + self.flagging_dir = Path(flagging_dir) + self.first_time = True + + def _create_dataset_file(self, additional_headers: list[str] | None = None): + os.makedirs(self.flagging_dir, exist_ok=True) + + if additional_headers is None: + additional_headers = [] + headers = ( + [ + getattr(component, "label", None) or f"component {idx}" + for idx, component in enumerate(self.components) + ] + + additional_headers + + [ + "timestamp", + ] + ) + headers = utils.sanitize_list_for_csv(headers) + dataset_files = list(Path(self.flagging_dir).glob("dataset*.csv")) + + if self.dataset_file_name: + self.dataset_filepath = self.flagging_dir / self.dataset_file_name + elif dataset_files: + try: + latest_file = max( + dataset_files, key=lambda f: int(re.findall(r"\d+", f.stem)[0]) + ) + latest_num = int(re.findall(r"\d+", latest_file.stem)[0]) + + with open(latest_file, newline="", encoding="utf-8") as csvfile: + reader = csv.reader(csvfile) + existing_headers = next(reader, None) + + if existing_headers != headers: + new_num = latest_num + 1 + self.dataset_filepath = self.flagging_dir / f"dataset{new_num}.csv" + else: + self.dataset_filepath = latest_file + except Exception: + self.dataset_filepath = self.flagging_dir / "dataset1.csv" + else: + self.dataset_filepath = self.flagging_dir / "dataset1.csv" + + if not Path(self.dataset_filepath).exists(): + with open( + self.dataset_filepath, "w", newline="", encoding="utf-8" + ) as csvfile: + writer = csv.writer(csvfile) + writer.writerow(utils.sanitize_list_for_csv(headers)) + if self.verbose: + print("Created dataset file at:", self.dataset_filepath) + elif self.verbose: + print("Using existing dataset file at:", self.dataset_filepath) + + def flag( + self, + flag_data: list[Any], + flag_option: str | None = None, + username: str | None = None, + ) -> int: + if self.first_time: + additional_headers = [] + if flag_option is not None: + additional_headers.append("flag") + if username is not None: + additional_headers.append("username") + self._create_dataset_file(additional_headers=additional_headers) + self.first_time = False + + csv_data = [] + for idx, (component, sample) in enumerate( + zip(self.components, flag_data, strict=False) + ): + save_dir = ( + self.flagging_dir + / client_utils.strip_invalid_filename_characters( + str(getattr(component, "label", None) or f"component {idx}") + ) + ) + if utils.is_prop_update(sample): + csv_data.append(str(sample)) + else: + data = ( + component.flag(sample, flag_dir=save_dir) + if sample is not None + else "" + ) + if self.simplify_file_data: + data = utils.simplify_file_data_in_str(data) + csv_data.append(data) + + if flag_option is not None: + csv_data.append(flag_option) + if username is not None: + csv_data.append(username) + csv_data.append(str(datetime.datetime.now())) + + with self.lock: + with open( + self.dataset_filepath, "a", newline="", encoding="utf-8" + ) as csvfile: + writer = csv.writer(csvfile) + writer.writerow(utils.sanitize_list_for_csv(csv_data)) + with open(self.dataset_filepath, encoding="utf-8") as csvfile: + line_count = len(list(csv.reader(csvfile))) - 1 + + return line_count + + +class ChatCSVLogger: + """ + Flagging callback for chat conversations. + Flagged conversations and like/dislike reactions are logged to a CSV file on the machine running the gradio app. + """ + + def __init__(self): + pass + + def setup(self, flagging_dir: str): + self.flagging_dir = flagging_dir + os.makedirs(flagging_dir, exist_ok=True) + + def flag( + self, + like_data: LikeData, + messages: list, + ): + flagging_dir = self.flagging_dir + log_filepath = Path(flagging_dir) / "log.csv" + is_new = not Path(log_filepath).exists() + + feedback = ( + "Like" + if like_data.liked is True + else "Dislike" + if like_data.liked is False + else like_data.liked + ) + csv_data = [ + json.dumps(messages), + like_data.index, + feedback, + str(datetime.datetime.now()), + ] + + with open(log_filepath, "a", encoding="utf-8", newline="") as csvfile: + if is_new: + writer = csv.writer(csvfile) + writer.writerow(["conversation", "index", "value", "flag", "timestamp"]) + writer = csv.writer(csvfile) + writer.writerow(utils.sanitize_list_for_csv(csv_data)) + + +class FlagMethod: + """ + Helper class that contains the flagging options and calls the flagging method. Also + provides visual feedback to the user when flag is clicked. + """ + + def __init__( + self, + flagging_callback: FlaggingCallback, + label: str, + value: str | None, + visual_feedback: bool = True, + ): + self.flagging_callback = flagging_callback + self.label = label + self.value = value + self.__name__ = "Flag" + self.visual_feedback = visual_feedback + + def __call__(self, request: gr.Request, *flag_data): + try: + self.flagging_callback.flag( + list(flag_data), flag_option=self.value, username=request.username + ) + except Exception as e: + print(f"Error while flagging: {e}") + if self.visual_feedback: + return "Error!" + if not self.visual_feedback: + return + time.sleep(0.8) # to provide enough time for the user to observe button change + return self.reset() + + def reset(self): + return gr.Button(value=self.label, interactive=True) diff --git a/gradio/helpers.py b/gradio/helpers.py new file mode 100644 index 0000000..1f58850 --- /dev/null +++ b/gradio/helpers.py @@ -0,0 +1,1257 @@ +""" +Defines helper methods useful for loading and caching Interface examples. +""" + +from __future__ import annotations + +import ast +import copy +import csv +import inspect +import os +import shutil +import warnings +from collections.abc import Callable, Iterable, MutableMapping, Sequence +from functools import partial +from pathlib import Path +from types import SimpleNamespace +from typing import TYPE_CHECKING, Any, Literal, Optional, get_origin + +from gradio_client import utils as client_utils +from gradio_client.documentation import document + +from gradio import components, oauth, processing_utils, routes, utils +from gradio.caching import Cache, resolve_generator +from gradio.context import Context, LocalContext, get_blocks_context +from gradio.data_classes import GradioModel, GradioRootModel +from gradio.events import Dependency, EventData +from gradio.exceptions import Error +from gradio.flagging import CSVLogger +from gradio.i18n import I18nData +from gradio.route_utils import Header +from gradio.utils import UnhashableKeyDict + +if TYPE_CHECKING: # Only import for type checking (to avoid circular imports). + from gradio.components import Component + +LOG_FILE = "log.csv" + + +def create_examples( + examples: list[Any] | list[list[Any]] | str, + inputs: Component | Sequence[Component], + outputs: Component | Sequence[Component] | None = None, + fn: Callable | None = None, + cache_examples: bool | None = None, + cache_mode: Literal["eager", "lazy"] | None = None, + examples_per_page: int = 10, + _api_mode: bool = False, + label: str | I18nData | None = None, + elem_id: str | None = None, + run_on_click: bool = False, + preprocess: bool = True, + postprocess: bool = True, + api_visibility: Literal["public", "private", "undocumented"] = "undocumented", + api_name: str | None = "load_example", + api_description: str | None | Literal[False] = None, + batch: bool = False, + *, + example_labels: list[str] | None = None, + visible: bool | Literal["hidden"] = True, + preload: int | Literal[False] = 0, +): + """Top-level synchronous function that creates Examples. Provided for backwards compatibility, i.e. so that gr.Examples(...) can be used to create the Examples component.""" + examples_obj = Examples( + examples=examples, + inputs=inputs, + outputs=outputs, + fn=fn, + cache_examples=cache_examples, + cache_mode=cache_mode, + examples_per_page=examples_per_page, + _api_mode=_api_mode, + label=label, + elem_id=elem_id, + run_on_click=run_on_click, + preprocess=preprocess, + postprocess=postprocess, + api_visibility=api_visibility, + api_name=api_name, + api_description=api_description, + batch=batch, + example_labels=example_labels, + visible=visible, + _initiated_directly=False, + preload=preload, + ) + examples_obj.create() + return examples_obj + + +@document() +class Examples: + """ + This class is a wrapper over the Dataset component and can be used to create Examples + for Blocks / Interfaces. Populates the Dataset component with examples and + assigns event listener so that clicking on an example populates the input/output + components. Optionally handles example caching for fast inference. + + Demos: calculator_blocks + Guides: more-on-examples-and-flagging + """ + + def __init__( + self, + examples: list[Any] | list[list[Any]] | str, + inputs: Component | Sequence[Component], + outputs: Component | Sequence[Component] | None = None, + fn: Callable | None = None, + cache_examples: bool | None = None, + cache_mode: Literal["eager", "lazy"] | None = None, + examples_per_page: int = 10, + _api_mode: bool = False, + label: str | I18nData | None = "Examples", + elem_id: str | None = None, + run_on_click: bool = False, + preprocess: bool = True, + postprocess: bool = True, + api_visibility: Literal["public", "private", "undocumented"] = "undocumented", + api_name: str | None = "load_example", + api_description: str | None | Literal[False] = None, + batch: bool = False, + *, + example_labels: list[str] | None = None, + visible: bool | Literal["hidden"] = True, + preload: int | Literal[False] = 0, + _initiated_directly: bool = True, + ): + """ + Parameters: + examples: example inputs that can be clicked to populate specific components. Should be nested list, in which the outer list consists of samples and each inner list consists of an input corresponding to each input component. A string path to a directory of examples can also be provided but it should be within the directory with the python file running the gradio app. If there are multiple input components and a directory is provided, a log.csv file must be present in the directory to link corresponding inputs. + inputs: the component or list of components corresponding to the examples + outputs: optionally, provide the component or list of components corresponding to the output of the examples. Required if `cache_examples` is not False. + fn: optionally, provide the function to run to generate the outputs corresponding to the examples. Required if `cache_examples` is not False. Also required if `run_on_click` is True. + cache_examples: If True, caches examples in the server for fast runtime in examples. If "lazy", then examples are cached (for all users of the app) after their first use (by any user of the app). If None, will use the GRADIO_CACHE_EXAMPLES environment variable, which should be either "true" or "false". In HuggingFace Spaces, this parameter is True (as long as `fn` and `outputs` are also provided). The default option otherwise is False. Note that examples are cached separately from Gradio's queue() so certain features, such as gr.Progress(), gr.Info(), gr.Warning(), etc. will not be displayed in Gradio's UI for cached examples. + cache_mode: if "lazy", examples are cached after their first use. If "eager", all examples are cached at app launch. If None, will use the GRADIO_CACHE_MODE environment variable if defined, or default to "eager". + examples_per_page: how many examples to show per page. + label: the label to use for the examples component (by default, "Examples") + elem_id: an optional string that is assigned as the id of this component in the HTML DOM. + run_on_click: if cache_examples is False, clicking on an example does not run the function when an example is clicked. Set this to True to run the function when an example is clicked. Has no effect if cache_examples is True. + preprocess: if True, preprocesses the example input before running the prediction function and caching the output. Only applies if `cache_examples` is not False. + postprocess: if True, postprocesses the example output after running the prediction function and before caching. Only applies if `cache_examples` is not False. + api_visibility: Controls the visibility of the event associated with clicking on the examples. Can be "public" (shown in API docs and callable), "private" (hidden from API docs and not callable by the Gradio client libraries), or "undocumented" (hidden from API docs but callable). + api_name: Defines how the event associated with clicking on the examples appears in the API docs. Can be a string or None. If set to a string, the endpoint will be exposed in the API docs with the given name. If None, an auto-generated name will be used. + api_description: Description of the event associated with clicking on the examples in the API docs. Can be a string, None, or False. If set to a string, the endpoint will be exposed in the API docs with the given description. If None, the function's docstring will be used as the API endpoint description. If False, then no description will be displayed in the API docs. + batch: If True, then the function should process a batch of inputs, meaning that it should accept a list of input values for each parameter. Used only if cache_examples is not False. + example_labels: A list of labels for each example. If provided, the length of this list should be the same as the number of examples, and these labels will be used in the UI instead of rendering the example values. + visible: If False, the examples component will be hidden in the UI. + preload: If an integer is provided (and examples are being cached eagerly and none of the input components have a developer-provided `value`), the example at that index in the examples list will be preloaded when the Gradio app is first loaded. If False, no example will be preloaded. + """ + if _initiated_directly: + warnings.warn( + "Please use gr.Examples(...) instead of gr.helpers.Examples(...) to create the Examples.", + ) + + self.cache_examples = False + if cache_examples is None: + if ( + os.getenv("GRADIO_CACHE_EXAMPLES", "").lower() in ["true", "lazy"] + and fn is not None + and outputs is not None + ): + self.cache_examples = True + elif cache_examples in [True, False]: + self.cache_examples = cache_examples + else: + raise ValueError( + f"The `cache_examples` parameter should be either True or False, not {cache_examples}" + ) + if self.cache_examples and (fn is None or outputs is None): + raise ValueError("If caching examples, `fn` and `outputs` must be provided") + + if (cache_mode_env := os.getenv("GRADIO_CACHE_MODE")) and cache_mode is None: + if cache_mode_env.lower() == "eager": + cache_mode = "eager" + elif cache_mode_env.lower() == "lazy": + cache_mode = "lazy" + else: + cache_mode = "eager" + warnings.warn( + "The `GRADIO_CACHE_MODE` environment variable must be either 'eager' or 'lazy'. " + "Defaulting to 'eager'." + ) + + if self.cache_examples and cache_mode == "lazy": + self.cache_examples = "lazy" + + if not isinstance(inputs, Sequence): + inputs = [inputs] + if outputs and not isinstance(outputs, Sequence): + outputs = [outputs] + + working_directory = Path().absolute() + + if examples is None: + raise ValueError("The parameter `examples` cannot be None") + elif isinstance(examples, list) and ( + len(examples) == 0 or isinstance(examples[0], list) + ): + pass + elif ( + isinstance(examples, list) and len(inputs) == 1 + ): # If there is only one input component, examples can be provided as a regular list instead of a list of lists + examples = [[e] for e in examples] + elif isinstance(examples, str): + if not Path(examples).exists(): + raise FileNotFoundError( + f"Could not find examples directory: {examples}" + ) + working_directory = examples + if not (Path(examples) / LOG_FILE).exists(): + if len(inputs) == 1: + examples = [[e] for e in os.listdir(examples)] + else: + raise FileNotFoundError( + "Could not find log file (required for multiple inputs): " + + LOG_FILE + ) + else: + with open(Path(examples) / LOG_FILE) as logs: + examples = list(csv.reader(logs)) + examples = [ + examples[i][: len(inputs)] for i in range(1, len(examples)) + ] # remove header and unnecessary columns + + else: + raise ValueError( + "The parameter `examples` must either be a string directory or a list" + "(if there is only 1 input component) or (more generally), a nested " + "list, where each sublist represents a set of inputs." + ) + + input_has_examples = [False] * len(inputs) + for example in examples: + for idx, example_for_input in enumerate(example): + if example_for_input is not None: + try: + input_has_examples[idx] = True + except IndexError: + pass # If there are more example components than inputs, ignore. This can sometimes be intentional (e.g. loading from a log file where outputs and timestamps are also logged) + + inputs_with_examples = [ + inp for (inp, keep) in zip(inputs, input_has_examples, strict=False) if keep + ] + non_none_examples = [ + [ + ex + for (ex, keep) in zip(example, input_has_examples, strict=False) + if keep + ] + for example in examples + ] + if example_labels is not None and len(example_labels) != len(examples): + raise ValueError( + "If `example_labels` are provided, the length of `example_labels` must be the same as the number of examples." + ) + + self.examples = examples + self.non_none_examples = non_none_examples + self.inputs = inputs + self.input_has_examples = input_has_examples + self.inputs_with_examples = inputs_with_examples + self.outputs = outputs or [] + self.fn = fn + self._api_mode = _api_mode + self.preprocess = preprocess + self.postprocess = postprocess + self.api_visibility = api_visibility + self.api_name: str | None = api_name + self.api_description: str | None | Literal[False] = api_description + self.batch = batch + self.example_labels = example_labels + self.working_directory = working_directory + self.preload = preload + + from gradio import components + + with utils.set_directory(working_directory): + self.dataset = components.Dataset( + components=inputs_with_examples, + samples=copy.deepcopy(non_none_examples), + type="tuple", + label=label, + samples_per_page=examples_per_page, + elem_id=elem_id, + visible=visible, + sample_labels=example_labels, + ) + + self.cache_logger = CSVLogger( + simplify_file_data=False, verbose=False, dataset_file_name="log.csv" + ) + self.cached_folder = utils.get_cache_folder() / str(self.dataset._id) + if ( + os.environ.get("GRADIO_RESET_EXAMPLES_CACHE") == "True" + and self.cached_folder.exists() + ): + shutil.rmtree(self.cached_folder) + self.cached_file = Path(self.cached_folder) / "log.csv" + self.cached_indices_file = Path(self.cached_folder) / "indices.csv" + self.run_on_click = run_on_click + self.cache_event: Dependency | None = None + self.non_none_processed_examples = UnhashableKeyDict() + + if self.dataset.samples: + for index, example in enumerate(self.non_none_examples): + self.non_none_processed_examples[self.dataset.samples[index]] = ( + self._get_processed_example(example) + ) + + if self.cache_examples == "lazy": + print( + f"Will cache examples in '{utils.abspath(self.cached_folder)}' directory at first use.", + end="", + ) + if Path(self.cached_file).exists(): + print( + "If method or examples have changed since last caching, delete this folder to reset cache." + ) + print("\n") + + def _get_processed_example(self, example): + """ + This function is used to get the post-processed example values, ready to be used + in the frontend for each input component. For example, if the input components are + image components, the post-processed example values will be the a list of ImageData dictionaries + with the path, url, size, mime_type, orig_name, and is_stream keys. For any input components + that should be skipped (b/c they are None for all samples), they will simply be absent + from the returned list + + Parameters: + example: a list of example values for each input component, excluding those components + that have all None values + """ + if example in self.non_none_processed_examples: + return self.non_none_processed_examples[example] + with utils.set_directory(self.working_directory): + sub = [] + for component, sample in zip( + self.inputs_with_examples, example, strict=False + ): + prediction_value = component.postprocess(sample) + if isinstance(prediction_value, (GradioRootModel, GradioModel)): + prediction_value = prediction_value.model_dump() + prediction_value = processing_utils.move_files_to_cache( + prediction_value, + component, + postprocess=True, + ) + sub.append(prediction_value) + return sub + + def create(self) -> None: + """Creates the Dataset component to hold the examples""" + blocks_config = get_blocks_context() + self.root_block = Context.root_block or ( + blocks_config.root_block if blocks_config else None + ) + if blocks_config: + if self.root_block: + self.root_block.extra_startup_events.append(self._start_caching) + + if self.cache_examples: + + def load_example_input(example_tuple): + _, example_value = example_tuple + processed_example = self._get_processed_example(example_value) + return utils.resolve_singleton(processed_example) + + def load_example_output(example_tuple): + example_id, _ = example_tuple + cached_outputs = self.load_from_cache(example_id) + return utils.resolve_singleton(cached_outputs) + + self.cache_event = self.load_input_event = self.dataset.click( + load_example_input, + inputs=[self.dataset], + outputs=self.inputs, + show_progress="hidden", + postprocess=False, + queue=False, + api_visibility="undocumented", + ).then( + load_example_output, + inputs=[self.dataset], + outputs=self.outputs, + postprocess=False, + api_name=self.api_name, + api_description=self.api_description, + api_visibility=self.api_visibility, + ) + + if ( + self.preload is not False + and self.cache_examples != "lazy" + and self.root_block + and not any( + "value" in inp.constructor_args + for inp in self.inputs_with_examples + ) + ): + self.root_block.load( + load_example_input, + inputs=[ + components.State( + (self.preload, self.non_none_examples[self.preload]) + ) + ], + outputs=self.inputs, + show_progress="hidden", + postprocess=False, + queue=False, + api_visibility="undocumented", + ) + self.root_block.load( + load_example_output, + inputs=[ + components.State( + (self.preload, self.non_none_examples[self.preload]) + ) + ], + outputs=self.outputs, + postprocess=False, + show_progress="hidden", + api_visibility="undocumented", + ) + + else: + + def load_example(example_tuple): + _, example_value = example_tuple + processed_example = self._get_processed_example(example_value) + if len(self.inputs_with_examples) == 1: + return update( + value=processed_example[0], + **self.dataset.component_props[0], # type: ignore + ) + return [ + update( + value=processed_example[i], + **self.dataset.component_props[i], # type: ignore + ) + for i in range(len(self.inputs_with_examples)) + ] + + self.load_input_event = self.dataset.click( + load_example, + inputs=[self.dataset], + outputs=self.inputs_with_examples, + show_progress="hidden", + postprocess=False, + queue=False, + api_name=self.api_name, + api_description=self.api_description, + api_visibility=self.api_visibility, + ) + + if self.run_on_click: + if self.fn is None: + raise ValueError( + "Cannot run_on_click if no function is provided" + ) + self.load_input_event.then( + self.fn, + inputs=self.inputs, + outputs=self.outputs, + api_visibility="undocumented", + ) + else: + warnings.warn( + f"If an Examples object is created outside a Blocks Context, make sure to call `examples.dataset.render()`{'and `examples.create()`' if self.cache_examples else ''} to render the examples in the interface." + ) + + async def _postprocess_output(self, output) -> list: + """ + This is a way that we can postprocess the data manually, since we set postprocess=False in the lazy_cache + event handler. The reason we did that is because we don't want to postprocess data if we are loading from + the cache, since that has already been postprocessed. We postprocess this data manually if we are calling + the function using the _handle_callable_as_generator() method. + """ + import gradio as gr + + with gr.Blocks() as demo: + [output.render() for output in self.outputs] # type: ignore + demo.load(self.fn, self.inputs, self.outputs) + demo.unrender() + return await demo.postprocess_data(demo.default_config.fns[0], output, None) + + def _get_cached_index_if_cached(self, example_index) -> int | None: + if Path(self.cached_indices_file).exists(): + with open(self.cached_indices_file) as f: + cached_indices = [int(line.strip()) for line in f] + if example_index in cached_indices: + cached_index = cached_indices.index(example_index) + return cached_index + return None + + async def _start_caching(self): + if self.cache_examples: + for example in self.examples: + if len([ex for ex in example if ex is not None]) != len(self.inputs): + warnings.warn( + "Examples will be cached but not all input components have " + "example values. This may result in an exception being thrown by " + "your function. If you do get an error while caching examples, make " + "sure all of your inputs have example values for all of your examples " + "or you provide default values for those particular parameters in your function." + ) + break + if self.cache_examples is True: + await self.cache() + + async def cache(self, example_id: int | None = None) -> None: + """ + Caches examples so that their predictions can be shown immediately. + Parameters: + example_id: The id of the example to process (zero-indexed). If None, all examples are cached. + """ + if self.root_block is None: + raise Error("Cannot cache examples if not in a Blocks context.") + if Path(self.cached_file).exists() and example_id is None: + print( + f"Using cache from '{utils.abspath(self.cached_folder)}' directory. If method or examples have changed since last caching, delete this folder to clear cache.\n" + ) + else: + print(f"Caching examples at: '{utils.abspath(self.cached_folder)}'") + self.cache_logger.setup(self.outputs, self.cached_folder) # type: ignore + assert self.fn is not None # noqa: S101 + fn, generated_values = resolve_generator(self.fn) + if generated_values is None: + generated_values = [] + + # create a fake dependency to process the examples and get the predictions + from gradio.events import EventListenerMethod + + _, fn_index = self.root_block.default_config.set_event_trigger( + [EventListenerMethod(Context.root_block, "load")], + fn=fn, + inputs=self.inputs, + outputs=self.outputs, + preprocess=self.preprocess and not self._api_mode, + postprocess=self.postprocess and not self._api_mode, + batch=self.batch, + ) + + if self.outputs is None: + raise ValueError("self.outputs is missing") + for i, example in enumerate(self.non_none_examples): + if example_id is not None and i != example_id: + continue + processed_input = self._get_processed_example(example) + for index, keep in enumerate(self.input_has_examples): + if not keep: + processed_input.insert(index, None) + if self.batch: + processed_input = [[value] for value in processed_input] + with utils.MatplotlibBackendMananger(): + # When caching examples lazily, set in_event_listener to False + # so that all components are properly instantiated + # See https://github.com/gradio-app/gradio/issues/12564 + prediction = await self.root_block.process_api( + block_fn=self.root_block.default_config.fns[fn_index], + inputs=processed_input, + request=None, + in_event_listener=self.cache_examples != "lazy", + ) + output = prediction["data"] + if generated_values: + output = await merge_generated_values_into_output( + self.outputs, # type: ignore + generated_values, + output, # type: ignore + ) + if self.batch: + output = [value[0] for value in output] + self.cache_logger.flag(output) + with open(self.cached_indices_file, "a") as f: + f.write(f"{example_id or i}\n") + + # Remove the "fake_event" to prevent bugs in loading interfaces from spaces + self.root_block.default_config.fns.pop(fn_index) + + def load_from_cache(self, example_id: int) -> list[Any]: + """Loads a particular cached example for the interface. + Parameters: + example_id: The id of the example to process (zero-indexed). + """ + cached_index = self._get_cached_index_if_cached(example_id) + if cached_index is None: + client_utils.synchronize_async(self.cache, example_id) + with open(self.cached_indices_file) as f: + cached_index = len(f.readlines()) - 1 + + with open(self.cached_file, encoding="utf-8") as cache: + examples = list(csv.reader(cache)) + + if cached_index + 1 >= len(examples): + raise IndexError("Cached example not found in cache file") + example = examples[cached_index + 1] # +1 to adjust for header + + output = [] + if self.outputs is None: + raise ValueError("self.outputs is missing") + for component, value in zip(self.outputs, example, strict=False): # type: ignore + value_to_use = value + try: + value_as_dict = ast.literal_eval(value) + # File components that output multiple files get saved as a python list + # need to pass the parsed list to serialize + # TODO: Better file serialization in 4.0 + if isinstance(value_as_dict, list) and isinstance( + component, components.File + ): + value_to_use = value_as_dict + if not utils.is_prop_update(value_as_dict): + raise TypeError("value wasn't an update") # caught below + output.append(value_as_dict) + except (ValueError, TypeError, SyntaxError): + output.append(component.read_from_flag(value_to_use)) + return output + + +async def merge_generated_values_into_output( + components: Sequence[Component], generated_values: list, output: list +): + from gradio.components.base import StreamingOutput + + for output_index, output_component in enumerate(components): + if isinstance(output_component, StreamingOutput) and output_component.streaming: + binary_chunks = [] + desired_output_format = None + for i, chunk in enumerate(generated_values): + if len(components) > 1: + chunk = chunk[output_index] + processed_chunk = output_component.postprocess(chunk) + if isinstance(processed_chunk, (GradioModel, GradioRootModel)): + processed_chunk = processed_chunk.model_dump() + stream_chunk = await output_component.stream_output( + processed_chunk, "", i == 0 + ) + if i == 0 and (orig_name := stream_chunk[1].get("orig_name")): + desired_output_format = Path(orig_name).suffix[1:] + if stream_chunk[0]: + binary_chunks.append(stream_chunk[0]["data"]) + combined_output = await output_component.combine_stream( + binary_chunks, desired_output_format=desired_output_format + ) + output[output_index] = combined_output.model_dump() + + return output + + +class TrackedIterable: + def __init__( + self, + iterable: Iterable | None, + index: int | float | None, + length: int | float | None, + desc: str | None, + unit: str | None, + _tqdm=None, + progress: float | None = None, + ) -> None: + self.iterable = iterable + self.index = index + self.length = length + self.desc = desc + self.unit = unit + self._tqdm = _tqdm + self.progress = progress + + +@document("__call__", "tqdm") +class Progress(Iterable): + """ + The Progress class provides a custom progress tracker that is used in a function signature. + To attach a Progress tracker to a function, simply add a parameter right after the input parameters that has a default value set to a `gradio.Progress()` instance. + The Progress tracker can then be updated in the function by calling the Progress object or using the `tqdm` method on an Iterable. + Example: + import gradio as gr + import time + def my_function(x, progress=gr.Progress()): + progress(0, desc="Starting...") + time.sleep(1) + for i in progress.tqdm(range(100)): + time.sleep(0.1) + return x + gr.Interface(my_function, gr.Textbox(), gr.Textbox()).launch() + Guides: progress-bars + """ + + def __init__( + self, + track_tqdm: bool = False, + ): + """ + Parameters: + track_tqdm: If True, the Progress object will track any tqdm.tqdm iterations with the tqdm library in the function. + """ + if track_tqdm: + patch_tqdm() + self.track_tqdm = track_tqdm + self.iterables: list[TrackedIterable] = [] + + def __len__(self): + if not self.iterables: + return 0 + return self.iterables[-1].length + + def __iter__(self): + return self + + def __next__(self): + """ + Updates progress tracker with next item in iterable. + """ + callback = self._progress_callback() + if callback: + current_iterable = self.iterables[-1] + while ( + not hasattr(current_iterable.iterable, "__next__") + and len(self.iterables) > 0 + ): + current_iterable = self.iterables.pop() + callback(self.iterables) + if current_iterable.index is None: + raise IndexError("Index not set.") + current_iterable.index += 1 + try: + return next(current_iterable.iterable) # type: ignore + except StopIteration: + self.iterables.pop() + raise + else: + return self + + def __call__( + self, + progress: float | tuple[int, int | None] | None, + desc: str | None = None, + total: int | float | None = None, + unit: str = "steps", + _tqdm=None, + ): + """ + Updates progress tracker with progress and message text. + Parameters: + progress: If float, should be between 0 and 1 representing completion. If Tuple, first number represents steps completed, and second value represents total steps or None if unknown. If None, hides progress bar. + desc: description to display. + total: estimated total number of steps. + unit: unit of iterations. + """ + callback = self._progress_callback() + if callback: + if isinstance(progress, tuple): + index, total = progress + progress = None + else: + index = None + callback( + self.iterables + + [TrackedIterable(None, index, total, desc, unit, _tqdm, progress)] # type: ignore + ) + else: + return progress + + def tqdm( + self, + iterable: Iterable | None, + desc: str | None = None, + total: int | float | None = None, + unit: str = "steps", + _tqdm=None, + ): + """ + Attaches progress tracker to iterable, like tqdm. + Parameters: + iterable: iterable to attach progress tracker to. + desc: description to display. + total: estimated total number of steps. + unit: unit of iterations. + """ + callback = self._progress_callback() + if callback: + if iterable is None: + new_iterable = TrackedIterable(None, 0, total, desc, unit, _tqdm) + self.iterables.append(new_iterable) + callback(self.iterables) + return self + length = len(iterable) if hasattr(iterable, "__len__") else total # type: ignore + self.iterables.append( + TrackedIterable(iter(iterable), 0, length, desc, unit, _tqdm) + ) + return self + if iterable is None: + return iter([]) + return iter(iterable) + + def update(self, n: int | float = 1): + """ + Increases latest iterable with specified number of steps. + Parameters: + n: number of steps completed. + """ + callback = self._progress_callback() + if callback and len(self.iterables) > 0: + current_iterable = self.iterables[-1] + if current_iterable.index is None: + raise IndexError("Index not set.") + current_iterable.index += n + callback(self.iterables) + else: + return + + def close(self, _tqdm): + """ + Removes iterable with given _tqdm. + """ + callback = self._progress_callback() + if callback: + for i in range(len(self.iterables)): + if id(self.iterables[i]._tqdm) == id(_tqdm): + self.iterables.pop(i) + break + callback(self.iterables) + else: + return + + @staticmethod + def _progress_callback(): + blocks = LocalContext.blocks.get(None) + event_id = LocalContext.event_id.get(None) + if not (blocks and event_id): + return None + return partial(blocks._queue.set_progress, event_id) + + +def patch_tqdm() -> None: + try: + _tqdm = __import__("tqdm") + except ModuleNotFoundError: + return + + def init_tqdm( + self, iterable=None, desc=None, total=None, unit="steps", *args, **kwargs + ): + self._progress = LocalContext.progress.get(None) + if self._progress is not None: + callback = self._progress._progress_callback() + if callback is not None: + self._progress.tqdm(iterable, desc, total, unit, _tqdm=self) + kwargs["file"] = open(os.devnull, "w") # noqa: SIM115 + self.__init__orig__(iterable, desc, total, *args, unit=unit, **kwargs) + + def iter_tqdm(self): + if self._progress is not None: + callback = self._progress._progress_callback() + if callback is not None: + return self._progress + return self.__iter__orig__() + + def update_tqdm(self, n=1): + if self._progress is not None: + callback = self._progress._progress_callback() + if callback is not None: + self._progress.update(n) + return self.__update__orig__(n) + + def close_tqdm(self): + if self._progress is not None: + callback = self._progress._progress_callback() + if callback is not None: + self._progress.close(self) + return self.__close__orig__() + + def exit_tqdm(self, exc_type, exc_value, traceback): + if self._progress is not None: + callback = self._progress._progress_callback() + if callback is not None: + self._progress.close(self) + return self.__exit__orig__(exc_type, exc_value, traceback) + + # Backup + if not hasattr(_tqdm.tqdm, "__init__orig__"): + _tqdm.tqdm.__init__orig__ = _tqdm.tqdm.__init__ # type: ignore + if not hasattr(_tqdm.tqdm, "__update__orig__"): + _tqdm.tqdm.__update__orig__ = _tqdm.tqdm.update # type: ignore + if not hasattr(_tqdm.tqdm, "__close__orig__"): + _tqdm.tqdm.__close__orig__ = _tqdm.tqdm.close # type: ignore + if not hasattr(_tqdm.tqdm, "__exit__orig__"): + _tqdm.tqdm.__exit__orig__ = _tqdm.tqdm.__exit__ # type: ignore + if not hasattr(_tqdm.tqdm, "__iter__orig__"): + _tqdm.tqdm.__iter__orig__ = _tqdm.tqdm.__iter__ # type: ignore + + # Patch + _tqdm.tqdm.__init__ = init_tqdm # type: ignore + _tqdm.tqdm.update = update_tqdm # type: ignore + _tqdm.tqdm.close = close_tqdm # type: ignore + _tqdm.tqdm.__exit__ = exit_tqdm # type: ignore + _tqdm.tqdm.__iter__ = iter_tqdm # type: ignore + + if hasattr(_tqdm, "auto") and hasattr(_tqdm.auto, "tqdm"): + _tqdm.auto.tqdm = _tqdm.tqdm + + +def create_tracker(fn, track_tqdm): + progress = Progress(track_tqdm=track_tqdm) + if not track_tqdm: + return progress, fn + return progress, utils.function_wrapper( + f=fn, + before_fn=LocalContext.progress.set, + before_args=(progress,), + after_fn=LocalContext.progress.set, + after_args=(None,), + ) + + +def _session_from_request(request: Any) -> MutableMapping[str, Any]: + """Return the request's session, or an empty mapping if there is no session + to read (the normal case for apps without OAuth / SessionMiddleware). + + For a real Starlette/fastapi request the session lives in `scope["session"]`; + touching `.session` directly would raise Starlette's "SessionMiddleware must + be installed" assertion when it's absent, so we gate on `scope` membership + instead. A `gr.Request` exposes the fastapi request as `.request`; objects + without a Starlette `scope` (a queued `gr.Request` rebuilt from kwargs) may + still carry a plain `.session`. + """ + if request is None: + return {} + underlying = getattr(request, "request", None) or request + scope = getattr(underlying, "scope", None) + if isinstance(scope, dict): + return scope.get("session", {}) + return getattr(underlying, "session", None) or {} + + +def special_args( + fn: Callable, + inputs: list[Any] | None = None, + request: routes.Request | None = None, + event_data: EventData | None = None, + component_props: dict[int, dict[str, Any]] | None = None, + token: oauth.OAuthToken | None = None, +) -> tuple[list, int | None, int | None, list[int]]: + """ + Checks if function has special arguments Request or EventData (via annotation) or Progress (via default value). + Also checks if any parameters are type-hinted with gr.Component types, in which case all component props should be passed. + If inputs is provided, these values will be loaded into the inputs array. + Parameters: + fn: function to check. + inputs: array to load special arguments into. + request: request to load into inputs. + event_data: event-related data to load into inputs. + component_props: dictionary mapping input indices to their full component props. + token: an OAuthToken to inject into OAuthToken-annotated params when one cannot + be derived from the request session (e.g. server-side workflow calls that + resolve the token outside of a browser session). + Returns: + updated inputs, progress index, event data index, list of input indices that need component props. + """ + try: + signature = inspect.signature(fn) + except ValueError: + return inputs or [], None, None, [] + from gradio.components.base import Component + + type_hints = utils.get_type_hints(fn) + positional_args = [] + for param in signature.parameters.values(): + if param.kind not in (param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD): + break + positional_args.append(param) + progress_index = None + event_data_index = None + component_prop_indices = [] + for i, param in enumerate(positional_args): + type_hint = type_hints.get(param.name) + if isinstance(param.default, Progress): + progress_index = i + if inputs is not None: + inputs.insert(i, param.default) + elif isinstance(param.default, Cache): + if inputs is not None: + inputs.insert(i, param.default) + elif type_hint in (routes.Request, Optional[routes.Request]): + if inputs is not None: + inputs.insert(i, request) + elif type_hint in ( + oauth.OAuthProfile | None, + oauth.OAuthToken | None, + oauth.OAuthProfile, + oauth.OAuthToken, + ): + if inputs is not None: + # Read the session if a SessionMiddleware is installed (i.e. the + # user may be logged in); otherwise treat as logged out. Required + # OAuth params still raise an explicit error below. + session = _session_from_request(request) + + # Expiry means "treat as logged out" here; the session entry + # itself is only removed on the next LoginButton page-load check, + # which operates on the raw session. + oauth_info = oauth._get_valid_oauth_info_from_session(session) + + # Inject user profile + if type_hint in (Optional[oauth.OAuthProfile], oauth.OAuthProfile): + oauth_profile = ( + oauth_info["userinfo"] if oauth_info is not None else None + ) + if oauth_profile is not None: + oauth_profile = oauth.OAuthProfile(oauth_profile) + elif type_hint == oauth.OAuthProfile: + raise Error( + "This action requires a logged in user. Please sign in and retry." + ) + inputs.insert(i, oauth_profile) + + # Inject user token + elif type_hint in (Optional[oauth.OAuthToken], oauth.OAuthToken): + oauth_token = ( + oauth.OAuthToken( + token=oauth_info["access_token"], + scope=oauth_info["scope"], + expires_at=oauth_info["expires_at"], + ) + if oauth_info is not None + else None + ) + # Fall back to a directly-supplied token when the session does + # not yield one (e.g. server-side workflow calls). + if oauth_token is None and token is not None: + oauth_token = token + if oauth_token is None and type_hint == oauth.OAuthToken: + raise Error( + "This action requires a logged in user. Please sign in and retry." + ) + inputs.insert(i, oauth_token) + elif type_hint in (Header, Optional[Header]): + if inputs is not None and request is not None: + header_name = param.name.replace("_", "-").lower() + header_value = None + if hasattr(request, "headers"): + for k, v in dict(request.headers).items(): + if k.lower() == header_name: + header_value = v + break + if len(inputs) > i: + inputs[i] = header_value + else: + inputs.insert(i, header_value) + elif ( + type_hint + and inspect.isclass(type_hint) + and issubclass(type_hint, EventData) + ): + event_data_index = i + if inputs is not None and event_data is not None: + processing_utils.check_all_files_in_cache(event_data._data) + inputs.insert(i, type_hint(event_data.target, event_data._data)) + elif ( + type_hint + and get_origin(type_hint) is None + and inspect.isclass(type_hint) + and issubclass(type_hint, Component) + ): + component_prop_indices.append(i) + if inputs is not None and component_props and i in component_props: + inputs[i] = SimpleNamespace( + **component_props[i], _is_component_update=True + ) + elif ( + param.default is not param.empty and inputs is not None and len(inputs) <= i + ): + inputs.insert(i, param.default) + if inputs is not None: + while len(inputs) < len(positional_args): + i = len(inputs) + param = positional_args[i] + if param.default == param.empty: + warnings.warn("Unexpected argument. Filling with None.") + inputs.append(None) + else: + inputs.append(param.default) + return inputs or [], progress_index, event_data_index, component_prop_indices + + +def update( + elem_id: str | None = None, + elem_classes: list[str] | str | None = None, + visible: bool | Literal["hidden"] | None = None, + **kwargs: Any, +) -> dict[str, Any]: + """ + Updates a component's properties. When a function passed into a Gradio Interface or a Blocks + events returns a value, it typically updates the value of the output component. But it is also possible + to update the *properties* of an output component (such as the number of lines of a `Textbox` or + the visibility of an `Row`) by returning a component and passing in the parameters to update in + the constructor of the component. Alternatively, you can return `gr.update(...)` with any arbitrary + parameters to update. (This is useful as a shorthand or if the same function can be called with different + components to update.) For `gr.State` components, only the `value` parameter is supported. + + Parameters: + elem_id: Use this to update the id of the component in the HTML DOM + elem_classes: Use this to update the classes of the component in the HTML DOM + visible: Use this to update the visibility of the component + kwargs: Any other keyword arguments to update the component's properties. + Example: + import gradio as gr + with gr.Blocks() as demo: + radio = gr.Radio([1, 2, 4], label="Set the value of the number") + number = gr.Number(value=2, interactive=True) + radio.change(fn=lambda value: gr.update(value=value), inputs=radio, outputs=number) + demo.launch() + """ + kwargs["__type__"] = "update" + if elem_id is not None: + kwargs["elem_id"] = elem_id + if elem_classes is not None: + kwargs["elem_classes"] = elem_classes + if visible is not None: + kwargs["visible"] = visible + return kwargs + + +@document() +def validate(is_valid: bool, message: str): + """ + A special function that can be returned from a Gradio function to set the validation error of an output component. + """ + return {"__type__": "validate", "is_valid": is_valid, "message": message} + + +@document() +def skip() -> dict: + """ + A special function that can be returned from a Gradio function to skip updating the output component. This may be useful when + you want to update the output component conditionally, and in some cases, you want to skip updating the output component. + If you have multiple output components, you can return `gr.skip()` as part of a tuple to skip updating a specific output component, + or you can return a single `gr.skip()` to skip updating all output components. + """ + return {"__type__": "update"} + + +def log_message( + message: str, + title: str, + level: Literal["info", "warning", "success"] = "info", + duration: float | None = 10, + visible: bool = True, +): + from gradio.context import LocalContext + + blocks = LocalContext.blocks.get(None) + event_id = LocalContext.event_id.get(None) + if blocks is None or event_id is None: + # Function called outside of Gradio if blocks is None + # Or from /api/predict if event_id is None + if level in ("info", "success"): + print(message) + elif level == "warning": + warnings.warn(message) + return + blocks._queue.log_message( + event_id=event_id, + log=message, + title=title, + level=level, + duration=duration, + visible=visible, + ) + + +@document(documentation_group="modals") +def Warning( # noqa: N802 + message: str = "Warning issued.", + duration: float | None = 10, + visible: bool = True, + title: str = "Warning", +): + """ + This function allows you to pass custom warning messages to the user. You can do so simply by writing `gr.Warning('message here')` in your function, and when that line is executed the custom message will appear in a modal on the demo. The modal is yellow by default and has the heading: "Warning." Queue must be enabled for this behavior; otherwise, the warning will be printed to the console using the `warnings` library. + Demos: blocks_chained_events + Parameters: + message: The warning message to be displayed to the user. Can be HTML, which will be rendered in the modal. + duration: The duration in seconds that the warning message should be displayed for. If None or 0, the message will be displayed indefinitely until the user closes it. + visible: Whether the error message should be displayed in the UI. + title: The title to be displayed to the user at the top of the modal. + Example: + import gradio as gr + def hello_world(): + gr.Warning('This is a warning message.') + return "hello world" + with gr.Blocks() as demo: + md = gr.Markdown() + demo.load(hello_world, inputs=None, outputs=[md]) + demo.launch() + """ + log_message( + message, title=title, level="warning", duration=duration, visible=visible + ) + + +@document(documentation_group="modals") +def Info( # noqa: N802 + message: str = "Info issued.", + duration: float | None = 10, + visible: bool = True, + title: str = "Info", +): + """ + This function allows you to pass custom info messages to the user. You can do so simply by writing `gr.Info('message here')` in your function, and when that line is executed the custom message will appear in a modal on the demo. The modal is gray by default and has the heading: "Info." Queue must be enabled for this behavior; otherwise, the message will be printed to the console. + Demos: blocks_chained_events + Parameters: + message: The info message to be displayed to the user. Can be HTML, which will be rendered in the modal. + duration: The duration in seconds that the info message should be displayed for. If None or 0, the message will be displayed indefinitely until the user closes it. + visible: Whether the error message should be displayed in the UI. + title: The title to be displayed to the user at the top of the modal. + Example: + import gradio as gr + def hello_world(): + gr.Info('This is some info.') + return "hello world" + with gr.Blocks() as demo: + md = gr.Markdown() + demo.load(hello_world, inputs=None, outputs=[md]) + demo.launch() + """ + log_message(message, title=title, level="info", duration=duration, visible=visible) + + +@document(documentation_group="modals") +def Success( # noqa: N802 + message: str = "Success.", + duration: float | None = 10, + visible: bool = True, + title: str = "Success", +): + """ + This function allows you to pass custom success messages to the user. You can do so simply by writing `gr.Success('message here')` in your function, and when that line is executed the custom message will appear in a modal on the demo. The modal is green by default and has the heading: "Success." Queue must be enabled for this behavior; otherwise, the message will be printed to the console. + Parameters: + message: The success message to be displayed to the user. Can be HTML, which will be rendered in the modal. + duration: The duration in seconds that the success message should be displayed for. If None or 0, the message will be displayed indefinitely until the user closes it. + visible: Whether the error message should be displayed in the UI. + title: The title to be displayed to the user at the top of the modal. + Example: + def hello_world(): + gr.Success('Operation completed successfully!') + return "hello world" + with gr.Blocks() as demo: + md = gr.Markdown() + demo.load(hello_world, inputs=None, outputs=[md]) + demo.launch() + """ + log_message( + message, title=title, level="success", duration=duration, visible=visible + ) diff --git a/gradio/http_server.py b/gradio/http_server.py new file mode 100644 index 0000000..84924f1 --- /dev/null +++ b/gradio/http_server.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +import os +import socket +import sys +import threading +import time +from collections.abc import Callable +from functools import partial +from typing import TYPE_CHECKING, TypeVar + +import uvicorn +from uvicorn.config import Config + +from gradio.exceptions import ServerFailedToStartError +from gradio.routes import App +from gradio.utils import ( + ServerReloader, + SourceFileReloader, + SpacesReloader, + get_space, + watchfn, + watchfn_spaces, +) + +if TYPE_CHECKING: # Only import for type checking (to avoid circular imports). + _ServerReloaderT = TypeVar("_ServerReloaderT", bound=ServerReloader) + +# By default, the local server will try to open on localhost, port 7860. +# If that is not available, then it will try 7861, 7862, ... 7959. +INITIAL_PORT_VALUE = int(os.getenv("GRADIO_SERVER_PORT", "7860")) +TRY_NUM_PORTS = int(os.getenv("GRADIO_NUM_PORTS", "100")) +LOCALHOST_NAME = os.getenv("GRADIO_SERVER_NAME", "127.0.0.1") + +GRADIO_HOT_RELOAD = os.getenv("GRADIO_HOT_RELOAD", "false").lower() + +should_watch = bool(os.getenv("GRADIO_WATCH_DIRS", "")) +GRADIO_WATCH_DIRS = ( + os.getenv("GRADIO_WATCH_DIRS", "").split(",") if should_watch else [] +) +GRADIO_WATCH_MODULE_NAME = os.getenv("GRADIO_WATCH_MODULE_NAME", "app") +GRADIO_WATCH_DEMO_NAME = os.getenv("GRADIO_WATCH_DEMO_NAME", "") +GRADIO_WATCH_DEMO_PATH = os.getenv("GRADIO_WATCH_DEMO_PATH", "") + + +class Server(uvicorn.Server): + def __init__( + self, + config: Config, + reloader: _ServerReloaderT | None = None, + watchfn: Callable[[_ServerReloaderT], None] = watchfn, # ty: ignore[invalid-parameter-default] + ) -> None: + self.running_app = config.app + super().__init__(config) + self.reloader = reloader + if self.reloader: + self.event = threading.Event() + self.watch = partial(watchfn, self.reloader) + + def install_signal_handlers(self): + pass + + def run_in_thread(self): + self.thread = threading.Thread(target=self.run, daemon=True) + if self.reloader: + self.watch_thread = threading.Thread(target=self.watch, daemon=True) + self.watch_thread.start() + self.thread.start() + start = time.time() + while not self.started: + time.sleep(1e-3) + if time.time() - start > 5: + raise ServerFailedToStartError( + "Server failed to start. Please check that the port is available." + ) + + def close(self): + self.should_exit = True + if self.reloader: + self.reloader.stop() + self.watch_thread.join() + self.thread.join(timeout=5) + + +def start_server( + app: App, + server_name: str | None = None, + server_port: int | None = None, + ssl_keyfile: str | None = None, + ssl_certfile: str | None = None, + ssl_keyfile_password: str | None = None, +) -> tuple[str, int, str, Server]: + """Launches a local server running the provided Interface + Parameters: + app: the FastAPI app object to run + server_name: to make app accessible on local network, set this to "0.0.0.0". Can be set by environment variable GRADIO_SERVER_NAME. + server_port: will start gradio app on this port (if available). Can be set by environment variable GRADIO_SERVER_PORT. + auth: If provided, username and password (or list of username-password tuples) required to access the Blocks. Can also provide function that takes username and password and returns True if valid login. + ssl_keyfile: If a path to a file is provided, will use this as the private key file to create a local server running on https. + ssl_certfile: If a path to a file is provided, will use this as the signed certificate for https. Needs to be provided if ssl_keyfile is provided. + ssl_keyfile_password: If a password is provided, will use this with the ssl certificate for https. + + Returns: + server_name: the name of the server (default is "localhost") + port: the port number the server is running on + path_to_local_server: the complete address that the local server can be accessed at + server: the server object that is a subclass of uvicorn.Server (used to close the server) + """ + if ssl_keyfile is not None and ssl_certfile is None: + raise ValueError("ssl_certfile must be provided if ssl_keyfile is provided.") + + server_name = server_name or LOCALHOST_NAME + url_host_name = "localhost" if server_name == "0.0.0.0" else server_name + + # Strip IPv6 brackets from the address if they exist. + # This is needed as http://[::1]:port/ is a valid browser address, + # but not a valid IPv6 address, so asyncio will throw an exception. + if server_name.startswith("[") and server_name.endswith("]"): + host = server_name[1:-1] + else: + host = server_name + + server_ports = ( + [server_port] + if server_port is not None + else range(INITIAL_PORT_VALUE, INITIAL_PORT_VALUE + TRY_NUM_PORTS) + ) + + for port in server_ports: + try: + # The fastest way to check if a port is available is to try to bind to it with socket. + # If the port is not available, socket will throw an OSError. + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # Really, we should be checking if (server_name, server_port) is available, but + # socket.bind() doesn't seem to throw an OSError with ipv6 addresses, based on my testing. + # Instead, we just check if the port is available on localhost. + s.bind((LOCALHOST_NAME, port)) + s.close() + + # To avoid race conditions, so we also check if the port by trying to start the uvicorn server. + # If the port is not available, this will throw a ServerFailedToStartError. + config = uvicorn.Config( + app=app, + port=port, + host=host, + log_level="warning", + ssl_keyfile=ssl_keyfile, + ssl_certfile=ssl_certfile, + ssl_keyfile_password=ssl_keyfile_password, + ) + if get_space() is not None: + reloader = SpacesReloader( + app=app, + watch_dirs=GRADIO_WATCH_DIRS, + demo_name=GRADIO_WATCH_DEMO_NAME, + stop_event=threading.Event(), + watch_module=sys.modules["__main__"], + ) + server = Server( + config=config, reloader=reloader, watchfn=watchfn_spaces + ) + elif GRADIO_WATCH_DIRS: + reloader = SourceFileReloader( + app=app, + watch_dirs=GRADIO_WATCH_DIRS, + watch_module_name=GRADIO_WATCH_MODULE_NAME, + demo_name=GRADIO_WATCH_DEMO_NAME, + stop_event=threading.Event(), + demo_file=GRADIO_WATCH_DEMO_PATH, + watch_module=sys.modules["__main__"], + encoding=os.getenv("GRADIO_WATCH_ENCODING", "utf-8"), + ) + server = Server(config=config, reloader=reloader) + else: + server = Server(config=config) + server.run_in_thread() + break + except (OSError, ServerFailedToStartError): + pass + else: + raise OSError( + f"Cannot find empty port in range: {min(server_ports)}-{max(server_ports)}. You can specify a different port by setting the GRADIO_SERVER_PORT environment variable or passing the `server_port` parameter to `launch()`." + ) + + if ssl_keyfile is not None: + path_to_local_server = f"https://{url_host_name}:{port}/" + else: + path_to_local_server = f"http://{url_host_name}:{port}/" + + return server_name, port, path_to_local_server, server diff --git a/gradio/i18n.py b/gradio/i18n.py new file mode 100644 index 0000000..7097444 --- /dev/null +++ b/gradio/i18n.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +import re +import warnings +from typing import Any + + +class I18nData: + """ + A class that wraps a translation key with metadata. + + This object will be serialized and sent to the frontend, where the actual + translation will happen using the frontend's i18n system. + """ + + def __init__(self, key: str): + """ + Initialize a I18nData object. + + Args: + key: The translation key to be translated in the frontend. + """ + self.key = key + self._type = "translation_metadata" + + def to_dict(self) -> dict[str, Any]: + """ + Convert the I18nData object to a dictionary for serialization. + This allows the frontend to recognize it as a translatable object. + """ + return {"__type__": self._type, "key": self.key} + + def __str__(self) -> str: + """ + String representation of the I18nData object. + Used when the object is converted to a string. + This returns a special format that can be recognized by the frontend + as needing translation. + """ + import json + + return f"__i18n__{json.dumps(self.to_dict())}" + + def __repr__(self) -> str: + """ + Representation of the I18nData object for debugging. + """ + return self.__str__() + + def __add__(self, other): + """ + Handle string concatenation (self + other). + """ + return str(self) + str(other) + + def __radd__(self, other): + """ + Handle string concatenation (other + self). + """ + return str(other) + str(self) + + def __getattr__(self, name): + """ + Handle attribute access for I18nData. + This makes it possible to use I18nData objects in contexts + that expect strings with methods. + """ + if name.startswith("__") and name.endswith("__"): + raise AttributeError(f"{self.__class__.__name__} has no attribute {name}") + + def method(*_args, **_kwargs): + return self + + return method + + def tojson(self) -> dict[str, Any]: + """ + Convert the I18nData object to a JSON-serializable dictionary. + This is used by the default Python JSON serializer. + """ + return self.to_dict() + + +class I18n: + """ + Handles internationalization (i18n) for Gradio applications. + + Stores translation dictionaries and provides a method to retrieve translation keys. + The translation lookup happens on the frontend based on the browser's locale + and the provided translation dictionaries. + """ + + # BCP 47 language tag regex pattern + _LOCALE_PATTERN = re.compile(r"^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$") + + def __init__(self, **translations: dict[str, str]): + """ + Initializes the I18n class. + + Args: + **translations: Each keyword argument should be a locale code (e.g., "en", "fr") with a + dictionary value, which maps translation keys to translated strings. + Example: gr.I18n(en={"greeting": "Hello"}, es={"greeting": "Hola"}) + + These translations can be passed to the frontend for use there. + """ + self.translations = {} + + for locale, translation_dict in translations.items(): + if not self._is_valid_locale(locale): + warnings.warn( + f"Invalid locale code: '{locale}'. Locale codes should follow BCP 47 format (e.g., 'en', 'en-US'). " + f"This locale will still be included, but may not work correctly.", + UserWarning, + ) + self.translations[locale] = translation_dict + + def _is_valid_locale(self, locale: str) -> bool: + return bool(self._LOCALE_PATTERN.match(locale)) + + def __call__(self, key: str) -> I18nData: + """ + Returns a I18nData object containing the translation key. + + This metadata object will be serialized and sent to the frontend, + where it will be translated by the frontend's i18n system. + + Args: + key: The key to identify the translation string (e.g., "submit_button"). + + Returns: + A I18nData object containing the translation key. + """ + return I18nData(key) + + @property + def translations_dict(self) -> dict[str, dict[str, str]]: + """ + Returns the dictionary of translations provided during initialization. + These can be passed to the frontend for use in its translation system. + """ + return self.translations diff --git a/gradio/icons/README.md b/gradio/icons/README.md new file mode 100644 index 0000000..f38df88 --- /dev/null +++ b/gradio/icons/README.md @@ -0,0 +1,2 @@ +The icons in this directory are loaded via `gradio.utils.get_icon_path` and +can be used directly in backend code (e.g. to populate icons in components). \ No newline at end of file diff --git a/gradio/icons/huggingface-logo.svg b/gradio/icons/huggingface-logo.svg new file mode 100644 index 0000000..43c5d3c --- /dev/null +++ b/gradio/icons/huggingface-logo.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/gradio/icons/link.svg b/gradio/icons/link.svg new file mode 100644 index 0000000..25f5c5d --- /dev/null +++ b/gradio/icons/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gradio/icons/plus.svg b/gradio/icons/plus.svg new file mode 100644 index 0000000..cb2ec6f --- /dev/null +++ b/gradio/icons/plus.svg @@ -0,0 +1,12 @@ + + + diff --git a/gradio/image_utils.py b/gradio/image_utils.py new file mode 100644 index 0000000..6e7a86a --- /dev/null +++ b/gradio/image_utils.py @@ -0,0 +1,359 @@ +from __future__ import annotations + +import base64 +import warnings +from io import BytesIO +from pathlib import Path +from typing import Literal, cast +from urllib.parse import quote + +import numpy as np +import PIL.Image +from gradio_client import utils as client_utils +from gradio_client.utils import get_mimetype, is_http_url_like +from PIL import ImageOps + +from gradio import processing_utils +from gradio.components.image_editor import WatermarkOptions +from gradio.data_classes import ImageData +from gradio.exceptions import Error +from gradio.profiling import traced_sync + +PIL.Image.init() # fixes https://github.com/gradio-app/gradio/issues/2843 (remove when requiring Pillow 9.4+) + + +def open_image(orig_img: np.ndarray | PIL.Image.Image | str | Path) -> PIL.Image.Image: + """ + Provided an array, PIL Image or filepath, return a PIL Image. + Parameters: + orig_img: Local image file. If a filepath, it must be a webp, png, jpeg, or bmp. + Returns: + open_img: A PIL.Image.Image. + """ + + if isinstance(orig_img, np.ndarray): + open_img = PIL.Image.fromarray(orig_img) + elif isinstance(orig_img, (str, Path)): + open_img = PIL.Image.open(orig_img) + elif isinstance(orig_img, PIL.Image.Image): + open_img = orig_img + else: + raise ValueError( + "Expected image or path to image of type webp, png, bmp or jpeg; PIL image; or numpy array. Received " + + str(type(orig_img)) + ) + return open_img + + +def format_image( + im: PIL.Image.Image | None, + type: Literal["numpy", "pil", "filepath"], + cache_dir: str, + name: str = "image", + format: str = "webp", +) -> np.ndarray | PIL.Image.Image | str | None: + """Helper method to format an image based on self.type""" + if im is None: + return im + if type == "pil": + return im + elif type == "numpy": + return np.array(im) + elif type == "filepath": + try: + path = processing_utils.save_pil_to_cache( + im, cache_dir=cache_dir, name=name, format=format + ) + # Catch error if format is not supported by PIL + except (KeyError, ValueError): + path = processing_utils.save_pil_to_cache( + im, + cache_dir=cache_dir, + name=name, + format="png", # type: ignore + ) + return path + else: + raise ValueError( + "Unknown type: " + + str(type) + + ". Please choose from: 'numpy', 'pil', 'filepath'." + ) + + +def save_image( + y: np.ndarray | PIL.Image.Image | str | Path, cache_dir: str, format: str = "webp" +): + if isinstance(y, np.ndarray): + path = processing_utils.save_img_array_to_cache( + y, cache_dir=cache_dir, format=format + ) + elif isinstance(y, PIL.Image.Image): + try: + path = processing_utils.save_pil_to_cache( + y, cache_dir=cache_dir, format=format + ) + # Catch error if format is not supported by PIL + except (KeyError, ValueError): + path = processing_utils.save_pil_to_cache( + y, cache_dir=cache_dir, format="png" + ) + elif isinstance(y, Path): + path = str(y) + elif isinstance(y, str): + path = y + else: + raise ValueError( + "Cannot process this value as an Image, it is of type: " + str(type(y)) + ) + + return path + + +def add_watermark( + base_img: np.ndarray | PIL.Image.Image | str | Path, + watermark_option: WatermarkOptions, +) -> PIL.Image.Image: + """Overlays a watermark image on a base image. + Parameters: + base_img: Base image onto which the watermark is applied. Can be an array, PIL Image, or filepath. + watermarkOption: WatermarkOptions instance containing watermark image and position settings. + Returns: + watermarked_img: A PIL Image of the base image overlaid with the watermark image. + """ + base_img = open_image(base_img) + base_img_width, base_img_height = base_img.size + watermark_option.watermark = open_image( + cast(np.ndarray | PIL.Image.Image | str | Path, watermark_option.watermark) + ) + watermark_width, watermark_height = watermark_option.watermark.size + + if isinstance(watermark_option.position, str): + padding = 10 + if watermark_option.position == "top-left": + x, y = padding, padding + elif watermark_option.position == "top-right": + x, y = base_img_width - watermark_width - padding, padding + elif watermark_option.position == "bottom-left": + x, y = padding, base_img_height - watermark_height - padding + elif watermark_option.position == "bottom-right": + x, y = ( + base_img_width - watermark_width - padding, + base_img_height - watermark_height - padding, + ) + else: + x, y = watermark_option.position + + if ( + x < 0 + or x + watermark_width > base_img_width + or y < 0 + or y + watermark_height > base_img_height + ): + x = base_img_width - watermark_width - 10 + y = base_img_height - watermark_height - 10 + + watermark_position = (x, y) + orig_img_mode = base_img.mode + base_img = base_img.convert("RGBA") + watermark_option.watermark = watermark_option.watermark.convert("RGBA") + base_img.paste( + watermark_option.watermark, watermark_position, mask=watermark_option.watermark + ) + base_img = base_img.convert(orig_img_mode) + + return base_img + + +def crop_scale(img: PIL.Image.Image, final_width: int, final_height: int): + original_width, original_height = img.size + target_aspect_ratio = final_width / final_height + + if original_width / original_height > target_aspect_ratio: + crop_height = original_height + crop_width = crop_height * target_aspect_ratio + else: + crop_width = original_width + crop_height = crop_width / target_aspect_ratio + + left = (original_width - crop_width) / 2 + top = (original_height - crop_height) / 2 + + img_cropped = img.crop( + (int(left), int(top), int(left + crop_width), int(top + crop_height)) + ) + + img_resized = img_cropped.resize((final_width, final_height)) + + return img_resized + + +def decode_base64_to_image(encoding: str) -> PIL.Image.Image: + image_encoded = processing_utils.extract_base64_data(encoding) + img = PIL.Image.open(BytesIO(base64.b64decode(image_encoded))) + try: + if hasattr(ImageOps, "exif_transpose"): + img = ImageOps.exif_transpose(img) + except Exception: + print( + "Failed to transpose image %s based on EXIF data.", + img, + ) + assert img is not None # noqa: S101 + return img + + +def decode_base64_to_image_array(encoding: str) -> np.ndarray: + img = decode_base64_to_image(encoding) + return np.asarray(img) + + +def decode_base64_to_file(encoding: str, cache_dir: str, format: str = "webp") -> str: + img = decode_base64_to_image(encoding) + return save_image(img, cache_dir, format) + + +def encode_image_array_to_base64(image_array: np.ndarray) -> str: + with BytesIO() as output_bytes: + pil_image = PIL.Image.fromarray( + processing_utils._convert(image_array, np.uint8, force_copy=False) + ) + pil_image.save(output_bytes, "JPEG") + bytes_data = output_bytes.getvalue() + base64_str = str(base64.b64encode(bytes_data), "utf-8") + return "data:image/jpeg;base64," + base64_str + + +def encode_image_to_base64(image: PIL.Image.Image) -> str: + with BytesIO() as output_bytes: + image.save(output_bytes, "JPEG") + bytes_data = output_bytes.getvalue() + base64_str = str(base64.b64encode(bytes_data), "utf-8") + return "data:image/jpeg;base64," + base64_str + + +def encode_image_file_to_base64(image_file: str | Path) -> str: + mime_type = get_mimetype(str(image_file)) + with open(image_file, "rb") as f: + bytes_data = f.read() + base64_str = str(base64.b64encode(bytes_data), "utf-8") + return f"data:{mime_type};base64," + base64_str + + +def extract_svg_content(image_file: str | Path) -> str: + """ + Provided a path or URL to an SVG file, return the SVG content as a string. + Parameters: + image_file: Local file path or URL to an SVG file + Returns: + str: The SVG content as a string + """ + image_file = str(image_file) + if is_http_url_like(image_file): + response = client_utils.synchronize_async( + processing_utils.async_ssrf_protected_get, image_file + ) + response.raise_for_status() + return response.text + else: + with open(image_file) as file: + svg_content = file.read() + return svg_content + + +@traced_sync("preprocess_format_image") +def preprocess_image( + payload: ImageData | None, + cache_dir: str, + format: str, + image_mode: Literal[ + "1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F" + ] + | None, + type: Literal["numpy", "pil", "filepath"], +) -> np.ndarray | PIL.Image.Image | str | None: + if payload is None: + return payload + if payload.url and payload.url.startswith("data:"): + if type == "pil": + return decode_base64_to_image(payload.url) + elif type == "numpy": + return decode_base64_to_image_array(payload.url) + elif type == "filepath": + return decode_base64_to_file(payload.url, cache_dir, format) + if payload.path is None: + raise ValueError("Image path is None.") + file_path = Path(payload.path) + if payload.orig_name: + p = Path(payload.orig_name) + name = p.stem + suffix = p.suffix.replace(".", "") + if suffix in ["jpg", "jpeg"]: + suffix = "jpeg" + else: + name = "image" + suffix = "webp" + + if suffix.lower() == "svg": + if type == "filepath": + return str(file_path) + raise Error("SVG files are not supported as input images for this app.") + + im = PIL.Image.open(file_path) + if type == "filepath" and (image_mode in [None, im.mode]): + return str(file_path) + + exif = im.getexif() + # 274 is the code for image rotation and 1 means "correct orientation" + if exif.get(274, 1) != 1 and hasattr(ImageOps, "exif_transpose"): + try: + im = ImageOps.exif_transpose(im) + except Exception: + warnings.warn(f"Failed to transpose image {file_path} based on EXIF data.") + if suffix.lower() != "gif" and im is not None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + if image_mode is not None: + im = im.convert(image_mode) + + return format_image( + im, + type=type, + cache_dir=cache_dir, + name=name, + format=suffix, + ) + + +def postprocess_image( + value: np.ndarray | PIL.Image.Image | str | Path | None, + cache_dir: str, + format: str, + watermark: WatermarkOptions | None = None, +) -> ImageData | None: + """ + Parameters: + value: Expects a `numpy.array`, `PIL.Image`, or `str` or `pathlib.Path` filepath to an image which is displayed. + watermark: An optional `WatermarkOptions` instance to apply a watermark to the image. + Returns: + Returns the image as a `FileData` object. + """ + from gradio import Warning + + if value is None: + return None + if isinstance(value, str) and value.lower().endswith(".svg"): + svg_content = extract_svg_content(value) + if watermark is not None: + Warning( + "Watermarking for SVG images is currently not supported. No watermark will be applied." + ) + return ImageData( + orig_name=Path(value).name, + url=f"data:image/svg+xml,{quote(svg_content)}", + ) + if watermark and watermark.watermark is not None: + value = add_watermark(value, watermark) + saved = save_image(value, cache_dir=cache_dir, format=format) + orig_name = Path(saved).name if Path(saved).exists() else None + return ImageData(path=saved, orig_name=orig_name) diff --git a/gradio/interface.py b/gradio/interface.py new file mode 100644 index 0000000..f8addfc --- /dev/null +++ b/gradio/interface.py @@ -0,0 +1,1009 @@ +""" +This file defines two useful high-level abstractions to build Gradio apps: Interface and TabbedInterface. +""" + +from __future__ import annotations + +import inspect +import json +import os +import warnings +from collections.abc import Callable, Sequence +from typing import TYPE_CHECKING, Any, Literal + +from gradio_client.documentation import document + +from gradio import Examples, utils +from gradio.blocks import Blocks +from gradio.components import ( + Button, + ClearButton, + Component, + DeepLinkButton, + DuplicateButton, + Markdown, + State, + get_component_instance, +) +from gradio.data_classes import InterfaceTypes +from gradio.events import Dependency, Events, on +from gradio.exceptions import RenderError +from gradio.flagging import CSVLogger, FlaggingCallback, FlagMethod +from gradio.i18n import I18nData +from gradio.layouts import Accordion, Column, Row, Tab, Tabs +from gradio.pipelines import load_from_pipeline + +if TYPE_CHECKING: # Only import for type checking (is False at runtime). + from diffusers import DiffusionPipeline # type: ignore + from transformers.pipelines.base import Pipeline + + +@document("launch", "load", "from_pipeline", "integrate", "queue") +class Interface(Blocks): + """ + Interface is Gradio's main high-level class, and allows you to create a web-based GUI / demo + around a machine learning model (or any Python function) in a few lines of code. + You must specify three parameters: (1) the function to create a GUI for (2) the desired input components and + (3) the desired output components. Additional parameters can be used to control the appearance + and behavior of the demo. + + Example: + import gradio as gr + + def image_classifier(inp): + return {'cat': 0.3, 'dog': 0.7} + + demo = gr.Interface(fn=image_classifier, inputs="image", outputs="label") + demo.launch() + Demos: hello_world, hello_world_2, hello_world_3 + Guides: the-interface-class, interface-state, reactive-interfaces, four-kinds-of-interfaces, sharing-your-app + """ + + @classmethod + def from_pipeline( + cls, pipeline: Pipeline | DiffusionPipeline, **kwargs + ) -> Interface: + """ + Class method that constructs an Interface from a Hugging Face transformers.Pipeline or diffusers.DiffusionPipeline object. + The input and output components are automatically determined from the pipeline. + Parameters: + pipeline: the pipeline object to use. + Returns: + a Gradio Interface object from the given Pipeline + Example: + import gradio as gr + from transformers import pipeline + pipe = pipeline("image-classification") + gr.Interface.from_pipeline(pipe).launch() + """ + interface_info = load_from_pipeline(pipeline) + kwargs = dict(interface_info, **kwargs) + interface = cls(**kwargs) + return interface + + def __init__( + self, + fn: Callable, + inputs: str | Component | Sequence[str | Component] | None, + outputs: str | Component | Sequence[str | Component] | None, + examples: list[Any] | list[list[Any]] | str | None = None, + *, + cache_examples: bool | None = None, + cache_mode: Literal["eager", "lazy"] | None = None, + examples_per_page: int = 10, + example_labels: list[str] | None = None, + preload_example: int | Literal[False] = 0, + live: bool = False, + title: str | I18nData | None = None, + description: str | None = None, + article: str | None = None, + flagging_mode: Literal["never"] + | Literal["auto"] + | Literal["manual"] + | None = None, + flagging_options: list[str] | list[tuple[str, str]] | None = None, + flagging_dir: str = ".gradio/flagged", + flagging_callback: FlaggingCallback | None = None, + analytics_enabled: bool | None = None, + batch: bool = False, + max_batch_size: int = 4, + api_visibility: Literal["public", "private", "undocumented"] = "public", + api_name: str | None = None, + api_description: str | None | Literal[False] = None, + _api_mode: bool = False, + allow_duplication: bool = False, + concurrency_limit: int | None | Literal["default"] = "default", + additional_inputs: str | Component | Sequence[str | Component] | None = None, + additional_inputs_accordion: str | Accordion | None = None, + submit_btn: str | Button = "Submit", + stop_btn: str | Button = "Stop", + clear_btn: str | Button | None = "Clear", + delete_cache: tuple[int, int] | None = None, + show_progress: Literal["full", "minimal", "hidden"] = "full", + fill_width: bool = False, + time_limit: int | None = 30, + stream_every: float = 0.5, + deep_link: str | DeepLinkButton | bool | None = None, + validator: Callable | None = None, + **kwargs, + ): + """ + Parameters: + fn: the function to wrap an interface around. Often a machine learning model's prediction function. Each parameter of the function corresponds to one input component, and the function should return a single value or a tuple of values, with each element in the tuple corresponding to one output component. + inputs: a single Gradio component, or list of Gradio components. Components can either be passed as instantiated objects, or referred to by their string shortcuts. The number of input components should match the number of parameters in fn. If set to None, then only the output components will be displayed. + outputs: a single Gradio component, or list of Gradio components. Components can either be passed as instantiated objects, or referred to by their string shortcuts. The number of output components should match the number of values returned by fn. If set to None, then only the input components will be displayed. + examples: sample inputs for the function; if provided, appear below the UI components and can be clicked to populate the interface. Should be nested list, in which the outer list consists of samples and each inner list consists of an input corresponding to each input component. A string path to a directory of examples can also be provided, but it should be within the directory with the python file running the gradio app. If there are multiple input components and a directory is provided, a log.csv file must be present in the directory to link corresponding inputs. + cache_examples: If True, caches examples in the server for fast runtime in examples. If "lazy", then examples are cached (for all users of the app) after their first use (by any user of the app). If None, will use the GRADIO_CACHE_EXAMPLES environment variable, which should be either "true" or "false". In HuggingFace Spaces, this parameter defaults to True (as long as `fn` and `outputs` are also provided). Note that examples are cached separately from Gradio's queue() so certain features, such as gr.Progress(), gr.Info(), gr.Warning(), etc. will not be displayed in Gradio's UI for cached examples. + cache_mode: if "lazy", examples are cached after their first use. If "eager", all examples are cached at app launch. If None, will use the GRADIO_CACHE_MODE environment variable if defined, or default to "eager". In HuggingFace Spaces, this parameter defaults to "eager" except for ZeroGPU Spaces, in which case it defaults to "lazy". + examples_per_page: if examples are provided, how many to display per page. + preload_example: If an integer is provided (and examples are being cached eagerly and none of the input components have a developer-provided `value`), the example at that index in the examples list will be preloaded when the Gradio app is first loaded. If False, no example will be preloaded. + live: whether the interface should automatically rerun if any of the inputs change. + title: a title for the interface; if provided, appears above the input and output components in large font. Also used as the tab title when opened in a browser window. + description: a description for the interface; if provided, appears above the input and output components and beneath the title in regular font. Accepts Markdown and HTML content. + article: an expanded article explaining the interface; if provided, appears below the input and output components in regular font. Accepts Markdown and HTML content. If it is an HTTP(S) link to a downloadable remote file, the content of this file is displayed. + flagging_mode: one of "never", "auto", or "manual". If "never" or "auto", users will not see a button to flag an input and output. If "manual", users will see a button to flag. If "auto", every input the user submits will be automatically flagged, along with the generated output. If "manual", both the input and outputs are flagged when the user clicks flag button. This parameter can be set with environmental variable GRADIO_FLAGGING_MODE; otherwise defaults to "manual". + flagging_options: if provided, allows user to select from the list of options when flagging. Only applies if flagging_mode is "manual". Can either be a list of tuples of the form (label, value), where label is the string that will be displayed on the button and value is the string that will be stored in the flagging CSV; or it can be a list of strings ["X", "Y"], in which case the values will be the list of strings and the labels will ["Flag as X", "Flag as Y"], etc. + flagging_dir: path to the directory where flagged data is stored. If the directory does not exist, it will be created. + flagging_callback: either None or an instance of a subclass of FlaggingCallback which will be called when a sample is flagged. If set to None, an instance of gradio.flagging.CSVLogger will be created and logs will be saved to a local CSV file in flagging_dir. Default to None. + analytics_enabled: whether to allow basic telemetry. If None, will use GRADIO_ANALYTICS_ENABLED environment variable if defined, or default to True. + batch: if True, then the function should process a batch of inputs, meaning that it should accept a list of input values for each parameter. The lists should be of equal length (and be up to length `max_batch_size`). The function is then *required* to return a tuple of lists (even if there is only 1 output component), with each list in the tuple corresponding to one output component. + max_batch_size: the maximum number of inputs to batch together if this is called from the queue (only relevant if batch=True) + api_name: defines how the prediction endpoint appears in the API docs. Can be a string or None. If set to a string, the endpoint will be exposed in the API docs with the given name. If None, the name of the function will be used. + api_description: Description of the API endpoint. Can be a string, None, or False. If set to a string, the endpoint will be exposed in the API docs with the given description. If None, the function's docstring will be used as the API endpoint description. If False, then no description will be displayed in the API docs. + api_visibility: Controls the visibility of the prediction endpoint. Can be "public" (shown in API docs and callable), "private" (hidden from API docs and not callable by the Gradio client libraries), or "undocumented" (hidden from API docs but callable). + allow_duplication: if True, then will show a 'Duplicate Spaces' button on Hugging Face Spaces. + concurrency_limit: if set, this is the maximum number of this event that can be running simultaneously. Can be set to None to mean no concurrency_limit (any number of this event can be running simultaneously). Set to "default" to use the default concurrency limit (defined by the `default_concurrency_limit` parameter in `.queue()`, which itself is 1 by default). + additional_inputs: a single Gradio component, or list of Gradio components. Components can either be passed as instantiated objects, or referred to by their string shortcuts. These components will be rendered in an accordion below the main input components. By default, no additional input components will be displayed. + additional_inputs_accordion: if a string is provided, this is the label of the `gr.Accordion` to use to contain additional inputs. A `gr.Accordion` object can be provided as well to configure other properties of the container holding the additional inputs. Defaults to a `gr.Accordion(label="Additional Inputs", open=False)`. This parameter is only used if `additional_inputs` is provided. + submit_btn: the button to use for submitting inputs. Defaults to a `gr.Button("Submit", variant="primary")`. This parameter does not apply if the Interface is output-only, in which case the submit button always displays "Generate". Can be set to a string (which becomes the button label) or a `gr.Button` object (which allows for more customization). + stop_btn: the button to use for stopping the interface. Defaults to a `gr.Button("Stop", variant="stop", visible=False)`. Can be set to a string (which becomes the button label) or a `gr.Button` object (which allows for more customization). + clear_btn: the button to use for clearing the inputs. Defaults to a `gr.Button("Clear", variant="secondary")`. Can be set to a string (which becomes the button label) or a `gr.Button` object (which allows for more customization). Can be set to None, which hides the button. + delete_cache: a tuple corresponding [frequency, age] both expressed in number of seconds. Every `frequency` seconds, the temporary files created by this Blocks instance will be deleted if more than `age` seconds have passed since the file was created. For example, setting this to (86400, 86400) will delete temporary files every day. The cache will be deleted entirely when the server restarts. If None, no cache deletion will occur. + show_progress: how to show the progress animation while event is running: "full" shows a spinner which covers the output component area as well as a runtime display in the upper right corner, "minimal" only shows the runtime display, "hidden" shows no progress animation at all + example_labels: a list of labels for each example. If provided, the length of this list should be the same as the number of examples, and these labels will be used in the UI instead of rendering the example values. + fill_width: whether to horizontally expand to fill container fully. If False, centers and constrains app to a maximum width. + time_limit: The time limit for the stream to run. Default is 30 seconds. Parameter only used for streaming images or audio if the interface is live and the input components are set to "streaming=True". + stream_every: The latency (in seconds) at which stream chunks are sent to the backend. Defaults to 0.5 seconds. Parameter only used for streaming images or audio if the interface is live and the input components are set to "streaming=True". + deep_link: a string or `gr.DeepLinkButton` object that creates a unique URL you can use to share your app and all components **as they currently are** with others. Automatically enabled on Hugging Face Spaces unless explicitly set to False. + validator: a function that takes in the inputs and can optionally return a gr.validate() object for each input. + + """ + super().__init__( + analytics_enabled=analytics_enabled, + mode="interface", + title=title or "Gradio", + delete_cache=delete_cache, + fill_width=fill_width, + **kwargs, + ) + if isinstance(deep_link, str): + deep_link = DeepLinkButton(value=deep_link, render=False, interactive=False) + elif deep_link is True: + deep_link = DeepLinkButton(render=False, interactive=False) + if utils.get_space() and deep_link is None: + deep_link = DeepLinkButton(render=False, interactive=False) + if deep_link is False: + deep_link = None + self.deep_link = deep_link + self.time_limit = time_limit + self.stream_every = stream_every + self.api_name: str | None = api_name + self.api_description: str | None | Literal[False] = api_description + self.api_visibility = api_visibility + self.interface_type = InterfaceTypes.STANDARD + if (inputs is None or inputs == []) and (outputs is None or outputs == []): + raise ValueError("Must provide at least one of `inputs` or `outputs`") + elif outputs is None or outputs == []: + outputs = [] + self.interface_type = InterfaceTypes.INPUT_ONLY + elif inputs is None or inputs == []: + inputs = [] + self.interface_type = InterfaceTypes.OUTPUT_ONLY + if additional_inputs is None: + self.additional_input_components = [] + else: + if not isinstance(additional_inputs, Sequence): + additional_inputs = [additional_inputs] + self.additional_input_components = [ + get_component_instance(i, unrender=True) # type: ignore + for i in additional_inputs # type: ignore + ] + + if not isinstance(inputs, (Sequence, Component)): + raise TypeError( + f"inputs must be a string, list, or Component, not {inputs}" + ) + if not isinstance(outputs, (Sequence, Component)): + raise TypeError( + f"outputs must be a string, list, or Component, not {outputs}" + ) + + if isinstance(inputs, (str, Component)): + inputs = [inputs] + if isinstance(outputs, (str, Component)): + outputs = [outputs] + + self.cache_examples = cache_examples + self.cache_mode: Literal["eager", "lazy"] | None = cache_mode + self.preload_example = preload_example + + self.main_input_components = [ + get_component_instance(i, unrender=True) for i in inputs + ] + self.input_components = ( + self.main_input_components + self.additional_input_components + ) + self.output_components = [ + get_component_instance(o, unrender=True) + for o in outputs # type: ignore + ] + + state_input_indexes = [ + idx + for idx, i in enumerate(self.input_components) + if i == "state" or isinstance(i, State) + ] + state_output_indexes = [ + idx + for idx, o in enumerate(self.output_components) + if o == "state" or isinstance(o, State) + ] + + if len(state_input_indexes) == 0 and len(state_output_indexes) == 0: + pass + elif len(state_input_indexes) != 1 or len(state_output_indexes) != 1: + raise ValueError( + "If using 'state', there must be exactly one state input and one state output." + ) + else: + state_input_index = state_input_indexes[0] + state_output_index = state_output_indexes[0] + if self.input_components[state_input_index] == "state": + default = utils.get_default_args(fn)[state_input_index] + state_variable = State(value=default) + else: + state_variable = self.input_components[state_input_index] + + self.input_components[state_input_index] = state_variable + self.output_components[state_output_index] = state_variable + + if cache_examples: + warnings.warn( + "Cache examples cannot be used with state inputs and outputs. " + "Setting cache_examples to False." + ) + self.cache_examples = False + + if additional_inputs_accordion is None: + self.additional_inputs_accordion_params = { + "label": "Additional Inputs", + "open": False, + } + elif isinstance(additional_inputs_accordion, str): + self.additional_inputs_accordion_params = { + "label": additional_inputs_accordion + } + elif isinstance(additional_inputs_accordion, Accordion): + self.additional_inputs_accordion_params = ( + additional_inputs_accordion.recover_kwargs( + additional_inputs_accordion.get_config() + ) + ) + else: + raise ValueError( + f"The `additional_inputs_accordion` parameter must be a string or gr.Accordion, not {type(additional_inputs_accordion)}" + ) + + for component in self.input_components + self.output_components: + if not (isinstance(component, Component)): + raise ValueError( + f"{component} is not a valid input/output component for Interface." + ) + + if len(self.input_components) == len(self.output_components): + same_components = [ + i is o + for i, o in zip( + self.input_components, self.output_components, strict=False + ) + ] + if all(same_components): + self.interface_type = InterfaceTypes.UNIFIED + + if self.interface_type in [ + InterfaceTypes.STANDARD, + InterfaceTypes.OUTPUT_ONLY, + ]: + for o in self.output_components: + if not isinstance(o, Component): + raise TypeError( + f"Output component must be a Component, not {type(o)}" + ) + if o.interactive is None: + # Unless explicitly otherwise specified, force output components to + # be non-interactive + o.interactive = False + + self.api_mode = _api_mode + self.fn = fn + self.validator = validator + self.fn_durations = [0, 0] + self.__name__ = getattr(fn, "__name__", "fn") + self.live = live + self.title = title + + self.simple_description = utils.remove_html_tags(description) + self.description = description + if article is not None: + article = utils.download_if_url(article) + self.article = article + + self.examples = examples + self.examples_per_page = examples_per_page + self.example_labels = example_labels + + if isinstance(submit_btn, Button): + self.submit_btn_parms = submit_btn.recover_kwargs(submit_btn.get_config()) + elif isinstance(submit_btn, str): + self.submit_btn_parms = { + "value": submit_btn, + "variant": "primary", + } + else: + raise ValueError( + f"The submit_btn parameter must be a gr.Button or string, not {type(submit_btn)}" + ) + + if isinstance(stop_btn, Button): + self.stop_btn_parms = stop_btn.recover_kwargs(stop_btn.get_config()) + elif isinstance(stop_btn, str): + self.stop_btn_parms = { + "value": stop_btn, + "variant": "stop", + "visible": False, + } + else: + raise ValueError( + f"The stop_btn parameter must be a gr.Button or string, not {type(stop_btn)}" + ) + + if clear_btn is None: + self.clear_btn_params = { + "visible": False, + "variant": "secondary", + } + elif isinstance(clear_btn, Button): + self.clear_btn_params = clear_btn.recover_kwargs(clear_btn.get_config()) + elif isinstance(clear_btn, str): + self.clear_btn_params = { + "value": clear_btn, + "variant": "secondary", + } + else: + raise ValueError( + f"The clear_btn parameter must be a gr.Button, a string, or None, not {type(clear_btn)}" + ) + + self.simple_server = None + + # For flagging_mode: (1) first check for `flagging_mode` parameter, + # (2) check for env variable, (3) default to "manual" + if flagging_mode is None: + self.flagging_mode = os.getenv("GRADIO_FLAGGING_MODE", "manual") + elif flagging_mode in ["manual", "never", "auto"]: + self.flagging_mode = flagging_mode + else: + raise ValueError( + "Invalid value for `flagging_mode` parameter." + "Must be: 'auto', 'manual', or 'never'." + ) + + if flagging_options is None: + self.flagging_options = [("Flag", None)] + elif not (isinstance(flagging_options, list)): + raise ValueError( + "flagging_options must be a list of strings or list of (string, string) tuples." + ) + elif all(isinstance(x, str) for x in flagging_options): + self.flagging_options = [(f"Flag as {x}", x) for x in flagging_options] + elif all(isinstance(x, tuple) for x in flagging_options): + self.flagging_options = flagging_options + else: + raise ValueError( + "flagging_options must be a list of strings or list of (string, string) tuples." + ) + + if flagging_callback is None: + flagging_callback = CSVLogger() + + self.flagging_callback = flagging_callback + self.flagging_dir = flagging_dir + self.show_progress: Literal["full", "hidden", "minimal"] = show_progress + + self.batch = batch + self.max_batch_size = max_batch_size + self.allow_duplication = allow_duplication + self.concurrency_limit: int | None | Literal["default"] = concurrency_limit + + self.share = None + self.share_url = None + self.local_url = None + + self.favicon_path = None + self.i18n_instance = None + + param_types = utils.get_type_hints(self.fn) + # param_names = inspect.getfullargspec(self.fn)[0] + param_names = [] + try: + param_names = inspect.getfullargspec(self.fn)[0] + if len(param_names) > 0 and inspect.ismethod(self.fn): + param_names = param_names[1:] + for param_name in param_names.copy(): + if utils.is_special_typed_parameter(param_name, param_types): + param_names.remove(param_name) + except (TypeError, ValueError): + param_names = utils.default_input_labels() + for component, param_name in zip( + self.input_components, param_names, strict=False + ): + if not isinstance(component, Component): + raise TypeError( + f"Input component must be a Component, not {type(component)}" + ) + if component.label is None: + component.label = param_name + for i, component in enumerate(self.output_components): + if not isinstance(component, Component): + raise TypeError( + f"Output component must be a Component, not {type(component)}" + ) + if component.label is None: + if len(self.output_components) == 1: + component.label = "output" + else: + component.label = f"output {i}" + + if self.flagging_mode != "never": + if self.interface_type == InterfaceTypes.UNIFIED: + self.flagging_callback.setup(self.input_components, self.flagging_dir) # type: ignore + elif self.interface_type == InterfaceTypes.INPUT_ONLY: + pass + else: + self.flagging_callback.setup( + self.input_components + self.output_components, + self.flagging_dir, # type: ignore + ) + + # Render the Gradio UI + with self: + if self.deep_link: + self.deep_link.activate() + self.render_title_description() + + _submit_btn, _clear_btn, _stop_btn, flag_btns, duplicate_btn = ( + None, + None, + None, + None, + None, + ) # type: ignore + input_component_column = None + + with Row(): + if self.interface_type in [ + InterfaceTypes.STANDARD, + InterfaceTypes.INPUT_ONLY, + InterfaceTypes.UNIFIED, + ]: + ( + _submit_btn, + _clear_btn, + _stop_btn, + flag_btns, + input_component_column, + ) = self.render_input_column() # type: ignore + if self.interface_type in [ + InterfaceTypes.STANDARD, + InterfaceTypes.OUTPUT_ONLY, + ]: + ( + _submit_btn_out, + _clear_btn_2_out, + duplicate_btn, + _stop_btn_2_out, + flag_btns_out, + ) = self.render_output_column(_submit_btn) + _submit_btn = _submit_btn or _submit_btn_out + _clear_btn = _clear_btn or _clear_btn_2_out + _stop_btn = _stop_btn or _stop_btn_2_out + flag_btns = flag_btns or flag_btns_out + + if _clear_btn is None: + raise RenderError("Clear button not rendered") + + _submit_event = self.attach_submit_events(_submit_btn, _stop_btn) + self.attach_clear_events(_clear_btn, input_component_column) + if duplicate_btn is not None: + duplicate_btn.activate() + + self.attach_flagging_events(flag_btns, _clear_btn, _submit_event) + if _submit_event and self.deep_link: + _submit_event.then( + lambda: DeepLinkButton(interactive=True), + inputs=None, + outputs=[self.deep_link], + js=True, + api_visibility="undocumented", + ) + self.render_examples() + self.render_article() + + self.config = self.get_config_file() + + def render_title_description(self) -> None: + if self.title: + Markdown( + f"

{self.title}

" + ) + if self.description: + Markdown(self.description) + + def render_flag_btns(self) -> list[Button]: + return [Button(label) for label, _ in self.flagging_options] + + def render_input_column( + self, + ) -> tuple[ + Button | None, + ClearButton | None, + Button | None, + list[Button] | None, + Column, + ]: + _submit_btn, _clear_btn, _stop_btn, flag_btns = None, None, None, None + + with Column(): + input_component_column = Column() + with input_component_column: + for component in self.main_input_components: + component.render() + if self.additional_input_components: + with Accordion(**self.additional_inputs_accordion_params): # type: ignore + for component in self.additional_input_components: + component.render() + with Row(): + if self.interface_type in [ + InterfaceTypes.STANDARD, + InterfaceTypes.INPUT_ONLY, + ]: + _clear_btn = ClearButton(**self.clear_btn_params) # type: ignore + if not self.live: + if ( + self.deep_link + and self.interface_type == InterfaceTypes.INPUT_ONLY + ): + self.deep_link.render() + _submit_btn = Button(**self.submit_btn_parms) # type: ignore + # Stopping jobs only works if the queue is enabled + # We don't know if the queue is enabled when the interface + # is created. We use whether a generator function is provided + # as a proxy of whether the queue will be enabled. + # Using a generator function without the queue will raise an error. + if inspect.isgeneratorfunction( + self.fn + ) or inspect.isasyncgenfunction(self.fn): + _stop_btn = Button(**self.stop_btn_parms) # type: ignore # type: ignore + elif self.interface_type == InterfaceTypes.UNIFIED: + _clear_btn = ClearButton(**self.clear_btn_params) # type: ignore + _submit_btn = Button(**self.submit_btn_parms) # type: ignore + if self.deep_link: + self.deep_link.render() + if ( + inspect.isgeneratorfunction(self.fn) + or inspect.isasyncgenfunction(self.fn) + ) and not self.live: + _stop_btn = Button(**self.stop_btn_parms) # type: ignore + if self.flagging_mode == "manual": + flag_btns = self.render_flag_btns() + elif self.flagging_mode == "auto": + flag_btns = [_submit_btn] + return ( + _submit_btn, + _clear_btn, + _stop_btn, + flag_btns, + input_component_column, + ) + + def render_output_column( + self, + _submit_btn_in: Button | None, + ) -> tuple[ + Button | None, + ClearButton | None, + DuplicateButton | None, + Button | None, + list | None, + ]: + _submit_btn = _submit_btn_in + _clear_btn, duplicate_btn, flag_btns, _stop_btn = ( + None, + None, + None, + None, + ) + + with Column(): + for component in self.output_components: # type: ignore + if not (isinstance(component, State)): + component.render() + with Row(): + if self.deep_link: + self.deep_link.render() + if self.interface_type == InterfaceTypes.OUTPUT_ONLY: + _clear_btn = ClearButton(**self.clear_btn_params) # type: ignore + _submit_btn = Button("Generate", variant="primary") + if ( + inspect.isgeneratorfunction(self.fn) + or inspect.isasyncgenfunction(self.fn) + ) and not self.live: + # Stopping jobs only works if the queue is enabled + # We don't know if the queue is enabled when the interface + # is created. We use whether a generator function is provided + # as a proxy of whether the queue will be enabled. + # Using a generator function without the queue will raise an error. + _stop_btn = Button(**self.stop_btn_parms) # type: ignore + if self.flagging_mode == "manual": + flag_btns = self.render_flag_btns() + elif self.flagging_mode == "auto": + if _submit_btn is None: + raise RenderError("Submit button not rendered") + flag_btns = [_submit_btn] + + if self.allow_duplication: + duplicate_btn = DuplicateButton(scale=1, size="lg", _activate=False) + + return ( + _submit_btn, + _clear_btn, + duplicate_btn, + _stop_btn, + flag_btns, + ) + + def render_article(self): + if self.article: + Markdown(self.article) + + def attach_submit_events( + self, _submit_btn: Button | None, _stop_btn: Button | None + ) -> Dependency: + if self.live: + if self.interface_type == InterfaceTypes.OUTPUT_ONLY: + if _submit_btn is None: + raise RenderError("Submit button not rendered") + super().load(self.fn, None, self.output_components) + # For output-only interfaces, the user probably still want a "generate" + # button even if the Interface is live + return _submit_btn.click( + self.fn, + None, + self.output_components, + api_name=self.api_name, + api_visibility=self.api_visibility, + api_description=self.api_description, + preprocess=not (self.api_mode), + postprocess=not (self.api_mode), + batch=self.batch, + max_batch_size=self.max_batch_size, + validator=self.validator, + ) + else: + events: list[Callable] = [] + streaming_event = False + for component in self.input_components: # type: ignore + if component.has_event("stream") and component.streaming: # type: ignore + events.append(component.stream) # type: ignore + streaming_event = True + elif component.has_event("change"): + events.append(component.change) # type: ignore + return on( + events, + self.fn, + self.input_components, + self.output_components, + api_name=self.api_name, + api_description=self.api_description, + api_visibility=self.api_visibility, + preprocess=not (self.api_mode), + postprocess=not (self.api_mode), + show_progress="hidden" if streaming_event else self.show_progress, + trigger_mode="always_last" if not streaming_event else "multiple", + time_limit=self.time_limit, + stream_every=self.stream_every, + validator=self.validator, + ) + else: + if _submit_btn is None: + raise RenderError("Submit button not rendered") + fn = self.fn + extra_output = [] + + triggers = [_submit_btn.click] + [ + component.submit # type: ignore + for component in self.input_components # type: ignore + if component.has_event(Events.submit) + ] + + for component in self.input_components: # type: ignore + if getattr(component, "streaming", None): + warnings.warn( + "Streaming components are only supported in live interfaces." + ) + + if _stop_btn: + extra_output = [_submit_btn, _stop_btn] + + async def cleanup(): + return [Button(visible=True), Button(visible=False)] + + predict_event = on( + triggers, + utils.async_lambda( + lambda: ( + Button(visible=False), + Button(visible=True), + ) + ), + inputs=None, + outputs=[_submit_btn, _stop_btn], + queue=False, + api_visibility="undocumented", + ).then( + self.fn, + self.input_components, + self.output_components, + api_name=self.api_name, + api_visibility=self.api_visibility, + api_description=self.api_description, + scroll_to_output=True, + preprocess=not (self.api_mode), + postprocess=not (self.api_mode), + batch=self.batch, + max_batch_size=self.max_batch_size, + concurrency_limit=self.concurrency_limit, + show_progress=self.show_progress, + validator=self.validator, + ) + + final_event = predict_event.then( + cleanup, + inputs=None, + outputs=extra_output, # type: ignore + queue=False, + api_visibility="undocumented", + ) + + _stop_btn.click( + cleanup, + inputs=None, + outputs=[_submit_btn, _stop_btn], + cancels=predict_event, + queue=False, + api_visibility="undocumented", + ) + return final_event + else: + return on( + triggers, + fn, + self.input_components, + self.output_components, + api_name=self.api_name, + api_visibility=self.api_visibility, + api_description=self.api_description, + scroll_to_output=True, + preprocess=not (self.api_mode), + postprocess=not (self.api_mode), + batch=self.batch, + max_batch_size=self.max_batch_size, + concurrency_limit=self.concurrency_limit, + show_progress=self.show_progress, + validator=self.validator, + ) + + def attach_clear_events( + self, + _clear_btn: ClearButton, + input_component_column: Column | None, + ): + _clear_btn.add(self.input_components + self.output_components) # type: ignore + _clear_btn.click( + None, + [], + ([input_component_column] if input_component_column else []), # type: ignore + js=f"""() => { + json.dumps( + [{"variant": None, "visible": True, "__type__": "update"}] + if self.interface_type + in [ + InterfaceTypes.STANDARD, + InterfaceTypes.INPUT_ONLY, + InterfaceTypes.UNIFIED, + ] + else [] + ) + } + """, + ) + + def attach_flagging_events( + self, + flag_btns: list[Button] | None, + _clear_btn: ClearButton, + _submit_event: Dependency, + ): + if not ( + flag_btns + and self.interface_type + in ( + InterfaceTypes.STANDARD, + InterfaceTypes.OUTPUT_ONLY, + InterfaceTypes.UNIFIED, + ) + ): + return + + if self.flagging_mode == "auto": + flag_method = FlagMethod( + self.flagging_callback, "", None, visual_feedback=False + ) + _submit_event.success( + flag_method, + inputs=self.input_components + self.output_components, # type: ignore + outputs=None, + preprocess=False, + queue=False, + api_visibility="undocumented", + ) + return + + if self.interface_type == InterfaceTypes.UNIFIED: + flag_components = self.input_components # type: ignore + else: + flag_components = self.input_components + self.output_components # type: ignore + + for flag_btn, (label, value) in zip( + flag_btns, self.flagging_options, strict=False + ): + if value is not None and not isinstance(value, str): + raise TypeError( + f"Flagging option value must be a string, not {value!r}" + ) + flag_method = FlagMethod(self.flagging_callback, label, value) + flag_btn.click( + utils.async_lambda( + lambda: Button(value="Saving...", interactive=False) + ), + None, + flag_btn, + queue=False, + api_visibility="undocumented", + ) + flag_btn.click( + flag_method, + inputs=flag_components, + outputs=flag_btn, + preprocess=False, + queue=False, + api_visibility="undocumented", + ) + _clear_btn.click( + utils.async_lambda(flag_method.reset), + None, + flag_btn, + queue=False, + api_visibility="undocumented", + ) + + def render_examples(self): + if self.examples: + non_state_inputs = [ + c + for c in self.input_components # type: ignore + if not isinstance(c, State) # type: ignore + ] + non_state_outputs = [ + c + for c in self.output_components # type: ignore + if not isinstance(c, State) # type: ignore + ] + self.examples_handler = Examples( + examples=self.examples, + inputs=non_state_inputs, + outputs=non_state_outputs, + fn=self.fn, + cache_examples=self.cache_examples, + cache_mode=self.cache_mode, + examples_per_page=self.examples_per_page, + _api_mode=self.api_mode or False, # type: ignore + batch=self.batch, + example_labels=self.example_labels, + preload=self.preload_example, + ) + if self.deep_link and self.examples_handler.cache_event: + self.examples_handler.cache_event.then( + lambda: DeepLinkButton(interactive=True), + inputs=None, + outputs=[self.deep_link], + js=True, + api_visibility="undocumented", + ) + + def __str__(self): + return self.__repr__() + + def __repr__(self): + repr = f"Gradio Interface for: {self.__name__}" + repr += f"\n{'-' * len(repr)}" + repr += "\ninputs:" + for component in self.input_components: # type: ignore + repr += f"\n|-{component}" + repr += "\noutputs:" + for component in self.output_components: # type: ignore + repr += f"\n|-{component}" + return repr + + +@document() +class TabbedInterface(Blocks): + """ + A TabbedInterface is created by providing a list of Interfaces or Blocks, each of which gets + rendered in a separate tab. Only the components from the Interface/Blocks will be rendered in the tab. + + Demos: tabbed_interface_lite + """ + + def __init__( + self, + interface_list: Sequence[Blocks], + tab_names: list[str] | None = None, + title: str | None = None, + analytics_enabled: bool | None = None, + ): + """ + Parameters: + interface_list: A list of Interfaces (or Blocks) to be rendered in the tabs. + tab_names: A list of tab names. If None, the tab names will be "Tab 1", "Tab 2", etc. + title: The tab title to display when this demo is opened in a browser window. + analytics_enabled: Whether to allow basic telemetry. If None, will use GRADIO_ANALYTICS_ENABLED environment variable or default to True. + Returns: + a Gradio Tabbed Interface for the given interfaces + """ + super().__init__( + title=title or "Gradio", + analytics_enabled=analytics_enabled, + mode="tabbed_interface", + fill_height=True, + ) + if tab_names is None: + tab_names = [f"Tab {i}" for i in range(len(interface_list))] + with self: + if title: + Markdown( + f"

{title}

" + ) + with Tabs(): + for interface, tab_name in zip(interface_list, tab_names, strict=False): + with Tab( + label=tab_name, + scale=1 if interface.fill_height else None, + ): + interface.render() + + +def close_all(verbose: bool = True) -> None: + for instance in Blocks.get_instances(): + if instance.is_running: + instance.close(verbose) diff --git a/gradio/ipython_ext.py b/gradio/ipython_ext.py new file mode 100644 index 0000000..0c37bbc --- /dev/null +++ b/gradio/ipython_ext.py @@ -0,0 +1,89 @@ +try: + from IPython.core.magic import ( # ty: ignore[unresolved-import] + needs_local_scope, + register_cell_magic, + ) + from IPython.core.magic_arguments import ( # ty: ignore[unresolved-import] + argument, + magic_arguments, + parse_argstring, + ) +except ImportError: + pass + +import gradio as gr +from gradio.routes import App +from gradio.utils import BaseReloader + + +class CellIdTracker: + """Determines the most recently run cell in the notebook. + + Needed to keep track of which demo the user is updating. + """ + + def __init__(self, ipython): + ipython.events.register("pre_run_cell", self.pre_run_cell) + self.shell = ipython + self.current_cell: str = "" + + def pre_run_cell(self, info): + self._current_cell = info.cell_id + + +class JupyterReloader(BaseReloader): + """Swap a running blocks class in a notebook with the latest cell contents.""" + + def __init__(self, ipython) -> None: + super().__init__() + self._cell_tracker = CellIdTracker(ipython) + self._running: dict[str, gr.Blocks] = {} + + @property + def current_cell(self): + return self._cell_tracker.current_cell + + @property + def running_app(self) -> App: + if not self.running_demo.server: + raise RuntimeError("Server not running") + return self.running_demo.server.running_app # type: ignore + + @property + def running_demo(self): + return self._running[self.current_cell] + + def demo_tracked(self) -> bool: + return self.current_cell in self._running + + def track(self, demo: gr.Blocks): + self._running[self.current_cell] = demo + + +def load_ipython_extension(ipython): + reloader = JupyterReloader(ipython) + + @magic_arguments() # type: ignore + @argument("--demo-name", default="demo", help="Name of gradio blocks instance.") # type: ignore + @argument( # type: ignore + "--share", + default=False, + const=True, + nargs="?", + help="Whether to launch with sharing. Will slow down reloading.", + ) + @register_cell_magic # type: ignore + @needs_local_scope # type: ignore + def blocks(line, cell, local_ns): + """Launch a demo defined in a cell in reload mode.""" + + args = parse_argstring(blocks, line) # type: ignore + + exec(cell, None, local_ns) + demo: gr.Blocks = local_ns[args.demo_name] + if not reloader.demo_tracked(): + demo.launch(share=args.share) + reloader.track(demo) + else: + reloader.swap_blocks(demo) + return reloader.running_demo.artifact diff --git a/gradio/layouts/__init__.py b/gradio/layouts/__init__.py new file mode 100644 index 0000000..0d63199 --- /dev/null +++ b/gradio/layouts/__init__.py @@ -0,0 +1,24 @@ +from .accordion import Accordion +from .column import Column +from .draggable import Draggable +from .form import Form +from .group import Group +from .row import Row +from .sidebar import Sidebar +from .tabs import Tab, TabItem, Tabs +from .walkthrough import Step, Walkthrough + +__all__ = [ + "Accordion", + "Column", + "Draggable", + "Form", + "Row", + "Group", + "Tabs", + "Tab", + "TabItem", + "Sidebar", + "Walkthrough", + "Step", +] diff --git a/gradio/layouts/accordion.py b/gradio/layouts/accordion.py new file mode 100644 index 0000000..429b7be --- /dev/null +++ b/gradio/layouts/accordion.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +from gradio_client.documentation import document + +from gradio.blocks import BlockContext +from gradio.component_meta import ComponentMeta +from gradio.events import Events +from gradio.i18n import I18nData + +if TYPE_CHECKING: + pass + + +@document() +class Accordion(BlockContext, metaclass=ComponentMeta): + """ + Accordion is a layout element which can be toggled to show/hide the contained content. + Example: + with gr.Accordion("See Details"): + gr.Markdown("lorem ipsum") + Guides: controlling-layout + """ + + EVENTS = [Events.expand, Events.collapse] + + def __init__( + self, + label: str | I18nData | None = None, + *, + open: bool = True, + 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 = None, + ): + """ + Parameters: + label: name of accordion section. + open: if True, accordion is open by default. + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, this layout will not be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + self.label = label + self.open = open + BlockContext.__init__( + self, + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) diff --git a/gradio/layouts/column.py b/gradio/layouts/column.py new file mode 100644 index 0000000..6880069 --- /dev/null +++ b/gradio/layouts/column.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import warnings +from typing import Literal + +from gradio_client.documentation import document + +from gradio.blocks import BlockContext +from gradio.component_meta import ComponentMeta + + +@document() +class Column(BlockContext, metaclass=ComponentMeta): + """ + Column is a layout element within Blocks that renders all children vertically. The widths of columns can be set through the `scale` and `min_width` parameters. + If a certain scale results in a column narrower than min_width, the min_width parameter will win. + Example: + with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(scale=1): + text1 = gr.Textbox() + text2 = gr.Textbox() + with gr.Column(scale=4): + btn1 = gr.Button("Button 1") + btn2 = gr.Button("Button 2") + Guides: controlling-layout + """ + + EVENTS = [] + + def __init__( + self, + *, + scale: int = 1, + min_width: int = 320, + variant: Literal["default", "panel", "compact"] = "default", + visible: bool | Literal["hidden"] = True, + elem_id: str | None = None, + elem_classes: list[str] | str | None = None, + render: bool = True, + show_progress: bool = False, + key: int | str | tuple[int | str, ...] | None = None, + preserved_by_key: list[str] | str | None = None, + ): + """ + Parameters: + scale: relative width compared to adjacent Columns. For example, if Column A has scale=2, and Column B has scale=1, A will be twice as wide as B. + min_width: minimum pixel width of Column, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in a column narrower than min_width, the min_width parameter will be respected first. + variant: column type, 'default' (no background), 'panel' (gray background color and rounded corners), or 'compact' (rounded corners and no internal gap). + visible: If False, column will be hidden. + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + show_progress: If True, shows progress animation when being updated. + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + if scale != round(scale): + warnings.warn( + f"'scale' value should be an integer. Using {scale} will cause issues." + ) + + self.scale = scale + self.min_width = min_width + self.variant = variant + if variant == "compact": + self.allow_expected_parents = False + self.show_progress = show_progress + BlockContext.__init__( + self, + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) diff --git a/gradio/layouts/draggable.py b/gradio/layouts/draggable.py new file mode 100644 index 0000000..afde12c --- /dev/null +++ b/gradio/layouts/draggable.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import Literal + +from gradio_client.documentation import document + +from gradio.blocks import BlockContext +from gradio.component_meta import ComponentMeta + + +@document() +class Draggable(BlockContext, metaclass=ComponentMeta): + """ + Draggable is a layout element within Blocks that renders children with drag and drop functionality. + A user can reorder children by dragging them around and snapping them into place. If a child is a + layout (e.g. gr.Row, gr.Group), all the components in the child layout will drag together. + + Demos: draggable_dashboard + """ + + EVENTS = [] + + def __init__( + self, + *, + orientation: Literal["row", "column"] = "column", + 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 = None, + ): + """ + Parameters: + orientation: The direction in which children are arranged. 'row' arranges children horizontally, 'column' arranges them vertically. + visible: If False, draggable container will be hidden. + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, this layout will not be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + self.orientation = orientation + + super().__init__( + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) diff --git a/gradio/layouts/form.py b/gradio/layouts/form.py new file mode 100644 index 0000000..f7b9683 --- /dev/null +++ b/gradio/layouts/form.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from gradio.blocks import BlockContext, Blocks +from gradio.component_meta import ComponentMeta +from gradio.layouts.column import Column +from gradio.layouts.row import Row + +if TYPE_CHECKING: + from gradio.blocks import Block + + +class Form(BlockContext, metaclass=ComponentMeta): + EVENTS = [] + + def __init__( + self, + *, + scale: int = 0, + min_width: int = 0, + render: bool = True, + key: int | str | tuple[int | str, ...] | None = None, + preserved_by_key: list[str] | str | None = None, + ): + """ + Parameters: + scale: relative width compared to adjacent Columns. For example, if Column A has scale=2, and Column B has scale=1, A will be twice as wide as B. + min_width: minimum pixel width of Column, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in a column narrower than min_width, the min_width parameter will be respected first. + render: If False, this layout will not be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + self.scale = scale + self.min_width = min_width + BlockContext.__init__( + self, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) + + def add_child(self, child: Block): + if isinstance(self.parent, Row): + scale = getattr(child, "scale", None) + self.scale += 1 if scale is None else scale + self.min_width += getattr(child, "min_width", 0) or 0 + elif ( + isinstance(self.parent, Column) + and isinstance(self.parent.parent, Row) + and self.parent.parent.equal_height + ): + scale = getattr(child, "scale", None) + self.scale += 1 if scale is None else scale + elif isinstance(self.parent, Blocks) and self.parent.fill_height: + scale = getattr(child, "scale", None) + self.scale += 0 if scale is None else scale + BlockContext.add_child(self, child) diff --git a/gradio/layouts/group.py b/gradio/layouts/group.py new file mode 100644 index 0000000..7a72aaa --- /dev/null +++ b/gradio/layouts/group.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from typing import Literal + +from gradio_client.documentation import document + +from gradio.blocks import BlockContext +from gradio.component_meta import ComponentMeta + + +@document() +class Group(BlockContext, metaclass=ComponentMeta): + """ + Group is a layout element within Blocks which groups together children so that + they do not have any padding or margin between them. + Example: + with gr.Group(): + gr.Textbox(label="First") + gr.Textbox(label="Last") + """ + + EVENTS = [] + + def __init__( + self, + *, + 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 = None, + ): + """ + Parameters: + visible: If False, group will be hidden. + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, this layout will not be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + BlockContext.__init__( + self, + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) diff --git a/gradio/layouts/row.py b/gradio/layouts/row.py new file mode 100644 index 0000000..34ae76a --- /dev/null +++ b/gradio/layouts/row.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import warnings +from typing import Literal + +from gradio_client.documentation import document + +from gradio.blocks import BlockContext +from gradio.component_meta import ComponentMeta + + +@document() +class Row(BlockContext, metaclass=ComponentMeta): + """ + Row is a layout element within Blocks that renders all children horizontally. + Example: + with gr.Blocks() as demo: + with gr.Row(): + gr.Image("lion.jpg", scale=2) + gr.Image("tiger.jpg", scale=1) + demo.launch() + Guides: controlling-layout + """ + + EVENTS = [] + + def __init__( + self, + *, + variant: Literal["default", "panel", "compact"] = "default", + visible: bool | Literal["hidden"] = True, + elem_id: str | None = None, + elem_classes: list[str] | str | None = None, + scale: int | None = None, + render: bool = True, + height: int | str | None = None, + max_height: int | str | None = None, + min_height: int | str | None = None, + equal_height: bool = False, + show_progress: bool = False, + key: int | str | tuple[int | str, ...] | None = None, + preserved_by_key: list[str] | str | None = None, + ): + """ + Parameters: + variant: row type, 'default' (no background), 'panel' (gray background color and rounded corners), or 'compact' (rounded corners and no internal gap). + visible: If False, row will be hidden. + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. + scale: relative height compared to adjacent elements. 1 or greater indicates the Row will expand in height, and any child columns will also expand to fill the height. + render: If False, this layout will not be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + height: The height of the row, specified in pixels if a number is passed, or in CSS units if a string is passed. If content exceeds the height, the row will scroll vertically. If not set, the row will expand to fit the content. + max_height: The maximum height of the row, specified in pixels if a number is passed, or in CSS units if a string is passed. If content exceeds the height, the row will scroll vertically. If content is shorter than the height, the row will shrink to fit the content. Will not have any effect if `height` is set and is smaller than `max_height`. + min_height: The minimum height of the row, specified in pixels if a number is passed, or in CSS units if a string is passed. If content exceeds the height, the row will expand to fit the content. Will not have any effect if `height` is set and is larger than `min_height`. + equal_height: If True, makes every child element have equal height + show_progress: If True, shows progress animation when being updated. + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + self.variant = variant + self.equal_height = equal_height + if variant == "compact": + self.allow_expected_parents = False + self.show_progress = show_progress + self.height = height + self.max_height = max_height + self.min_height = min_height + if scale and scale != round(scale): + warnings.warn( + f"'scale' value should be an integer. Using {scale} will cause issues." + ) + + self.scale = scale + + BlockContext.__init__( + self, + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) + + @staticmethod + def update( + visible: bool | None = None, + ): + return { + "visible": visible, + "__type__": "update", + } diff --git a/gradio/layouts/sidebar.py b/gradio/layouts/sidebar.py new file mode 100644 index 0000000..349f13d --- /dev/null +++ b/gradio/layouts/sidebar.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from typing import Literal + +from gradio_client.documentation import document + +from gradio.blocks import BlockContext +from gradio.component_meta import ComponentMeta +from gradio.events import Events +from gradio.i18n import I18nData + + +@document() +class Sidebar(BlockContext, metaclass=ComponentMeta): + """ + Sidebar is a collapsible panel that renders child components on the left side of the screen within a Blocks layout. + Example: + with gr.Blocks() as demo: + with gr.Sidebar(): + gr.Textbox() + gr.Button() + Guides: controlling-layout + """ + + EVENTS = [Events.expand, Events.collapse] + + def __init__( + self, + label: str | I18nData | None = None, + *, + open: bool = True, + visible: bool | Literal["hidden"] = True, + elem_id: str | None = None, + elem_classes: list[str] | str | None = None, + render: bool = True, + width: int | str = 320, + position: Literal["left", "right"] = "left", + key: int | str | tuple[int | str, ...] | None = None, + preserved_by_key: list[str] | str | None = None, + ): + """ + Parameters: + label: name of the sidebar. Not displayed to the user. + open: if True, sidebar is open by default. + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, this layout will not be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + width: The width of the sidebar, specified in pixels if a number is passed, or in CSS units if a string is passed. + position: The position of the sidebar in the layout, either "left" or "right". Defaults to "left". + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + self.label = label + self.open = open + self.width = width + self.position = position + BlockContext.__init__( + self, + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) diff --git a/gradio/layouts/tabs.py b/gradio/layouts/tabs.py new file mode 100644 index 0000000..834469d --- /dev/null +++ b/gradio/layouts/tabs.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +import warnings +from typing import Literal + +from gradio_client.documentation import document + +from gradio.blocks import BlockContext +from gradio.component_meta import ComponentMeta +from gradio.events import EventListener, Events +from gradio.i18n import I18nData + + +class Tabs(BlockContext, metaclass=ComponentMeta): + """ + Tabs is a layout element within Blocks that can contain multiple "Tab" Components. + Guides: controlling-layout + """ + + EVENTS = [Events.change, Events.select] + + def __init__( + self, + *, + selected: int | str | 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 = None, + ): + """ + Parameters: + selected: The currently selected tab. Must correspond to an id passed to the one of the child TabItems. Defaults to the first TabItem. + visible: If False, Tabs will be hidden. + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, this layout will not be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + BlockContext.__init__( + self, + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) + self.selected = selected + + def __exit__(self, exc_type=None, *args): + super().__exit__(exc_type, *args) + if exc_type is not None: + return + for child in self.children: + if isinstance(child, Tab): + continue + # Invisible utility components (gr.State, gr.BrowserState, gr.Timer, ...) + # have no DOM and never cause the IntersectionObserver crash this + # validation is guarding against. They are conventionally marked by + # `breaks_grouping() == False`, the same flag fill_expected_parents + # already uses to allow them to live anywhere in the tree. + if not child.breaks_grouping(): + continue + # Surface the component(s) the user actually wrote rather than a + # gradio-generated auto-wrap (e.g. gr.Form grouping consecutive + # FormComponents into a single wrapper with multiple children). + if ( + isinstance(child, BlockContext) + and not getattr(child, "is_rendered", True) + and child.children + ): + names = ", ".join(f"gr.{type(c).__name__}()" for c in child.children) + else: + names = f"gr.{type(child).__name__}()" + warnings.warn( + f"gr.Tabs() can only contain gr.Tab() (or gr.TabItem()) components as direct children, " + f"but received {names}. Wrap inside a gr.Tab(...) block to fix this.", + UserWarning, + stacklevel=2, + ) + return + + +@document() +class Tab(BlockContext, metaclass=ComponentMeta): + """ + Tab (or its alias TabItem) is a layout element. Components defined within the Tab will be visible when this tab is selected tab. + Example: + with gr.Blocks() as demo: + with gr.Tab("Lion"): + gr.Image("lion.jpg") + gr.Button("New Lion") + with gr.Tab("Tiger"): + gr.Image("tiger.jpg") + gr.Button("New Tiger") + Guides: controlling-layout + """ + + EVENTS = [ + EventListener( + "select", + callback=lambda block: setattr(block, "_selectable", True), + doc="Event listener for when the user selects the Tab. Uses event data gradio.SelectData to carry `value` referring to the label of the Tab, and `selected` to refer to state of the Tab. See https://www.gradio.app/main/docs/gradio/eventdata documentation for more details.", + ) + ] + + def __init__( + self, + label: str | I18nData | None = None, + visible: bool | Literal["hidden"] = True, + interactive: bool = True, + *, + id: int | str | None = None, + elem_id: str | None = None, + elem_classes: list[str] | str | None = None, + scale: int | None = None, + render: bool = True, + key: int | str | tuple[int | str, ...] | None = None, + preserved_by_key: list[str] | str | None = None, + render_children: bool = False, + ): + """ + Parameters: + label: The visual label for the tab + id: An optional identifier for the tab, required if you wish to control the selected tab from a predict function. + elem_id: An optional string that is assigned as the id of the
containing the contents of the Tab layout. The same string followed by "-button" is attached to the Tab button. Can be used for targeting CSS styles. + elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, this layout will not be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + scale: relative size compared to adjacent elements. 1 or greater indicates the Tab will expand in size. + visible: If False, Tab will be hidden. + interactive: If False, Tab will not be clickable. + render_children: If True, the children of this Tab will be rendered on the page (but hidden) when the Tab is visible but inactive. This can be useful if you want to ensure that any components (e.g. videos or audio) within the Tab are pre-loaded before the user clicks on the Tab. + """ + BlockContext.__init__( + self, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) + self.label = label + self.id = id + self.visible = visible + self.scale = scale + self.interactive = interactive + self.render_children = render_children + + def get_expected_parent(self) -> type[Tabs]: + return Tabs + + def get_block_name(self): + return "tabitem" + + +TabItem = Tab diff --git a/gradio/layouts/walkthrough.py b/gradio/layouts/walkthrough.py new file mode 100644 index 0000000..c528850 --- /dev/null +++ b/gradio/layouts/walkthrough.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from gradio_client.documentation import document + +from gradio.blocks import BlockContext +from gradio.component_meta import ComponentMeta +from gradio.events import Events +from gradio.i18n import I18nData + + +@document() +class Walkthrough(BlockContext, metaclass=ComponentMeta): + """ + Walkthrough is a layout element within Blocks that can contain multiple "Step" Components, which can be used to create a step-by-step workflow. + Example: + with gr.Walkthrough(selected=1) as walkthrough: + with gr.Step("Step 1", id=1): + btn = gr.Button("go to Step 2") + btn.click(lambda: gr.Walkthrough(selected=2), outputs=walkthrough) + with gr.Step("Step 2", id=2): + txt = gr.Textbox("Welcome to Step 2") + + Guides: controlling-layout + Demos: walkthrough + """ + + EVENTS = [Events.change, Events.select] + + def __init__( + self, + *, + selected: int | None = None, + visible: 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 = None, + ): + """ + Parameters: + selected: The currently selected step. Must be a number corresponding to the step number. Defaults to the first step. + visible: If False, Walkthrough will be hidden. + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, this layout will not be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render. + preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor. + """ + BlockContext.__init__( + self, + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) + self.selected = selected + + def get_block_name(self): + return "walkthrough" + + +@document() +class Step(BlockContext, metaclass=ComponentMeta): + """ + Step is a layout element. A step is a single step in a step-by-step workflow. + """ + + EVENTS = [Events.select] + + def __init__( + self, + label: str | I18nData | None = None, + visible: bool = True, + interactive: bool = True, + *, + id: int | None = None, + elem_id: str | None = None, + elem_classes: list[str] | str | None = None, + scale: int | None = None, + render: bool = True, + key: int | str | tuple[int | str, ...] | None = None, + preserved_by_key: list[str] | str | None = None, + ): + """ + Parameters: + label: The visual label for the step + id: An optional numeric identifier for the step, required if you wish to control the selected step from a predict function. Must be a number. + elem_id: An optional string that is assigned as the id of the
containing the contents of the Step layout. The same string followed by "-button" is attached to the Step button. Can be used for targeting CSS styles. + elem_classes: An optional string or list of strings that are assigned as the class of this component in the HTML DOM. Can be used for targeting CSS styles. + render: If False, this layout will not be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + scale: relative size compared to adjacent elements. 1 or greater indicates the Step will expand in size. + visible: If False, Step will be hidden. + interactive: If False, Step will not be clickable. + """ + BlockContext.__init__( + self, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + preserved_by_key=preserved_by_key, + ) + self.label = label + self.id = id + self.visible = visible + self.scale = scale + self.interactive = interactive + + def get_expected_parent(self) -> type[Walkthrough]: + return Walkthrough + + def get_block_name(self): + return "walkthroughstep" diff --git a/gradio/mcp.py b/gradio/mcp.py new file mode 100644 index 0000000..6429b4d --- /dev/null +++ b/gradio/mcp.py @@ -0,0 +1,1632 @@ +import base64 +import contextlib +import copy +import html +import json +import os +import re +import tempfile +import warnings +from collections.abc import AsyncIterator, Sequence +from io import BytesIO +from pathlib import Path +from typing import TYPE_CHECKING, Any, Optional, cast +from urllib.parse import unquote, urlparse + +import gradio_client.utils as client_utils +import httpx +from anyio.to_thread import run_sync +from gradio_client import Client, handle_file +from gradio_client.utils import Status, StatusUpdate +from PIL import Image +from pydantic import AnyUrl +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import HTMLResponse, JSONResponse, Response +from starlette.routing import Mount, Route +from starlette.types import Receive, Scope, Send + +from gradio import processing_utils, route_utils, utils +from gradio.blocks import BlockFunction +from gradio.components import State +from gradio.route_utils import Header +from gradio.state_holder import SessionState + +if TYPE_CHECKING: + from mcp import types # noqa: F401 + from mcp.server import Server # noqa: F401 + from mcp.server.lowlevel.helper_types import ReadResourceContents # noqa: F401 + + from gradio.blocks import BlockContext, Blocks + from gradio.components import Component + + +DEFAULT_TEMP_DIR = os.environ.get("GRADIO_TEMP_DIR") or str( + Path(tempfile.gettempdir()) / "gradio" +) + + +# Landing page served on a plain browser GET to the MCP endpoint. The +# `__SERVER_URL__` / `__DOCS_URL__` (JSON-encoded, for the inline + + +""" + + +class GradioMCPServer: + """ + A class for creating an MCP server around a Gradio app. This class + requires `mcp` to be installed. + + Args: + blocks: The Blocks app to create the MCP server for. + """ + + # Imports are here to avoid needing to install `mcp` when not using this class. + # This way, we are able to export `gr.tool`, `gr.resource`, etc. to `__init__.py` + # without the user needing to have `mcp` installed. + try: + from mcp import types + from mcp.server import Server + from mcp.server.lowlevel.helper_types import ReadResourceContents + from mcp.server.sse import SseServerTransport + from mcp.server.streamable_http_manager import StreamableHTTPSessionManager + except ImportError: + pass + + def __init__(self, blocks: "Blocks"): + try: + import mcp # noqa: F401 + except ImportError as e: + raise ImportError( + "The `mcp` package is required to use the Gradio MCP integration. Please install it with the `mcp` extra: `pip install gradio[mcp]`." + ) from e + + self.blocks = blocks + self.api_info = self.blocks.get_api_info() + self.mcp_server = self.create_mcp_server() + self.root_path = "" + space_id = utils.get_space() + self.tool_prefix = space_id.split("/")[-1] + "_" if space_id else "" + self.tool_to_endpoint = self.get_tool_to_endpoint() + self.warn_about_state_inputs() + self._local_url: str | None = None + self._client_instance: Client | None = None + + manager = self.StreamableHTTPSessionManager( # type: ignore + app=self.mcp_server, json_response=False, stateless=True + ) + + async def handle_streamable_http( + scope: Scope, receive: Receive, send: Send + ) -> None: + path = scope.get("path", "") + if not path.endswith( + ( + "/gradio_api/mcp", + "/gradio_api/mcp/", + "/gradio_api/mcp/http", + "/gradio_api/mcp/http/", + ) + ): + response = Response( + content=f"Path '{path}' not found. The MCP HTTP transport is available at /gradio_api/mcp.", + status_code=404, + ) + await response(scope, receive, send) + return + + # MCP clients connect using the streamable HTTP transport, which + # requires an `Accept: text/event-stream` header. A plain browser + # GET (e.g. a user clicking the MCP link printed in the terminal) + # would otherwise receive a raw JSON-RPC "Not Acceptable" error, so + # serve a human-friendly landing page instead. + if scope.get("method") == "GET": + accept = b"" + for key, value in scope.get("headers", []): + if key.lower() == b"accept": + accept = value + break + if b"text/event-stream" not in accept: + server_url = self._server_url_from_scope(scope) + response = HTMLResponse(content=self._landing_page_html(server_url)) + await response(scope, receive, send) + return + + await manager.handle_request(scope, receive, send) + + @contextlib.asynccontextmanager + async def lifespan(app: Starlette) -> AsyncIterator[None]: # noqa: ARG001 + """Context manager for managing session manager lifecycle.""" + async with manager.run(): + try: + yield + finally: + pass + + self.lifespan = lifespan + self.manager = manager + self.handle_streamable_http = handle_streamable_http + + @property + def local_url(self) -> str | None: + return self._local_url + + @staticmethod + def _server_url_from_scope(scope: Scope) -> str: + """Reconstruct the public URL of the MCP endpoint from an ASGI scope. + + Honours ``X-Forwarded-Proto``/``X-Forwarded-Host`` so that the URL + shown on the landing page matches what the user typed into the browser + even when Gradio is behind a proxy (e.g. a Hugging Face Space). + """ + headers = {key.lower(): value for key, value in scope.get("headers", []) or []} + + scheme = scope.get("scheme", "http") + if (forwarded_proto := headers.get(b"x-forwarded-proto")) is not None: + scheme = forwarded_proto.decode("latin-1").split(",")[0].strip() + + host = b"" + if (forwarded_host := headers.get(b"x-forwarded-host")) is not None: + host = forwarded_host.split(b",")[0].strip() + elif (host_header := headers.get(b"host")) is not None: + host = host_header + host_str = host.decode("latin-1") or "localhost" + + # Normalise to the canonical streamable HTTP path with a trailing slash, + # matching the URL Gradio prints to the terminal at launch. + path = scope.get("path", "/gradio_api/mcp") + path = "/" + path.strip("/") + if path.endswith(("/http", "/http/")): + path = path[: path.rfind("/http")] + path = path.rstrip("/") + "/" + + return f"{scheme}://{host_str}{path}" + + @staticmethod + def _landing_page_html(server_url: str) -> str: + """HTML landing page shown when a browser navigates to the MCP endpoint. + + MCP clients connect using the streamable HTTP transport (which requires + an ``Accept: text/event-stream`` header). A plain browser ``GET`` would + otherwise receive a raw JSON-RPC "Not Acceptable" error, so we serve + this informational page instead. It surfaces the server URL and ready + to paste configuration snippets for popular MCP clients. + """ + docs_url = "https://www.gradio.app/guides/building-mcp-server-with-gradio" + server_url_js = json.dumps(server_url) + docs_url_js = json.dumps(docs_url) + parsed = urlparse(server_url) + path = parsed.path + if "/gradio_api/mcp" in path: + root_path = path.split("/gradio_api/mcp")[0] + else: + root_path = path.rstrip("/") + static_prefix = f"{root_path}/static" + return ( + _MCP_LANDING_PAGE_TEMPLATE.replace("__SERVER_URL__", server_url_js) + .replace("__DOCS_URL__", docs_url_js) + .replace("__SERVER_URL_TEXT__", html.escape(server_url)) + .replace("__STATIC_PREFIX__", static_prefix) + ) + + def get_route_path(self, request: Request) -> str: # type: ignore + """ + Gets the route path of the MCP server based on the incoming request. + Can be different depending on whether the request is coming from the MCP SSE transport or the HTTP transport. + """ + url = httpx.URL(str(request.url)) + url = url.copy_with(query=None) + url = str(url).rstrip("/") + if url.endswith("/gradio_api/mcp/messages"): + return "/gradio_api/mcp/messages" + else: + return "/gradio_api/mcp" + + def get_selected_tools_from_request(self) -> list[str] | None: + """ + Extract the selected tools from the request query parameters and return the full tool names (with the tool prefix). + Returns None if no tools parameter is specified (meaning all tools are available). + """ + context_request: Request | None = self.mcp_server.request_context.request + if context_request is None: + return None + query_params = dict(getattr(context_request, "query_params", {})) + if "tools" in query_params: + tools = query_params["tools"].split(",") + full_tool_names = [self.tool_prefix + tool for tool in tools] + return full_tool_names + return None + + @staticmethod + def valid_and_unique_tool_name( + tool_name: str, existing_tool_names: set[str] + ) -> str: + """ + Sanitizes a tool name to make it a valid MCP tool name (only + alphanumeric characters, underscores, <= 128 characters) + and is unique among the existing tool names. + """ + tool_name = re.sub(r"[^a-zA-Z0-9]", "_", tool_name) + tool_name = tool_name[:120] # Leave room for suffix if needed + tool_name_base = tool_name + suffix = 1 + while tool_name in existing_tool_names: + tool_name = tool_name_base + f"_{suffix}" + suffix += 1 + return tool_name + + def get_tool_to_endpoint(self) -> dict[str, str]: + """ + Gets all of the tools that are exposed by the Gradio app and also + creates a mapping from the tool names to the endpoint names in the API docs. + """ + tool_to_endpoint = {} + for endpoint_name in self.api_info["named_endpoints"]: + block_fn = self.get_block_fn_from_endpoint_name(endpoint_name) + if block_fn is None or block_fn.fn is None: + continue + fn_name = ( + getattr(block_fn.fn, "__name__", None) + or ( + hasattr(block_fn.fn, "__class__") + and getattr(block_fn.fn.__class__, "__name__", None) + ) + or endpoint_name.lstrip("/") + ) + tool_name = self.tool_prefix + fn_name + tool_name = self.valid_and_unique_tool_name( + tool_name, set(tool_to_endpoint.keys()) + ) + tool_to_endpoint[tool_name] = endpoint_name + return tool_to_endpoint + + def warn_about_state_inputs(self) -> None: + """ + Warn about tools that have gr.State inputs. + """ + for _, endpoint_name in self.tool_to_endpoint.items(): + block_fn = self.get_block_fn_from_endpoint_name(endpoint_name) + if block_fn and any(isinstance(input, State) for input in block_fn.inputs): + warnings.warn( + "This MCP server includes a tool that has a gr.State input, which will not be " + "updated between tool calls. The original, default value of the State will be " + "used each time." + ) + + def _get_or_create_client(self) -> Client: + if self._client_instance is None: + context_request: Request | None = self.mcp_server.request_context.request + if context_request is None: + raise ValueError( + "Could not find the request object in the MCP server context. This is not expected to happen. Please raise an issue: https://github.com/gradio-app/gradio." + ) + route_path = self.get_route_path(context_request) + root_url = route_utils.get_root_url( + request=context_request, + route_path=route_path, + root_path=self.root_path, + ) + self._client_instance = Client( + self.local_url or root_url, + download_files=False, + verbose=False, + analytics_enabled=False, + ssl_verify=False, + _skip_components=False, + headers={"x-gradio-user": "mcp"}, + ) + return self._client_instance + + def _prepare_tool_call_args( + self, name: str, arguments: dict[str, Any] + ) -> tuple[str, list[Any], dict[str, str], "BlockFunction"]: + """ + Prepare and validate arguments for a tool call. + + Returns: + A tuple of (endpoint_name, processed_args, request_headers, block_fn) + """ + selected_tools = self.get_selected_tools_from_request() + _, filedata_positions = self.get_input_schema(name) + processed_kwargs = self.convert_strings_to_filedata( + arguments, filedata_positions + ) + endpoint_name = self.tool_to_endpoint.get(name) + if endpoint_name is None: + raise ValueError(f"Unknown tool for this Gradio app: {name}") + + if selected_tools is not None and name not in selected_tools: + raise ValueError(f"Tool '{name}' is not in the selected tools list") + + block_fn = self.get_block_fn_from_endpoint_name(endpoint_name) + assert block_fn is not None # noqa: S101 + + if endpoint_name in self.api_info["named_endpoints"]: + parameters_info = self.api_info["named_endpoints"][endpoint_name][ + "parameters" + ] + processed_args = client_utils.construct_args( + parameters_info, + (), + processed_kwargs, + ) + else: + processed_args = [] + + context_request: Request | None = self.mcp_server.request_context.request + if context_request is None: + raise ValueError( + "Could not find the request object in the MCP server context. This is not expected to happen. Please raise an issue: https://github.com/gradio-app/gradio." + ) + request_headers = dict(context_request.headers.items()) + request_headers.pop("content-length", None) + request_headers.pop("x-gradio-user", None) + + return endpoint_name, processed_args, request_headers, block_fn + + async def _execute_tool_without_progress(self, job: Any) -> list[Any]: + """ + Execute a tool call without progress tracking (fast path). + + Calls job.result() to get the final output without processing + intermediate status updates. + + Returns: + The output data as a list. + """ + result = await run_sync(job.result) + return [result] + + @staticmethod + def _format_progress_message(update: StatusUpdate) -> str | None: + """ + Format a status update into a human-readable progress message. + + Returns: + A formatted message string, or None if no message should be shown. + """ + if update.code in [Status.JOINING_QUEUE, Status.STARTING]: + return "Joined server queue." + elif update.code in [Status.IN_QUEUE]: + message = f"In queue. Position {update.rank} out of {update.queue_size}." + if update.eta is not None: + message += f" Estimated time remaining: {update.eta} seconds." + return message + elif update.code in [Status.PROGRESS]: + for progress_unit in update.progress_data or []: + title = ( + "Progress" + if progress_unit.desc is None + else f"Progress {progress_unit.desc}" + ) + if progress_unit.index is not None and progress_unit.length is not None: + return ( + f"{title}: Step {progress_unit.index} of {progress_unit.length}" + ) + elif progress_unit.index is not None and progress_unit.length is None: + return f"{title}: Step {progress_unit.index}" + elif update.code in [Status.PROCESSING, Status.ITERATING]: + return "Processing" + return None + + async def _execute_tool_with_progress( # type: ignore + self, job: Any, progress_token: str | int + ) -> dict[str, Any]: + """ + Execute a tool call with progress tracking (streaming path). + + Iterates through job updates to send progress notifications to the client. + + Returns: + The output data as a list. + """ + step = 0 + async for update in job: + if update.type == "status": + update = cast(StatusUpdate, update) + message = self._format_progress_message(update) + + await ( + self.mcp_server.request_context.session.send_progress_notification( + progress_token=progress_token, + progress=step, + message=message, # type: ignore + related_request_id=str( + self.mcp_server.request_context.request_id + ), + ) + ) + step += 1 + elif update.type == "output" and update.final: + output = update.outputs + if not update.success: + error_title = output.get("title") + error_message = output.get("error") + if error_title and error_message: + msg = f"{error_title}: {error_message}" + elif error_message: + msg = error_message + elif error_title: + msg = error_title + else: + msg = "Error!" + raise RuntimeError(msg) + if job.exception(): + raise job.exception() + return output["data"] + + def create_mcp_server(self) -> "Server": + """ + Create an MCP server for the given Gradio Blocks app. + + Parameters: + blocks: The Blocks app to create the MCP server for. + + Returns: + The MCP server. + """ + server = self.Server(str(self.blocks.title or "Gradio App")) # type: ignore + + @server.call_tool() + async def call_tool( + name: str, arguments: dict[str, Any] + ) -> self.types.CallToolResult: # type: ignore + """ + Call a tool on the Gradio app. + + Args: + name: The name of the tool to call. + arguments: The arguments to pass to the tool. + """ + endpoint_name, processed_args, request_headers, block_fn = ( + self._prepare_tool_call_args(name, arguments) + ) + processed_args = self.insert_empty_state(block_fn.inputs, processed_args) + + if not block_fn.queue: + # Fast path for non-queued events: call blocks.process_api() + # directly instead of the HTTP loopback through gradio_client. + # This eliminates thread dispatches, TCP round-trips, and SSE + # overhead — reducing MCP tool-call latency significantly. + session_state = SessionState(self.blocks) + raw_output = await self.blocks.process_api( + block_fn=block_fn, + inputs=processed_args, + state=session_state, + request=self.mcp_server.request_context.request, + ) + output_data = raw_output["data"] + else: + # Queued path: use the HTTP loopback to preserve streaming + # updates, progress notifications, and queue-based features. + progress_token = None + if self.mcp_server.request_context.meta is not None: + progress_token = self.mcp_server.request_context.meta.progressToken + + client = await run_sync(self._get_or_create_client) + job = client.submit( + *processed_args, + api_name=endpoint_name, + headers=request_headers, + ) + + if progress_token is None: + output_data = await self._execute_tool_without_progress(job) + else: + output_data = await self._execute_tool_with_progress( + job, + progress_token, + ) + + output_data = self.pop_returned_state(block_fn.outputs, output_data) + + context_request: Request | None = self.mcp_server.request_context.request + route_path = self.get_route_path(context_request) # type: ignore + root_url = route_utils.get_root_url( # type: ignore + request=context_request, # type: ignore + route_path=route_path, # type: ignore + root_path=self.root_path, # type: ignore + ) + content = self.postprocess_output_data(output_data, root_url) + if getattr(block_fn.fn, "_mcp_structured_output", False): + structured_content = {"result": content} + else: + structured_content = None + return self.types.CallToolResult( # type: ignore + content=content, # type: ignore + structuredContent=structured_content, # type: ignore + _meta=getattr(block_fn.fn, "_mcp_meta", None), # type: ignore + ) + + @server.list_tools() + async def list_tools() -> list[self.types.Tool]: # type: ignore + """ + List all tools on the Gradio app. + """ + selected_tools = self.get_selected_tools_from_request() + + tools = [] + for tool_name, endpoint_name in self.tool_to_endpoint.items(): + if selected_tools is not None and tool_name not in selected_tools: + continue + + block_fn = self.get_block_fn_from_endpoint_name(endpoint_name) + if ( + block_fn is None + or block_fn.fn is None + or ( + hasattr(block_fn.fn, "_mcp_type") + and block_fn.fn._mcp_type != "tool" + ) + ): + continue + + description, parameters = self.get_fn_description(block_fn, tool_name) + schema, _ = self.get_input_schema(tool_name, parameters) + tool_meta = getattr(block_fn.fn, "_mcp_meta", None) + + tools.append( + self.types.Tool( # type: ignore + name=tool_name, + description=description, + inputSchema=schema, + _meta=tool_meta, # type: ignore + ) + ) + return tools + + @server.list_resources() + async def list_resources() -> list[self.types.Resource]: # type: ignore + """ + List all available resources. + """ + resources = [] + + selected_tools = self.get_selected_tools_from_request() + for tool_name, endpoint_name in self.tool_to_endpoint.items(): + if selected_tools is not None and tool_name not in selected_tools: + continue + + block_fn = self.get_block_fn_from_endpoint_name(endpoint_name) + if ( + block_fn + and block_fn.fn + and hasattr(block_fn.fn, "_mcp_type") + and block_fn.fn._mcp_type == "resource" + ): + uri_template = block_fn.fn._mcp_uri_template # type: ignore + parameters = re.findall(r"\{([^}]+)\}", uri_template) + description, parameters, _ = utils.get_function_description( + block_fn.fn + ) + if not parameters: + resources.append( + self.types.Resource( # type: ignore + uri=uri_template, + name=block_fn.fn.__name__, # type: ignore + description=description, + mimeType=block_fn.fn._mcp_mime_type, # type: ignore + ) + ) + return resources + + @server.list_resource_templates() + async def list_resource_templates() -> list[self.types.ResourceTemplate]: # type: ignore + """ + List all available resource templates. + """ + templates = [] + selected_tools = self.get_selected_tools_from_request() + for tool_name, endpoint_name in self.tool_to_endpoint.items(): + if selected_tools is not None and tool_name not in selected_tools: + continue + + block_fn = self.get_block_fn_from_endpoint_name(endpoint_name) + if ( + block_fn + and block_fn.fn + and hasattr(block_fn.fn, "_mcp_type") + and block_fn.fn._mcp_type == "resource" + ): + uri_template = block_fn.fn._mcp_uri_template # type: ignore + parameters = re.findall(r"\{([^}]+)\}", uri_template) + description, parameters, _ = utils.get_function_description( + block_fn.fn + ) + if parameters: + templates.append( + self.types.ResourceTemplate( # type: ignore + uriTemplate=uri_template, + name=block_fn.fn.__name__, # type: ignore + description=description, + mimeType=block_fn.fn._mcp_mime_type, # type: ignore + ) + ) + return templates + + @server.read_resource() + async def read_resource(uri: AnyUrl | str) -> list[self.ReadResourceContents]: # type: ignore + """ + Read a specific resource by URI. + """ + uri = str(uri) + client = await run_sync(self._get_or_create_client) + for endpoint_name in self.tool_to_endpoint.values(): + block_fn = self.get_block_fn_from_endpoint_name(endpoint_name) + + if ( + block_fn + and block_fn.fn + and hasattr(block_fn.fn, "_mcp_type") + and block_fn.fn._mcp_type == "resource" + ): + uri_template = block_fn.fn._mcp_uri_template # type: ignore + parameters = re.findall(r"\{([^}]+)\}", uri_template) # type: ignore + + kwargs = {} + matched = False + + if parameters: + pattern = re.escape(uri_template) + for param in parameters: + pattern = pattern.replace( + f"\\{{{param}\\}}", f"(?P<{param}>[^/]+)" + ) + match = re.match(f"^{pattern}$", uri) + if match: + kwargs = match.groupdict() + matched = True + elif uri_template == uri: + matched = True + + if matched: + if endpoint_name in self.api_info["named_endpoints"]: + parameters_info = self.api_info["named_endpoints"][ + endpoint_name + ]["parameters"] + processed_args = client_utils.construct_args( + parameters_info, + (), + kwargs, + ) + else: + processed_args = list(kwargs.values()) + + async for update in client.submit( + *processed_args, api_name=endpoint_name + ): + if update.type == "output" and update.final: # type: ignore + output = update.outputs # type: ignore + result = output["data"][0] + break + + mime_type = block_fn.fn._mcp_mime_type # type: ignore + if mime_type and not mime_type.startswith("text/"): + result = base64.b64decode(result.encode("ascii")) + return [ + self.ReadResourceContents( # type: ignore + content=result, mime_type=mime_type + ) + ] + + raise ValueError(f"Resource not found: {uri}") + + @server.list_prompts() + async def list_prompts() -> list[self.types.Prompt]: # type: ignore + """ + List all available prompts. + """ + prompts = [] + selected_tools = self.get_selected_tools_from_request() + for tool_name, endpoint_name in self.tool_to_endpoint.items(): + if selected_tools is not None and tool_name not in selected_tools: + continue + + block_fn = self.get_block_fn_from_endpoint_name(endpoint_name) + if ( + block_fn + and block_fn.fn + and hasattr(block_fn.fn, "_mcp_type") + and block_fn.fn._mcp_type == "prompt" + ): + description, parameters, _ = utils.get_function_description( + block_fn.fn + ) + function_params = utils.get_function_params(block_fn.fn) + arguments = [ + self.types.PromptArgument( # type: ignore + name=param_name, + description=parameters.get(param_name, ""), + required=not has_default, + ) + for param_name, has_default, _, _ in function_params + ] + prompts.append( + self.types.Prompt( # type: ignore + name=tool_name, + description=description, + arguments=arguments, + ) + ) + return prompts + + @server.get_prompt() + async def get_prompt( + name: str, arguments: dict[str, Any] | None = None + ) -> self.types.GetPromptResult: # type: ignore + """ + Get a specific prompt with filled-in arguments. + """ + client = await run_sync(self._get_or_create_client) + + endpoint_name = None + for endpoint_name in self.tool_to_endpoint.values(): + block_fn = self.get_block_fn_from_endpoint_name(endpoint_name) + if ( + block_fn + and block_fn.fn + and hasattr(block_fn.fn, "_mcp_type") + and block_fn.fn._mcp_type == "prompt" + and block_fn.fn._mcp_name == name # type: ignore + ): + break + + if not endpoint_name: + raise ValueError(f"Prompt not found: {name}") + + arguments = arguments or {} + + block_fn = self.get_block_fn_from_endpoint_name(endpoint_name) + assert block_fn is not None # noqa: S101 + + if endpoint_name in self.api_info["named_endpoints"]: + parameters_info = self.api_info["named_endpoints"][endpoint_name][ + "parameters" + ] + processed_args = client_utils.construct_args( + parameters_info, + (), + arguments, + ) + else: + processed_args = list(arguments.values()) + + async for update in client.submit(*processed_args, api_name=endpoint_name): + if update.type == "output" and update.final: # type: ignore + output = update.outputs # type: ignore + result = output["data"][0] + break + + return self.types.GetPromptResult( # type: ignore + messages=[ + self.types.PromptMessage( # type: ignore + role="user", + content=self.types.TextContent(type="text", text=str(result)), # type: ignore + ) + ] + ) + + return server + + def launch_mcp_on_sse(self, app: Starlette, subpath: str, root_path: str) -> None: + """ + Launch the MCP server on the SSE transport. + + Parameters: + app: The Gradio app to mount the MCP server on. + subpath: The subpath to mount the MCP server on. E.g. "/gradio_api/mcp" + root_path: The root path of the Gradio Blocks app. + """ + messages_path = "/messages/" + sse = self.SseServerTransport(messages_path) # type: ignore + self.root_path = root_path + + async def handle_sse(request): + try: + async with sse.connect_sse( + request.scope, request.receive, request._send + ) as streams: + await self.mcp_server.run( + streams[0], + streams[1], + self.mcp_server.create_initialization_options(), + ) + return Response() + except Exception as e: + print(f"MCP SSE connection error: {str(e)}") + raise + + app.mount( + subpath, + Starlette( + routes=[ + Route( + "/schema", + endpoint=self.get_complete_schema, # Not required for MCP but used by the Hugging Face MCP server to get the schema for MCP Spaces without needing to establish an SSE connection + ), + Route("/sse", endpoint=handle_sse), + Mount("/messages/", app=sse.handle_post_message), + Mount("/", app=self.handle_streamable_http), + ], + ), + ) + + def get_block_fn_from_endpoint_name( + self, endpoint_name: str + ) -> "BlockFunction | None": + """ + Get the BlockFunction for a given endpoint name (e.g. "/predict"). + + Parameters: + endpoint_name: The name of the endpoint to get the BlockFunction for. + + Returns: + The BlockFunction for the given endpoint name, or None if it is not found. + """ + block_fn = next( + ( + fn + for fn in self.blocks.fns.values() + if fn.api_name == endpoint_name.lstrip("/") + ), + None, + ) + return block_fn + + @property + def _file_data_tool_description(self) -> str: + """ + Sentence prompting the agent to use the upload_file_to_gradio tool if a file is passed as an input. + """ + return " If a user passes a file as an input, use the upload_file_to_gradio tool, if present, to upload the file to the gradio app and create a Gradio File Input. Then use the returned path as the input to the tool" + + def get_fn_description( + self, block_fn: "BlockFunction", tool_name: str + ) -> tuple[str, dict[str, str]]: + """ + Get the description of a function, which is used to describe the tool in the MCP server. + Also returns the description of each parameter of the function as a dictionary. + """ + description, parameters, returns = utils.get_function_description(block_fn.fn) # type: ignore + _, filedata_positions = self.get_input_schema(tool_name, parameters) + if block_fn.api_description is False: + description = "" + elif block_fn.api_description is None: + if len(filedata_positions) > 0: + description += self._file_data_tool_description + if returns: + description += ( + ("" if description.endswith(".") else ".") + + " Returns: " + + ", ".join(returns) + ) + else: + description = block_fn.api_description + if len(filedata_positions) > 0: + description += self._file_data_tool_description # type: ignore + assert isinstance(description, str) # noqa: S101 + return description, parameters + + @staticmethod + def insert_empty_state( + inputs: Sequence["Component | BlockContext"], data: list + ) -> list: + """ + Insert None placeholder values for any State input components, as State inputs + are not included in the endpoint schema. + """ + for i, input_component_type in enumerate(inputs): + if isinstance(input_component_type, State): + data.insert(i, None) + return data + + @staticmethod + def pop_returned_state( + components: Sequence["Component | BlockContext"], data: Any + ) -> list: + """ + Remove any values corresponding to State output components from the data + as State outputs are not included in the endpoint schema. + """ + for i, component_type in enumerate(components): + if isinstance(component_type, State): + data.pop(i) + return data + + def get_input_schema( + self, + tool_name: str, + parameters: dict[str, str] | None = None, + ) -> tuple[dict[str, Any], list[list[str | int]]]: + """ + Get the input schema of the Gradio app API, appropriately formatted for MCP. + + Parameters: + tool_name: The name of the tool to get the schema for, e.g. "predict" + parameters: The description and parameters of the tool to get the schema for. + Returns: + - The input schema of the Gradio app API. + - A list of positions of FileData objects in the input schema. + """ + endpoint_name = self.tool_to_endpoint.get(tool_name) + if endpoint_name is None: + raise ValueError(f"Unknown tool for this Gradio app: {tool_name}") + named_endpoints = self.api_info["named_endpoints"] + endpoint_info = named_endpoints.get(endpoint_name) + assert endpoint_info is not None # noqa: S101 + + schema = { + "type": "object", + "properties": { + p["parameter_name"]: { + **p["type"], + **( + {"description": parameters[p["parameter_name"]]} + if parameters and p["parameter_name"] in parameters + else {} + ), + **( + {"default": p["parameter_default"]} + if "parameter_default" in p and p["parameter_default"] + else {} + ), + } + for p in endpoint_info["parameters"] + }, + } + return self.simplify_filedata_schema(schema) + + async def get_complete_schema(self, request) -> JSONResponse: + """ + Get the complete schema of the Gradio app API. For debugging purposes, also used by + the Hugging Face MCP server to get the schema for MCP Spaces without needing to + establish an SSE connection. + + Parameters: + request: The Starlette request object. + + Returns: + A JSONResponse containing a dictionary mapping tool names to their input schemas. + """ + if not self.api_info: + return JSONResponse({}) + + query_params = dict(getattr(request, "query_params", {})) + selected_tools = None + if "tools" in query_params: + tools = query_params["tools"].split(",") + selected_tools = set(tools) + + file_data_present = False + + schemas = [] + for tool_name, endpoint_name in self.tool_to_endpoint.items(): + if selected_tools is not None and tool_name not in selected_tools: + continue + block_fn = self.get_block_fn_from_endpoint_name(endpoint_name) + assert block_fn is not None and block_fn.fn is not None # noqa: S101 + + description, parameters = self.get_fn_description(block_fn, tool_name) + schema, filedata_positions = self.get_input_schema(tool_name, parameters) + if len(filedata_positions) > 0 and not file_data_present: + file_data_present = True + + type_hints = utils.get_type_hints(block_fn.fn) + required_headers = [] + for param_name, type_hint in type_hints.items(): + if type_hint is Header or type_hint is Optional[Header]: + header_name = param_name.replace("_", "-").lower() + required_headers.append(header_name) + + mcp_type = "tool" # Default + if hasattr(block_fn.fn, "_mcp_type"): + mcp_type = block_fn.fn._mcp_type + + meta = { + "file_data_present": file_data_present, + "mcp_type": mcp_type, + "endpoint_name": block_fn.api_name, + } + if required_headers: + meta["headers"] = required_headers + + info = { + "name": tool_name, + "description": description, + "inputSchema": schema, + "meta": meta, + } + schemas.append(info) + + return JSONResponse(schemas) + + def simplify_filedata_schema( + self, schema: dict[str, Any] + ) -> tuple[dict[str, Any], list[list[str | int]]]: + """ + Parses a schema of a Gradio app API to identify positions of FileData objects. Replaces them with base64 + strings while keeping track of their positions so that they can be converted back to FileData objects + later. + + Parameters: + schema: The original schema of the Gradio app API. + + Returns: + A tuple containing the simplified schema and the positions of the FileData objects. + """ + + def is_gradio_filedata(obj: Any, defs: dict[str, Any]) -> bool: + if not isinstance(obj, dict): + return False + + if "$ref" in obj: + ref = obj["$ref"] + if ref.startswith("#/$defs/"): + key = ref.split("/")[-1] + obj = defs.get(key, {}) + else: + return False + + props = obj.get("properties", {}) + meta = props.get("meta", {}) + + if "$ref" in meta: + ref = meta["$ref"] + if ref.startswith("#/$defs/"): + key = ref.split("/")[-1] + meta = defs.get(key, {}) + else: + return False + + type_field = meta.get("properties", {}).get("_type", {}) + default_type = meta.get("default", {}).get("_type") + return ( + type_field.get("const") == "gradio.FileData" + or default_type == "gradio.FileData" + ) + + def traverse( + node: Any, + path: list[str | int] | None = None, + defs: dict[str, Any] | None = None, + ) -> Any: + if path is None: + path = [] + if defs is None: + defs = {} + # Deep copy the node to avoid modifying the original node + node = copy.deepcopy(node) + + if isinstance(node, dict): + if "$defs" in node: + defs.update(node["$defs"]) + + if is_gradio_filedata(node, defs): + filedata_positions.append(path.copy()) + for key in ["properties", "additional_description", "$defs"]: + node.pop(key, None) + node["type"] = "string" + node["format"] = "Gradio File Input - a http or https url to a file" + + result = {} + is_schema_root = "type" in node and "properties" in node + for key, value in node.items(): + if is_schema_root and key == "properties": + result[key] = traverse(value, path, defs) + else: + path.append(key) + result[key] = traverse(value, path, defs) + path.pop() + return result + + elif isinstance(node, list): + result = [] + for i, item in enumerate(node): + path.append(i) + result.append(traverse(item, path, defs)) + path.pop() + return result + + return node + + filedata_positions: list[list[str | int]] = [] + simplified_schema = traverse(schema) + return simplified_schema, filedata_positions + + def convert_strings_to_filedata( + self, value: Any, filedata_positions: list[list[str | int]] + ) -> Any: + """ + Convert specific string values back to FileData objects based on their positions. + This is used to convert string values (as base64 encoded strings) to FileData + dictionaries so that they can be passed into .preprocess() logic of a Gradio app. + + Parameters: + value: The input data to process, which can be an arbitrary nested data structure + that may or may not contain strings that should be converted to FileData objects. + filedata_positions: List of paths to positions in the input data that should be converted to FileData objects. + + Returns: + The processed data with strings converted to FileData objects where appropriate. Base64 + encoded strings are first saved to a temporary file and then converted to a FileData object. + + Example: + >>> convert_strings_to_filedata( + {"image": "data:image/jpeg;base64,..."}, + [["image"]] + ) + >>> {'image': FileData(path='')}, + """ + + def traverse(node: Any, path: list[str | int] | None = None) -> Any: + if path is None: + path = [] + + if isinstance(node, dict): + return { + key: traverse(value, path + [key]) for key, value in node.items() + } + elif isinstance(node, list): + return [traverse(item, path + [i]) for i, item in enumerate(node)] + elif isinstance(node, str) and path in filedata_positions: + if node.startswith("data:"): + # Even though base64 is not officially part of our schema, some MCP clients + # might return base64 encoded strings, so try to save it to a temporary file. + return handle_file( + processing_utils.save_base64_to_cache(node, DEFAULT_TEMP_DIR) + ) + elif node.startswith(("http://", "https://")): + return handle_file(node) + else: + raise ValueError( + f"Invalid file data format, provide a url ('http://...' or 'https://...'). Received: {node}" + ) + return node + + return traverse(value) + + @staticmethod + def get_image(file_path: str) -> Image.Image | None: + """ + If a filepath is a valid image, returns a PIL Image object. Otherwise returns None. + """ + if not os.path.exists(file_path): + return None + ext = os.path.splitext(file_path.lower())[1] + if ext not in Image.registered_extensions(): + return None + try: + return Image.open(file_path) + except Exception: + return None + + @staticmethod + def get_svg(file_data: Any) -> bytes | None: + """ + If a file_data is a valid FileDataDict with a url that is a data:image/svg+xml, returns bytes of the svg. Otherwise returns None. + """ + if isinstance(file_data, dict) and (url := file_data.get("url")): + if isinstance(url, str) and url.startswith("data:image/svg"): + return unquote(url.split(",", 1)[1]).encode() + else: + return None + else: + return None + + @staticmethod + def get_base64_data(image: Image.Image, format: str) -> str: + """ + Returns a base64 encoded string of the image. + """ + buffer = BytesIO() + image.save(buffer, format=format) + return base64.b64encode(buffer.getvalue()).decode("utf-8") + + def postprocess_output_data( + self, data: Any, root_url: str + ) -> list["types.TextContent | types.ImageContent"]: + """ + Postprocess the output data from the Gradio app to convert FileData objects back to base64 encoded strings. + + Parameters: + data: The output data to postprocess. + """ + return_values = [] + data = processing_utils.add_root_url(data, root_url, None) + for output in data: + if svg_bytes := self.get_svg(output): + base64_data = base64.b64encode(svg_bytes).decode("utf-8") + mimetype = "image/svg+xml" + svg_path = processing_utils.save_bytes_to_cache( + svg_bytes, f"{output['orig_name']}", DEFAULT_TEMP_DIR + ) + svg_url = f"{root_url}/gradio_api/file={svg_path}" + return_value = [ + self.types.ImageContent( # type: ignore + type="image", data=base64_data, mimeType=mimetype + ), + self.types.TextContent( # type: ignore + type="text", + text=f"SVG Image URL: {svg_url}", + ), + ] + elif client_utils.is_file_obj_with_meta(output): + if image := self.get_image(output["path"]): + image_format = image.format or "png" + base64_data = self.get_base64_data(image, image_format) + mimetype = f"image/{image_format.lower()}" + return_value = [ + self.types.ImageContent( # type: ignore + type="image", data=base64_data, mimeType=mimetype + ), + self.types.TextContent( # type: ignore + type="text", + text=f"Image URL: {output['url'] or output['path']}", + ), + ] + else: + return_value = [ + self.types.TextContent( # type: ignore + type="text", text=str(output["url"] or output["path"]) + ) + ] + else: + return_value = [self.types.TextContent(type="text", text=str(output))] # type: ignore + return_values.extend(return_value) + return return_values + + +###################################################### +### MCP decorators that add metadata to functions. +###################################################### + + +def resource( + uri_template: str, description: str | None = None, mime_type: str | None = None +): + """Decorator to mark a function as an MCP resource.""" + + def decorator(fn): + fn._mcp_type = "resource" + fn._mcp_uri_template = uri_template + fn._mcp_description = description + fn._mcp_mime_type = mime_type or "text/plain" + return fn + + return decorator + + +def prompt(name: str | None = None, description: str | None = None): + """Decorator to mark a function as an MCP prompt.""" + + def decorator(fn): + fn._mcp_type = "prompt" + fn._mcp_name = name or fn.__name__ + fn._mcp_description = description + return fn + + return decorator + + +def tool( + name: str | None = None, + description: str | None = None, + structured_output: bool = False, + _meta: dict[str, Any] | None = None, +): + """ + Decorator to mark a function as an MCP tool (optional, since functions are registered as tools by default). + Can be used to configure various aspects of the tool. + + Parameters: + name: The name of the tool. Overrides the default name of the function. + description: The description of the tool. Overrides the default description from the function's docstring. + structured_output: Whether the tool should return structured output (implementation is quite limited at the moment). If True, the output will be wrapped in a dictionary with the key "result" and the value being the output of the function. Recommended to keep this False unless you have a specific reason to need the structured output. + _meta: Additional metadata for the tool. + """ + + def decorator(fn): + fn._mcp_type = "tool" + fn._mcp_name = name + fn._mcp_structured_output = structured_output + fn._mcp_description = description + fn._mcp_meta = _meta + return fn + + return decorator diff --git a/gradio/media.py b/gradio/media.py new file mode 100644 index 0000000..50ece7a --- /dev/null +++ b/gradio/media.py @@ -0,0 +1,157 @@ +""" +Media Registry for Gradio Demos + +This module provides a centralized way to access media files. + +Usage: + from gradio.media import get_image, get_video, get_audio, get_model3d, get_file + + # Get specific media files + cheetah_img = get_image("cheetah1.jpg") + world_video = get_video("world.mp4") + cantina_audio = get_audio("cantina.wav") + bunny_model = get_model3d("Bunny.obj") + titanic_data = get_file("titanic.csv") + + # Get random media of a type + random_img = get_image() + random_video = get_video() + random_audio = get_audio() + +""" + +import random +from pathlib import Path +from typing import Optional + +MEDIA_ROOT = Path(__file__).parent / "media_assets" +MEDIA_PATHS = [ + MEDIA_ROOT / "images", + MEDIA_ROOT / "videos", + MEDIA_ROOT / "audio", + MEDIA_ROOT / "models3d", + MEDIA_ROOT / "data", +] + + +def _get_media_path(media_type: str, filename: Optional[str] = None) -> str: + """ + Internal function to get the path to a media file. + + Args: + media_type: Type of media (images, videos, audio, models3d, data) + filename: Optional filename of the media file. If None, returns a random file. + + Returns: + Absolute path to the media file + + Raises: + ValueError: If media_type is invalid + FileNotFoundError: If the media file doesn't exist + """ + media_dir = MEDIA_ROOT / media_type + + if not media_dir.exists(): + raise ValueError(f"Media directory not found: {media_dir}") + + if filename is None: + # Get a random file from the directory + media_files = list(media_dir.glob("*")) + if not media_files: + raise ValueError(f"No media files found in {media_dir}") + file_path = random.choice(media_files) + else: + if filename.startswith(("http://", "https://")): + return filename + + file_path = media_dir / filename + + if not file_path.exists(): + raise FileNotFoundError(f"Media file not found: {file_path}") + + return str(file_path.absolute()) + + +def get_image(filename: Optional[str] = None) -> str: + """ + Get path to an image file. + + Args: + filename: Filename of the image (e.g., "tower.jpg"). If None, returns a random image. + + Returns: + Absolute path to the image file + + Examples: + >>> get_image("tower.jpg") # Get specific image + >>> get_image() # Get random image + """ + return _get_media_path("images", filename) + + +def get_video(filename: Optional[str] = None) -> str: + """ + Get path to a video file. + + Args: + filename: Filename of the video (e.g., "world.mp4"). If None, returns a random video. + + Returns: + Absolute path to the video file + + Examples: + >>> get_video("world.mp4") # Get specific video + >>> get_video() # Get random video + """ + return _get_media_path("videos", filename) + + +def get_audio(filename: Optional[str] = None) -> str: + """ + Get path to an audio file. + + Args: + filename: Filename of the audio (e.g., "cantina.wav"). If None, returns a random audio file. + + Returns: + Absolute path to the audio file + + Examples: + >>> get_audio("cantina.wav") # Get specific audio + >>> get_audio() # Get random audio + """ + return _get_media_path("audio", filename) + + +def get_model3d(filename: Optional[str] = None) -> str: + """ + Get path to a 3D model file. + + Args: + filename: Filename of the 3D model (e.g., "Duck.glb"). If None, returns a random model. + + Returns: + Absolute path to the 3D model file + + Examples: + >>> get_model3d("Duck.glb") # Get specific model + >>> get_model3d() # Get random 3D model + """ + return _get_media_path("models3d", filename) + + +def get_file(filename: Optional[str] = None) -> str: + """ + Get path to a data file (CSV, JSON, text, etc.). + + Args: + filename: Filename of the data file (e.g., "titanic.csv"). If None, returns a random file. + + Returns: + Absolute path to the data file + + Examples: + >>> get_file("titanic.csv") # Get specific file + >>> get_file() # Get random data file + """ + return _get_media_path("data", filename) diff --git a/gradio/media_assets/audio/audio_sample.wav b/gradio/media_assets/audio/audio_sample.wav new file mode 100644 index 0000000..495f00d Binary files /dev/null and b/gradio/media_assets/audio/audio_sample.wav differ diff --git a/gradio/media_assets/audio/cantina.wav b/gradio/media_assets/audio/cantina.wav new file mode 100644 index 0000000..41f0204 Binary files /dev/null and b/gradio/media_assets/audio/cantina.wav differ diff --git a/gradio/media_assets/audio/cate_blanch.mp3 b/gradio/media_assets/audio/cate_blanch.mp3 new file mode 100644 index 0000000..4beee74 Binary files /dev/null and b/gradio/media_assets/audio/cate_blanch.mp3 differ diff --git a/gradio/media_assets/audio/cate_blanch_2.mp3 b/gradio/media_assets/audio/cate_blanch_2.mp3 new file mode 100644 index 0000000..1acc212 Binary files /dev/null and b/gradio/media_assets/audio/cate_blanch_2.mp3 differ diff --git a/gradio/media_assets/audio/heath_ledger.mp3 b/gradio/media_assets/audio/heath_ledger.mp3 new file mode 100644 index 0000000..eb63071 Binary files /dev/null and b/gradio/media_assets/audio/heath_ledger.mp3 differ diff --git a/gradio/media_assets/audio/recording1.wav b/gradio/media_assets/audio/recording1.wav new file mode 100644 index 0000000..a58ba50 Binary files /dev/null and b/gradio/media_assets/audio/recording1.wav differ diff --git a/gradio/media_assets/audio/sax.wav b/gradio/media_assets/audio/sax.wav new file mode 100644 index 0000000..de6d16a Binary files /dev/null and b/gradio/media_assets/audio/sax.wav differ diff --git a/gradio/media_assets/data/imagenet_labels.json b/gradio/media_assets/data/imagenet_labels.json new file mode 100644 index 0000000..fa059ce --- /dev/null +++ b/gradio/media_assets/data/imagenet_labels.json @@ -0,0 +1,1000 @@ +["tench", + "goldfish", + "great white shark", + "tiger shark", + "hammerhead shark", + "electric ray", + "stingray", + "cock", + "hen", + "ostrich", + "brambling", + "goldfinch", + "house finch", + "junco", + "indigo bunting", + "American robin", + "bulbul", + "jay", + "magpie", + "chickadee", + "American dipper", + "kite", + "bald eagle", + "vulture", + "great grey owl", + "fire salamander", + "smooth newt", + "newt", + "spotted salamander", + "axolotl", + "American bullfrog", + "tree frog", + "tailed frog", + "loggerhead sea turtle", + "leatherback sea turtle", + "mud turtle", + "terrapin", + "box turtle", + "banded gecko", + "green iguana", + "Carolina anole", + "desert grassland whiptail lizard", + "agama", + "frilled-necked lizard", + "alligator lizard", + "Gila monster", + "European green lizard", + "chameleon", + "Komodo dragon", + "Nile crocodile", + "American alligator", + "triceratops", + "worm snake", + "ring-necked snake", + "eastern hog-nosed snake", + "smooth green snake", + "kingsnake", + "garter snake", + "water snake", + "vine snake", + "night snake", + "boa constrictor", + "African rock python", + "Indian cobra", + "green mamba", + "sea snake", + "Saharan horned viper", + "eastern diamondback rattlesnake", + "sidewinder", + "trilobite", + "harvestman", + "scorpion", + "yellow garden spider", + "barn spider", + "European garden spider", + "southern black widow", + "tarantula", + "wolf spider", + "tick", + "centipede", + "black grouse", + "ptarmigan", + "ruffed grouse", + "prairie grouse", + "peacock", + "quail", + "partridge", + "grey parrot", + "macaw", + "sulphur-crested cockatoo", + "lorikeet", + "coucal", + "bee eater", + "hornbill", + "hummingbird", + "jacamar", + "toucan", + "duck", + "red-breasted merganser", + "goose", + "black swan", + "tusker", + "echidna", + "platypus", + "wallaby", + "koala", + "wombat", + "jellyfish", + "sea anemone", + "brain coral", + "flatworm", + "nematode", + "conch", + "snail", + "slug", + "sea slug", + "chiton", + "chambered nautilus", + "Dungeness crab", + "rock crab", + "fiddler crab", + "red king crab", + "American lobster", + "spiny lobster", + "crayfish", + "hermit crab", + "isopod", + "white stork", + "black stork", + "spoonbill", + "flamingo", + "little blue heron", + "great egret", + "bittern", + "crane (bird)", + "limpkin", + "common gallinule", + "American coot", + "bustard", + "ruddy turnstone", + "dunlin", + "common redshank", + "dowitcher", + "oystercatcher", + "pelican", + "king penguin", + "albatross", + "grey whale", + "killer whale", + "dugong", + "sea lion", + "Chihuahua", + "Japanese Chin", + "Maltese", + "Pekingese", + "Shih Tzu", + "King Charles Spaniel", + "Papillon", + "toy terrier", + "Rhodesian Ridgeback", + "Afghan Hound", + "Basset Hound", + "Beagle", + "Bloodhound", + "Bluetick Coonhound", + "Black and Tan Coonhound", + "Treeing Walker Coonhound", + "English foxhound", + "Redbone Coonhound", + "borzoi", + "Irish Wolfhound", + "Italian Greyhound", + "Whippet", + "Ibizan Hound", + "Norwegian Elkhound", + "Otterhound", + "Saluki", + "Scottish Deerhound", + "Weimaraner", + "Staffordshire Bull Terrier", + "American Staffordshire Terrier", + "Bedlington Terrier", + "Border Terrier", + "Kerry Blue Terrier", + "Irish Terrier", + "Norfolk Terrier", + "Norwich Terrier", + "Yorkshire Terrier", + "Wire Fox Terrier", + "Lakeland Terrier", + "Sealyham Terrier", + "Airedale Terrier", + "Cairn Terrier", + "Australian Terrier", + "Dandie Dinmont Terrier", + "Boston Terrier", + "Miniature Schnauzer", + "Giant Schnauzer", + "Standard Schnauzer", + "Scottish Terrier", + "Tibetan Terrier", + "Australian Silky Terrier", + "Soft-coated Wheaten Terrier", + "West Highland White Terrier", + "Lhasa Apso", + "Flat-Coated Retriever", + "Curly-coated Retriever", + "Golden Retriever", + "Labrador Retriever", + "Chesapeake Bay Retriever", + "German Shorthaired Pointer", + "Vizsla", + "English Setter", + "Irish Setter", + "Gordon Setter", + "Brittany", + "Clumber Spaniel", + "English Springer Spaniel", + "Welsh Springer Spaniel", + "Cocker Spaniels", + "Sussex Spaniel", + "Irish Water Spaniel", + "Kuvasz", + "Schipperke", + "Groenendael", + "Malinois", + "Briard", + "Australian Kelpie", + "Komondor", + "Old English Sheepdog", + "Shetland Sheepdog", + "collie", + "Border Collie", + "Bouvier des Flandres", + "Rottweiler", + "German Shepherd Dog", + "Dobermann", + "Miniature Pinscher", + "Greater Swiss Mountain Dog", + "Bernese Mountain Dog", + "Appenzeller Sennenhund", + "Entlebucher Sennenhund", + "Boxer", + "Bullmastiff", + "Tibetan Mastiff", + "French Bulldog", + "Great Dane", + "St. Bernard", + "husky", + "Alaskan Malamute", + "Siberian Husky", + "Dalmatian", + "Affenpinscher", + "Basenji", + "pug", + "Leonberger", + "Newfoundland", + "Pyrenean Mountain Dog", + "Samoyed", + "Pomeranian", + "Chow Chow", + "Keeshond", + "Griffon Bruxellois", + "Pembroke Welsh Corgi", + "Cardigan Welsh Corgi", + "Toy Poodle", + "Miniature Poodle", + "Standard Poodle", + "Mexican hairless dog", + "grey wolf", + "Alaskan tundra wolf", + "red wolf", + "coyote", + "dingo", + "dhole", + "African wild dog", + "hyena", + "red fox", + "kit fox", + "Arctic fox", + "grey fox", + "tabby cat", + "tiger cat", + "Persian cat", + "Siamese cat", + "Egyptian Mau", + "cougar", + "lynx", + "leopard", + "snow leopard", + "jaguar", + "lion", + "tiger", + "cheetah", + "brown bear", + "American black bear", + "polar bear", + "sloth bear", + "mongoose", + "meerkat", + "tiger beetle", + "ladybug", + "ground beetle", + "longhorn beetle", + "leaf beetle", + "dung beetle", + "rhinoceros beetle", + "weevil", + "fly", + "bee", + "ant", + "grasshopper", + "cricket", + "stick insect", + "cockroach", + "mantis", + "cicada", + "leafhopper", + "lacewing", + "dragonfly", + "damselfly", + "red admiral", + "ringlet", + "monarch butterfly", + "small white", + "sulphur butterfly", + "gossamer-winged butterfly", + "starfish", + "sea urchin", + "sea cucumber", + "cottontail rabbit", + "hare", + "Angora rabbit", + "hamster", + "porcupine", + "fox squirrel", + "marmot", + "beaver", + "guinea pig", + "common sorrel", + "zebra", + "pig", + "wild boar", + "warthog", + "hippopotamus", + "ox", + "water buffalo", + "bison", + "ram", + "bighorn sheep", + "Alpine ibex", + "hartebeest", + "impala", + "gazelle", + "dromedary", + "llama", + "weasel", + "mink", + "European polecat", + "black-footed ferret", + "otter", + "skunk", + "badger", + "armadillo", + "three-toed sloth", + "orangutan", + "gorilla", + "chimpanzee", + "gibbon", + "siamang", + "guenon", + "patas monkey", + "baboon", + "macaque", + "langur", + "black-and-white colobus", + "proboscis monkey", + "marmoset", + "white-headed capuchin", + "howler monkey", + "titi", + "Geoffroy's spider monkey", + "common squirrel monkey", + "ring-tailed lemur", + "indri", + "Asian elephant", + "African bush elephant", + "red panda", + "giant panda", + "snoek", + "eel", + "coho salmon", + "rock beauty", + "clownfish", + "sturgeon", + "garfish", + "lionfish", + "pufferfish", + "abacus", + "abaya", + "academic gown", + "accordion", + "acoustic guitar", + "aircraft carrier", + "airliner", + "airship", + "altar", + "ambulance", + "amphibious vehicle", + "analog clock", + "apiary", + "apron", + "waste container", + "assault rifle", + "backpack", + "bakery", + "balance beam", + "balloon", + "ballpoint pen", + "Band-Aid", + "banjo", + "baluster", + "barbell", + "barber chair", + "barbershop", + "barn", + "barometer", + "barrel", + "wheelbarrow", + "baseball", + "basketball", + "bassinet", + "bassoon", + "swimming cap", + "bath towel", + "bathtub", + "station wagon", + "lighthouse", + "beaker", + "military cap", + "beer bottle", + "beer glass", + "bell-cot", + "bib", + "tandem bicycle", + "bikini", + "ring binder", + "binoculars", + "birdhouse", + "boathouse", + "bobsleigh", + "bolo tie", + "poke bonnet", + "bookcase", + "bookstore", + "bottle cap", + "bow", + "bow tie", + "brass", + "bra", + "breakwater", + "breastplate", + "broom", + "bucket", + "buckle", + "bulletproof vest", + "high-speed train", + "butcher shop", + "taxicab", + "cauldron", + "candle", + "cannon", + "canoe", + "can opener", + "cardigan", + "car mirror", + "carousel", + "tool kit", + "carton", + "car wheel", + "automated teller machine", + "cassette", + "cassette player", + "castle", + "catamaran", + "CD player", + "cello", + "mobile phone", + "chain", + "chain-link fence", + "chain mail", + "chainsaw", + "chest", + "chiffonier", + "chime", + "china cabinet", + "Christmas stocking", + "church", + "movie theater", + "cleaver", + "cliff dwelling", + "cloak", + "clogs", + "cocktail shaker", + "coffee mug", + "coffeemaker", + "coil", + "combination lock", + "computer keyboard", + "confectionery store", + "container ship", + "convertible", + "corkscrew", + "cornet", + "cowboy boot", + "cowboy hat", + "cradle", + "crane (machine)", + "crash helmet", + "crate", + "infant bed", + "Crock Pot", + "croquet ball", + "crutch", + "cuirass", + "dam", + "desk", + "desktop computer", + "rotary dial telephone", + "diaper", + "digital clock", + "digital watch", + "dining table", + "dishcloth", + "dishwasher", + "disc brake", + "dock", + "dog sled", + "dome", + "doormat", + "drilling rig", + "drum", + "drumstick", + "dumbbell", + "Dutch oven", + "electric fan", + "electric guitar", + "electric locomotive", + "entertainment center", + "envelope", + "espresso machine", + "face powder", + "feather boa", + "filing cabinet", + "fireboat", + "fire engine", + "fire screen sheet", + "flagpole", + "flute", + "folding chair", + "football helmet", + "forklift", + "fountain", + "fountain pen", + "four-poster bed", + "freight car", + "French horn", + "frying pan", + "fur coat", + "garbage truck", + "gas mask", + "gas pump", + "goblet", + "go-kart", + "golf ball", + "golf cart", + "gondola", + "gong", + "gown", + "grand piano", + "greenhouse", + "grille", + "grocery store", + "guillotine", + "barrette", + "hair spray", + "half-track", + "hammer", + "hamper", + "hair dryer", + "hand-held computer", + "handkerchief", + "hard disk drive", + "harmonica", + "harp", + "harvester", + "hatchet", + "holster", + "home theater", + "honeycomb", + "hook", + "hoop skirt", + "horizontal bar", + "horse-drawn vehicle", + "hourglass", + "iPod", + "clothes iron", + "jack-o'-lantern", + "jeans", + "jeep", + "T-shirt", + "jigsaw puzzle", + "pulled rickshaw", + "joystick", + "kimono", + "knee pad", + "knot", + "lab coat", + "ladle", + "lampshade", + "laptop computer", + "lawn mower", + "lens cap", + "paper knife", + "library", + "lifeboat", + "lighter", + "limousine", + "ocean liner", + "lipstick", + "slip-on shoe", + "lotion", + "speaker", + "loupe", + "sawmill", + "magnetic compass", + "mail bag", + "mailbox", + "tights", + "tank suit", + "manhole cover", + "maraca", + "marimba", + "mask", + "match", + "maypole", + "maze", + "measuring cup", + "medicine chest", + "megalith", + "microphone", + "microwave oven", + "military uniform", + "milk can", + "minibus", + "miniskirt", + "minivan", + "missile", + "mitten", + "mixing bowl", + "mobile home", + "Model T", + "modem", + "monastery", + "monitor", + "moped", + "mortar", + "square academic cap", + "mosque", + "mosquito net", + "scooter", + "mountain bike", + "tent", + "computer mouse", + "mousetrap", + "moving van", + "muzzle", + "nail", + "neck brace", + "necklace", + "nipple", + "notebook computer", + "obelisk", + "oboe", + "ocarina", + "odometer", + "oil filter", + "organ", + "oscilloscope", + "overskirt", + "bullock cart", + "oxygen mask", + "packet", + "paddle", + "paddle wheel", + "padlock", + "paintbrush", + "pajamas", + "palace", + "pan flute", + "paper towel", + "parachute", + "parallel bars", + "park bench", + "parking meter", + "passenger car", + "patio", + "payphone", + "pedestal", + "pencil case", + "pencil sharpener", + "perfume", + "Petri dish", + "photocopier", + "plectrum", + "Pickelhaube", + "picket fence", + "pickup truck", + "pier", + "piggy bank", + "pill bottle", + "pillow", + "ping-pong ball", + "pinwheel", + "pirate ship", + "pitcher", + "hand plane", + "planetarium", + "plastic bag", + "plate rack", + "plow", + "plunger", + "Polaroid camera", + "pole", + "police van", + "poncho", + "billiard table", + "soda bottle", + "pot", + "potter's wheel", + "power drill", + "prayer rug", + "printer", + "prison", + "projectile", + "projector", + "hockey puck", + "punching bag", + "purse", + "quill", + "quilt", + "race car", + "racket", + "radiator", + "radio", + "radio telescope", + "rain barrel", + "recreational vehicle", + "reel", + "reflex camera", + "refrigerator", + "remote control", + "restaurant", + "revolver", + "rifle", + "rocking chair", + "rotisserie", + "eraser", + "rugby ball", + "ruler", + "running shoe", + "safe", + "safety pin", + "salt shaker", + "sandal", + "sarong", + "saxophone", + "scabbard", + "weighing scale", + "school bus", + "schooner", + "scoreboard", + "CRT screen", + "screw", + "screwdriver", + "seat belt", + "sewing machine", + "shield", + "shoe store", + "shoji", + "shopping basket", + "shopping cart", + "shovel", + "shower cap", + "shower curtain", + "ski", + "ski mask", + "sleeping bag", + "slide rule", + "sliding door", + "slot machine", + "snorkel", + "snowmobile", + "snowplow", + "soap dispenser", + "soccer ball", + "sock", + "solar thermal collector", + "sombrero", + "soup bowl", + "space bar", + "space heater", + "space shuttle", + "spatula", + "motorboat", + "spider web", + "spindle", + "sports car", + "spotlight", + "stage", + "steam locomotive", + "through arch bridge", + "steel drum", + "stethoscope", + "scarf", + "stone wall", + "stopwatch", + "stove", + "strainer", + "tram", + "stretcher", + "couch", + "stupa", + "submarine", + "suit", + "sundial", + "sunglass", + "sunglasses", + "sunscreen", + "suspension bridge", + "mop", + "sweatshirt", + "swimsuit", + "swing", + "switch", + "syringe", + "table lamp", + "tank", + "tape player", + "teapot", + "teddy bear", + "television", + "tennis ball", + "thatched roof", + "front curtain", + "thimble", + "threshing machine", + "throne", + "tile roof", + "toaster", + "tobacco shop", + "toilet seat", + "torch", + "totem pole", + "tow truck", + "toy store", + "tractor", + "semi-trailer truck", + "tray", + "trench coat", + "tricycle", + "trimaran", + "tripod", + "triumphal arch", + "trolleybus", + "trombone", + "tub", + "turnstile", + "typewriter keyboard", + "umbrella", + "unicycle", + "upright piano", + "vacuum cleaner", + "vase", + "vault", + "velvet", + "vending machine", + "vestment", + "viaduct", + "violin", + "volleyball", + "waffle iron", + "wall clock", + "wallet", + "wardrobe", + "military aircraft", + "sink", + "washing machine", + "water bottle", + "water jug", + "water tower", + "whiskey jug", + "whistle", + "wig", + "window screen", + "window shade", + "Windsor tie", + "wine bottle", + "wing", + "wok", + "wooden spoon", + "wool", + "split-rail fence", + "shipwreck", + "yawl", + "yurt", + "website", + "comic book", + "crossword", + "traffic sign", + "traffic light", + "dust jacket", + "menu", + "plate", + "guacamole", + "consomme", + "hot pot", + "trifle", + "ice cream", + "ice pop", + "baguette", + "bagel", + "pretzel", + "cheeseburger", + "hot dog", + "mashed potato", + "cabbage", + "broccoli", + "cauliflower", + "zucchini", + "spaghetti squash", + "acorn squash", + "butternut squash", + "cucumber", + "artichoke", + "bell pepper", + "cardoon", + "mushroom", + "Granny Smith", + "strawberry", + "orange", + "lemon", + "fig", + "pineapple", + "banana", + "jackfruit", + "custard apple", + "pomegranate", + "hay", + "carbonara", + "chocolate syrup", + "dough", + "meatloaf", + "pizza", + "pot pie", + "burrito", + "red wine", + "espresso", + "cup", + "eggnog", + "alp", + "bubble", + "cliff", + "coral reef", + "geyser", + "lakeshore", + "promontory", + "shoal", + "seashore", + "valley", + "volcano", + "baseball player", + "bridegroom", + "scuba diver", + "rapeseed", + "daisy", + "yellow lady's slipper", + "corn", + "acorn", + "rose hip", + "horse chestnut seed", + "coral fungus", + "agaric", + "gyromitra", + "stinkhorn mushroom", + "earth star", + "hen-of-the-woods", + "bolete", + "ear", + "toilet paper"] \ No newline at end of file diff --git a/gradio/media_assets/data/s1.srt b/gradio/media_assets/data/s1.srt new file mode 100644 index 0000000..c9d3854 --- /dev/null +++ b/gradio/media_assets/data/s1.srt @@ -0,0 +1,29 @@ +0 +0:00:00.000 --> 0:00:00.500 +A + +1 +0:00:00.500 --> 0:00:01.000 +B + +2 +0:00:01.000 --> 0:00:01.500 +C + +3 +0:00:01.500 --> 0:00:02.000 +D + +4 +0:00:02.000 --> 0:00:02.500 +E + +5 +0:00:02.500 --> 0:00:03.000 +F + +6 +0:00:03.000 --> 0:00:10.000 +This is multiple line test. +Why did the tomato turn red? Because it saw the salad dressing. +In Africa, every 60 seconds a minute passes diff --git a/gradio/media_assets/data/s2.vtt b/gradio/media_assets/data/s2.vtt new file mode 100644 index 0000000..4fa1878 --- /dev/null +++ b/gradio/media_assets/data/s2.vtt @@ -0,0 +1,16 @@ +WEBVTT + +0:00:00.000 --> 0:00:00.500 +一 + +0:00:00.500 --> 0:00:01.000 +二 + +0:00:01.000 --> 0:00:01.500 +三 + +0:00:01.500 --> 0:00:02.000 +四 + +0:00:02.000 --> 0:00:02.500 +中文測試 diff --git a/gradio/media_assets/data/s3.json b/gradio/media_assets/data/s3.json new file mode 100644 index 0000000..6b1ca61 --- /dev/null +++ b/gradio/media_assets/data/s3.json @@ -0,0 +1,22 @@ +[ + { + "text": "一", + "timestamp": [0.0, 0.5] + }, + { + "text": "二", + "timestamp": [0.5, 1.0] + }, + { + "text": "三", + "timestamp": [1.0, 1.5] + }, + { + "text": "四", + "timestamp": [1.5, 2.0] + }, + { + "text": "中文測試", + "timestamp": [2.0, 2.5] + } +] diff --git a/gradio/media_assets/data/sample.txt b/gradio/media_assets/data/sample.txt new file mode 100644 index 0000000..11f15e4 --- /dev/null +++ b/gradio/media_assets/data/sample.txt @@ -0,0 +1 @@ +hello friends \ No newline at end of file diff --git a/gradio/media_assets/data/time.csv b/gradio/media_assets/data/time.csv new file mode 100644 index 0000000..ddb2035 --- /dev/null +++ b/gradio/media_assets/data/time.csv @@ -0,0 +1,8 @@ +time,value,price +1,1,4 +2,3,8 +3,6,12 +4,10,16 +5,15,20 +6,21,24 +7,28,28 \ No newline at end of file diff --git a/gradio/media_assets/data/titanic.csv b/gradio/media_assets/data/titanic.csv new file mode 100644 index 0000000..63b68ab --- /dev/null +++ b/gradio/media_assets/data/titanic.csv @@ -0,0 +1,892 @@ +PassengerId,Survived,Pclass,Name,Sex,Age,SibSp,Parch,Ticket,Fare,Cabin,Embarked +1,0,3,"Braund, Mr. Owen Harris",male,22,1,0,A/5 21171,7.25,,S +2,1,1,"Cumings, Mrs. John Bradley (Florence Briggs Thayer)",female,38,1,0,PC 17599,71.2833,C85,C +3,1,3,"Heikkinen, Miss. Laina",female,26,0,0,STON/O2. 3101282,7.925,,S +4,1,1,"Futrelle, Mrs. Jacques Heath (Lily May Peel)",female,35,1,0,113803,53.1,C123,S +5,0,3,"Allen, Mr. William Henry",male,35,0,0,373450,8.05,,S +6,0,3,"Moran, Mr. James",male,,0,0,330877,8.4583,,Q +7,0,1,"McCarthy, Mr. Timothy J",male,54,0,0,17463,51.8625,E46,S +8,0,3,"Palsson, Master. Gosta Leonard",male,2,3,1,349909,21.075,,S +9,1,3,"Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)",female,27,0,2,347742,11.1333,,S +10,1,2,"Nasser, Mrs. Nicholas (Adele Achem)",female,14,1,0,237736,30.0708,,C +11,1,3,"Sandstrom, Miss. Marguerite Rut",female,4,1,1,PP 9549,16.7,G6,S +12,1,1,"Bonnell, Miss. Elizabeth",female,58,0,0,113783,26.55,C103,S +13,0,3,"Saundercock, Mr. William Henry",male,20,0,0,A/5. 2151,8.05,,S +14,0,3,"Andersson, Mr. Anders Johan",male,39,1,5,347082,31.275,,S +15,0,3,"Vestrom, Miss. Hulda Amanda Adolfina",female,14,0,0,350406,7.8542,,S +16,1,2,"Hewlett, Mrs. (Mary D Kingcome) ",female,55,0,0,248706,16,,S +17,0,3,"Rice, Master. Eugene",male,2,4,1,382652,29.125,,Q +18,1,2,"Williams, Mr. Charles Eugene",male,,0,0,244373,13,,S +19,0,3,"Vander Planke, Mrs. Julius (Emelia Maria Vandemoortele)",female,31,1,0,345763,18,,S +20,1,3,"Masselmani, Mrs. Fatima",female,,0,0,2649,7.225,,C +21,0,2,"Fynney, Mr. Joseph J",male,35,0,0,239865,26,,S +22,1,2,"Beesley, Mr. Lawrence",male,34,0,0,248698,13,D56,S +23,1,3,"McGowan, Miss. Anna ""Annie""",female,15,0,0,330923,8.0292,,Q +24,1,1,"Sloper, Mr. William Thompson",male,28,0,0,113788,35.5,A6,S +25,0,3,"Palsson, Miss. Torborg Danira",female,8,3,1,349909,21.075,,S +26,1,3,"Asplund, Mrs. Carl Oscar (Selma Augusta Emilia Johansson)",female,38,1,5,347077,31.3875,,S +27,0,3,"Emir, Mr. Farred Chehab",male,,0,0,2631,7.225,,C +28,0,1,"Fortune, Mr. Charles Alexander",male,19,3,2,19950,263,C23 C25 C27,S +29,1,3,"O'Dwyer, Miss. Ellen ""Nellie""",female,,0,0,330959,7.8792,,Q +30,0,3,"Todoroff, Mr. Lalio",male,,0,0,349216,7.8958,,S +31,0,1,"Uruchurtu, Don. Manuel E",male,40,0,0,PC 17601,27.7208,,C +32,1,1,"Spencer, Mrs. William Augustus (Marie Eugenie)",female,,1,0,PC 17569,146.5208,B78,C +33,1,3,"Glynn, Miss. Mary Agatha",female,,0,0,335677,7.75,,Q +34,0,2,"Wheadon, Mr. Edward H",male,66,0,0,C.A. 24579,10.5,,S +35,0,1,"Meyer, Mr. Edgar Joseph",male,28,1,0,PC 17604,82.1708,,C +36,0,1,"Holverson, Mr. Alexander Oskar",male,42,1,0,113789,52,,S +37,1,3,"Mamee, Mr. Hanna",male,,0,0,2677,7.2292,,C +38,0,3,"Cann, Mr. Ernest Charles",male,21,0,0,A./5. 2152,8.05,,S +39,0,3,"Vander Planke, Miss. Augusta Maria",female,18,2,0,345764,18,,S +40,1,3,"Nicola-Yarred, Miss. Jamila",female,14,1,0,2651,11.2417,,C +41,0,3,"Ahlin, Mrs. Johan (Johanna Persdotter Larsson)",female,40,1,0,7546,9.475,,S +42,0,2,"Turpin, Mrs. William John Robert (Dorothy Ann Wonnacott)",female,27,1,0,11668,21,,S +43,0,3,"Kraeff, Mr. Theodor",male,,0,0,349253,7.8958,,C +44,1,2,"Laroche, Miss. Simonne Marie Anne Andree",female,3,1,2,SC/Paris 2123,41.5792,,C +45,1,3,"Devaney, Miss. Margaret Delia",female,19,0,0,330958,7.8792,,Q +46,0,3,"Rogers, Mr. William John",male,,0,0,S.C./A.4. 23567,8.05,,S +47,0,3,"Lennon, Mr. Denis",male,,1,0,370371,15.5,,Q +48,1,3,"O'Driscoll, Miss. Bridget",female,,0,0,14311,7.75,,Q +49,0,3,"Samaan, Mr. Youssef",male,,2,0,2662,21.6792,,C +50,0,3,"Arnold-Franchi, Mrs. Josef (Josefine Franchi)",female,18,1,0,349237,17.8,,S +51,0,3,"Panula, Master. Juha Niilo",male,7,4,1,3101295,39.6875,,S +52,0,3,"Nosworthy, Mr. Richard Cater",male,21,0,0,A/4. 39886,7.8,,S +53,1,1,"Harper, Mrs. Henry Sleeper (Myna Haxtun)",female,49,1,0,PC 17572,76.7292,D33,C +54,1,2,"Faunthorpe, Mrs. Lizzie (Elizabeth Anne Wilkinson)",female,29,1,0,2926,26,,S +55,0,1,"Ostby, Mr. Engelhart Cornelius",male,65,0,1,113509,61.9792,B30,C +56,1,1,"Woolner, Mr. Hugh",male,,0,0,19947,35.5,C52,S +57,1,2,"Rugg, Miss. Emily",female,21,0,0,C.A. 31026,10.5,,S +58,0,3,"Novel, Mr. Mansouer",male,28.5,0,0,2697,7.2292,,C +59,1,2,"West, Miss. Constance Mirium",female,5,1,2,C.A. 34651,27.75,,S +60,0,3,"Goodwin, Master. William Frederick",male,11,5,2,CA 2144,46.9,,S +61,0,3,"Sirayanian, Mr. Orsen",male,22,0,0,2669,7.2292,,C +62,1,1,"Icard, Miss. Amelie",female,38,0,0,113572,80,B28, +63,0,1,"Harris, Mr. Henry Birkhardt",male,45,1,0,36973,83.475,C83,S +64,0,3,"Skoog, Master. Harald",male,4,3,2,347088,27.9,,S +65,0,1,"Stewart, Mr. Albert A",male,,0,0,PC 17605,27.7208,,C +66,1,3,"Moubarek, Master. Gerios",male,,1,1,2661,15.2458,,C +67,1,2,"Nye, Mrs. (Elizabeth Ramell)",female,29,0,0,C.A. 29395,10.5,F33,S +68,0,3,"Crease, Mr. Ernest James",male,19,0,0,S.P. 3464,8.1583,,S +69,1,3,"Andersson, Miss. Erna Alexandra",female,17,4,2,3101281,7.925,,S +70,0,3,"Kink, Mr. Vincenz",male,26,2,0,315151,8.6625,,S +71,0,2,"Jenkin, Mr. Stephen Curnow",male,32,0,0,C.A. 33111,10.5,,S +72,0,3,"Goodwin, Miss. Lillian Amy",female,16,5,2,CA 2144,46.9,,S +73,0,2,"Hood, Mr. Ambrose Jr",male,21,0,0,S.O.C. 14879,73.5,,S +74,0,3,"Chronopoulos, Mr. Apostolos",male,26,1,0,2680,14.4542,,C +75,1,3,"Bing, Mr. Lee",male,32,0,0,1601,56.4958,,S +76,0,3,"Moen, Mr. Sigurd Hansen",male,25,0,0,348123,7.65,F G73,S +77,0,3,"Staneff, Mr. Ivan",male,,0,0,349208,7.8958,,S +78,0,3,"Moutal, Mr. Rahamin Haim",male,,0,0,374746,8.05,,S +79,1,2,"Caldwell, Master. Alden Gates",male,0.83,0,2,248738,29,,S +80,1,3,"Dowdell, Miss. Elizabeth",female,30,0,0,364516,12.475,,S +81,0,3,"Waelens, Mr. Achille",male,22,0,0,345767,9,,S +82,1,3,"Sheerlinck, Mr. Jan Baptist",male,29,0,0,345779,9.5,,S +83,1,3,"McDermott, Miss. Brigdet Delia",female,,0,0,330932,7.7875,,Q +84,0,1,"Carrau, Mr. Francisco M",male,28,0,0,113059,47.1,,S +85,1,2,"Ilett, Miss. Bertha",female,17,0,0,SO/C 14885,10.5,,S +86,1,3,"Backstrom, Mrs. Karl Alfred (Maria Mathilda Gustafsson)",female,33,3,0,3101278,15.85,,S +87,0,3,"Ford, Mr. William Neal",male,16,1,3,W./C. 6608,34.375,,S +88,0,3,"Slocovski, Mr. Selman Francis",male,,0,0,SOTON/OQ 392086,8.05,,S +89,1,1,"Fortune, Miss. Mabel Helen",female,23,3,2,19950,263,C23 C25 C27,S +90,0,3,"Celotti, Mr. Francesco",male,24,0,0,343275,8.05,,S +91,0,3,"Christmann, Mr. Emil",male,29,0,0,343276,8.05,,S +92,0,3,"Andreasson, Mr. Paul Edvin",male,20,0,0,347466,7.8542,,S +93,0,1,"Chaffee, Mr. Herbert Fuller",male,46,1,0,W.E.P. 5734,61.175,E31,S +94,0,3,"Dean, Mr. Bertram Frank",male,26,1,2,C.A. 2315,20.575,,S +95,0,3,"Coxon, Mr. Daniel",male,59,0,0,364500,7.25,,S +96,0,3,"Shorney, Mr. Charles Joseph",male,,0,0,374910,8.05,,S +97,0,1,"Goldschmidt, Mr. George B",male,71,0,0,PC 17754,34.6542,A5,C +98,1,1,"Greenfield, Mr. William Bertram",male,23,0,1,PC 17759,63.3583,D10 D12,C +99,1,2,"Doling, Mrs. John T (Ada Julia Bone)",female,34,0,1,231919,23,,S +100,0,2,"Kantor, Mr. Sinai",male,34,1,0,244367,26,,S +101,0,3,"Petranec, Miss. Matilda",female,28,0,0,349245,7.8958,,S +102,0,3,"Petroff, Mr. Pastcho (""Pentcho"")",male,,0,0,349215,7.8958,,S +103,0,1,"White, Mr. Richard Frasar",male,21,0,1,35281,77.2875,D26,S +104,0,3,"Johansson, Mr. Gustaf Joel",male,33,0,0,7540,8.6542,,S +105,0,3,"Gustafsson, Mr. Anders Vilhelm",male,37,2,0,3101276,7.925,,S +106,0,3,"Mionoff, Mr. Stoytcho",male,28,0,0,349207,7.8958,,S +107,1,3,"Salkjelsvik, Miss. Anna Kristine",female,21,0,0,343120,7.65,,S +108,1,3,"Moss, Mr. Albert Johan",male,,0,0,312991,7.775,,S +109,0,3,"Rekic, Mr. Tido",male,38,0,0,349249,7.8958,,S +110,1,3,"Moran, Miss. Bertha",female,,1,0,371110,24.15,,Q +111,0,1,"Porter, Mr. Walter Chamberlain",male,47,0,0,110465,52,C110,S +112,0,3,"Zabour, Miss. Hileni",female,14.5,1,0,2665,14.4542,,C +113,0,3,"Barton, Mr. David John",male,22,0,0,324669,8.05,,S +114,0,3,"Jussila, Miss. Katriina",female,20,1,0,4136,9.825,,S +115,0,3,"Attalah, Miss. Malake",female,17,0,0,2627,14.4583,,C +116,0,3,"Pekoniemi, Mr. Edvard",male,21,0,0,STON/O 2. 3101294,7.925,,S +117,0,3,"Connors, Mr. Patrick",male,70.5,0,0,370369,7.75,,Q +118,0,2,"Turpin, Mr. William John Robert",male,29,1,0,11668,21,,S +119,0,1,"Baxter, Mr. Quigg Edmond",male,24,0,1,PC 17558,247.5208,B58 B60,C +120,0,3,"Andersson, Miss. Ellis Anna Maria",female,2,4,2,347082,31.275,,S +121,0,2,"Hickman, Mr. Stanley George",male,21,2,0,S.O.C. 14879,73.5,,S +122,0,3,"Moore, Mr. Leonard Charles",male,,0,0,A4. 54510,8.05,,S +123,0,2,"Nasser, Mr. Nicholas",male,32.5,1,0,237736,30.0708,,C +124,1,2,"Webber, Miss. Susan",female,32.5,0,0,27267,13,E101,S +125,0,1,"White, Mr. Percival Wayland",male,54,0,1,35281,77.2875,D26,S +126,1,3,"Nicola-Yarred, Master. Elias",male,12,1,0,2651,11.2417,,C +127,0,3,"McMahon, Mr. Martin",male,,0,0,370372,7.75,,Q +128,1,3,"Madsen, Mr. Fridtjof Arne",male,24,0,0,C 17369,7.1417,,S +129,1,3,"Peter, Miss. Anna",female,,1,1,2668,22.3583,F E69,C +130,0,3,"Ekstrom, Mr. Johan",male,45,0,0,347061,6.975,,S +131,0,3,"Drazenoic, Mr. Jozef",male,33,0,0,349241,7.8958,,C +132,0,3,"Coelho, Mr. Domingos Fernandeo",male,20,0,0,SOTON/O.Q. 3101307,7.05,,S +133,0,3,"Robins, Mrs. Alexander A (Grace Charity Laury)",female,47,1,0,A/5. 3337,14.5,,S +134,1,2,"Weisz, Mrs. Leopold (Mathilde Francoise Pede)",female,29,1,0,228414,26,,S +135,0,2,"Sobey, Mr. Samuel James Hayden",male,25,0,0,C.A. 29178,13,,S +136,0,2,"Richard, Mr. Emile",male,23,0,0,SC/PARIS 2133,15.0458,,C +137,1,1,"Newsom, Miss. Helen Monypeny",female,19,0,2,11752,26.2833,D47,S +138,0,1,"Futrelle, Mr. Jacques Heath",male,37,1,0,113803,53.1,C123,S +139,0,3,"Osen, Mr. Olaf Elon",male,16,0,0,7534,9.2167,,S +140,0,1,"Giglio, Mr. Victor",male,24,0,0,PC 17593,79.2,B86,C +141,0,3,"Boulos, Mrs. Joseph (Sultana)",female,,0,2,2678,15.2458,,C +142,1,3,"Nysten, Miss. Anna Sofia",female,22,0,0,347081,7.75,,S +143,1,3,"Hakkarainen, Mrs. Pekka Pietari (Elin Matilda Dolck)",female,24,1,0,STON/O2. 3101279,15.85,,S +144,0,3,"Burke, Mr. Jeremiah",male,19,0,0,365222,6.75,,Q +145,0,2,"Andrew, Mr. Edgardo Samuel",male,18,0,0,231945,11.5,,S +146,0,2,"Nicholls, Mr. Joseph Charles",male,19,1,1,C.A. 33112,36.75,,S +147,1,3,"Andersson, Mr. August Edvard (""Wennerstrom"")",male,27,0,0,350043,7.7958,,S +148,0,3,"Ford, Miss. Robina Maggie ""Ruby""",female,9,2,2,W./C. 6608,34.375,,S +149,0,2,"Navratil, Mr. Michel (""Louis M Hoffman"")",male,36.5,0,2,230080,26,F2,S +150,0,2,"Byles, Rev. Thomas Roussel Davids",male,42,0,0,244310,13,,S +151,0,2,"Bateman, Rev. Robert James",male,51,0,0,S.O.P. 1166,12.525,,S +152,1,1,"Pears, Mrs. Thomas (Edith Wearne)",female,22,1,0,113776,66.6,C2,S +153,0,3,"Meo, Mr. Alfonzo",male,55.5,0,0,A.5. 11206,8.05,,S +154,0,3,"van Billiard, Mr. Austin Blyler",male,40.5,0,2,A/5. 851,14.5,,S +155,0,3,"Olsen, Mr. Ole Martin",male,,0,0,Fa 265302,7.3125,,S +156,0,1,"Williams, Mr. Charles Duane",male,51,0,1,PC 17597,61.3792,,C +157,1,3,"Gilnagh, Miss. Katherine ""Katie""",female,16,0,0,35851,7.7333,,Q +158,0,3,"Corn, Mr. Harry",male,30,0,0,SOTON/OQ 392090,8.05,,S +159,0,3,"Smiljanic, Mr. Mile",male,,0,0,315037,8.6625,,S +160,0,3,"Sage, Master. Thomas Henry",male,,8,2,CA. 2343,69.55,,S +161,0,3,"Cribb, Mr. John Hatfield",male,44,0,1,371362,16.1,,S +162,1,2,"Watt, Mrs. James (Elizabeth ""Bessie"" Inglis Milne)",female,40,0,0,C.A. 33595,15.75,,S +163,0,3,"Bengtsson, Mr. John Viktor",male,26,0,0,347068,7.775,,S +164,0,3,"Calic, Mr. Jovo",male,17,0,0,315093,8.6625,,S +165,0,3,"Panula, Master. Eino Viljami",male,1,4,1,3101295,39.6875,,S +166,1,3,"Goldsmith, Master. Frank John William ""Frankie""",male,9,0,2,363291,20.525,,S +167,1,1,"Chibnall, Mrs. (Edith Martha Bowerman)",female,,0,1,113505,55,E33,S +168,0,3,"Skoog, Mrs. William (Anna Bernhardina Karlsson)",female,45,1,4,347088,27.9,,S +169,0,1,"Baumann, Mr. John D",male,,0,0,PC 17318,25.925,,S +170,0,3,"Ling, Mr. Lee",male,28,0,0,1601,56.4958,,S +171,0,1,"Van der hoef, Mr. Wyckoff",male,61,0,0,111240,33.5,B19,S +172,0,3,"Rice, Master. Arthur",male,4,4,1,382652,29.125,,Q +173,1,3,"Johnson, Miss. Eleanor Ileen",female,1,1,1,347742,11.1333,,S +174,0,3,"Sivola, Mr. Antti Wilhelm",male,21,0,0,STON/O 2. 3101280,7.925,,S +175,0,1,"Smith, Mr. James Clinch",male,56,0,0,17764,30.6958,A7,C +176,0,3,"Klasen, Mr. Klas Albin",male,18,1,1,350404,7.8542,,S +177,0,3,"Lefebre, Master. Henry Forbes",male,,3,1,4133,25.4667,,S +178,0,1,"Isham, Miss. Ann Elizabeth",female,50,0,0,PC 17595,28.7125,C49,C +179,0,2,"Hale, Mr. Reginald",male,30,0,0,250653,13,,S +180,0,3,"Leonard, Mr. Lionel",male,36,0,0,LINE,0,,S +181,0,3,"Sage, Miss. Constance Gladys",female,,8,2,CA. 2343,69.55,,S +182,0,2,"Pernot, Mr. Rene",male,,0,0,SC/PARIS 2131,15.05,,C +183,0,3,"Asplund, Master. Clarence Gustaf Hugo",male,9,4,2,347077,31.3875,,S +184,1,2,"Becker, Master. Richard F",male,1,2,1,230136,39,F4,S +185,1,3,"Kink-Heilmann, Miss. Luise Gretchen",female,4,0,2,315153,22.025,,S +186,0,1,"Rood, Mr. Hugh Roscoe",male,,0,0,113767,50,A32,S +187,1,3,"O'Brien, Mrs. Thomas (Johanna ""Hannah"" Godfrey)",female,,1,0,370365,15.5,,Q +188,1,1,"Romaine, Mr. Charles Hallace (""Mr C Rolmane"")",male,45,0,0,111428,26.55,,S +189,0,3,"Bourke, Mr. John",male,40,1,1,364849,15.5,,Q +190,0,3,"Turcin, Mr. Stjepan",male,36,0,0,349247,7.8958,,S +191,1,2,"Pinsky, Mrs. (Rosa)",female,32,0,0,234604,13,,S +192,0,2,"Carbines, Mr. William",male,19,0,0,28424,13,,S +193,1,3,"Andersen-Jensen, Miss. Carla Christine Nielsine",female,19,1,0,350046,7.8542,,S +194,1,2,"Navratil, Master. Michel M",male,3,1,1,230080,26,F2,S +195,1,1,"Brown, Mrs. James Joseph (Margaret Tobin)",female,44,0,0,PC 17610,27.7208,B4,C +196,1,1,"Lurette, Miss. Elise",female,58,0,0,PC 17569,146.5208,B80,C +197,0,3,"Mernagh, Mr. Robert",male,,0,0,368703,7.75,,Q +198,0,3,"Olsen, Mr. Karl Siegwart Andreas",male,42,0,1,4579,8.4042,,S +199,1,3,"Madigan, Miss. Margaret ""Maggie""",female,,0,0,370370,7.75,,Q +200,0,2,"Yrois, Miss. Henriette (""Mrs Harbeck"")",female,24,0,0,248747,13,,S +201,0,3,"Vande Walle, Mr. Nestor Cyriel",male,28,0,0,345770,9.5,,S +202,0,3,"Sage, Mr. Frederick",male,,8,2,CA. 2343,69.55,,S +203,0,3,"Johanson, Mr. Jakob Alfred",male,34,0,0,3101264,6.4958,,S +204,0,3,"Youseff, Mr. Gerious",male,45.5,0,0,2628,7.225,,C +205,1,3,"Cohen, Mr. Gurshon ""Gus""",male,18,0,0,A/5 3540,8.05,,S +206,0,3,"Strom, Miss. Telma Matilda",female,2,0,1,347054,10.4625,G6,S +207,0,3,"Backstrom, Mr. Karl Alfred",male,32,1,0,3101278,15.85,,S +208,1,3,"Albimona, Mr. Nassef Cassem",male,26,0,0,2699,18.7875,,C +209,1,3,"Carr, Miss. Helen ""Ellen""",female,16,0,0,367231,7.75,,Q +210,1,1,"Blank, Mr. Henry",male,40,0,0,112277,31,A31,C +211,0,3,"Ali, Mr. Ahmed",male,24,0,0,SOTON/O.Q. 3101311,7.05,,S +212,1,2,"Cameron, Miss. Clear Annie",female,35,0,0,F.C.C. 13528,21,,S +213,0,3,"Perkin, Mr. John Henry",male,22,0,0,A/5 21174,7.25,,S +214,0,2,"Givard, Mr. Hans Kristensen",male,30,0,0,250646,13,,S +215,0,3,"Kiernan, Mr. Philip",male,,1,0,367229,7.75,,Q +216,1,1,"Newell, Miss. Madeleine",female,31,1,0,35273,113.275,D36,C +217,1,3,"Honkanen, Miss. Eliina",female,27,0,0,STON/O2. 3101283,7.925,,S +218,0,2,"Jacobsohn, Mr. Sidney Samuel",male,42,1,0,243847,27,,S +219,1,1,"Bazzani, Miss. Albina",female,32,0,0,11813,76.2917,D15,C +220,0,2,"Harris, Mr. Walter",male,30,0,0,W/C 14208,10.5,,S +221,1,3,"Sunderland, Mr. Victor Francis",male,16,0,0,SOTON/OQ 392089,8.05,,S +222,0,2,"Bracken, Mr. James H",male,27,0,0,220367,13,,S +223,0,3,"Green, Mr. George Henry",male,51,0,0,21440,8.05,,S +224,0,3,"Nenkoff, Mr. Christo",male,,0,0,349234,7.8958,,S +225,1,1,"Hoyt, Mr. Frederick Maxfield",male,38,1,0,19943,90,C93,S +226,0,3,"Berglund, Mr. Karl Ivar Sven",male,22,0,0,PP 4348,9.35,,S +227,1,2,"Mellors, Mr. William John",male,19,0,0,SW/PP 751,10.5,,S +228,0,3,"Lovell, Mr. John Hall (""Henry"")",male,20.5,0,0,A/5 21173,7.25,,S +229,0,2,"Fahlstrom, Mr. Arne Jonas",male,18,0,0,236171,13,,S +230,0,3,"Lefebre, Miss. Mathilde",female,,3,1,4133,25.4667,,S +231,1,1,"Harris, Mrs. Henry Birkhardt (Irene Wallach)",female,35,1,0,36973,83.475,C83,S +232,0,3,"Larsson, Mr. Bengt Edvin",male,29,0,0,347067,7.775,,S +233,0,2,"Sjostedt, Mr. Ernst Adolf",male,59,0,0,237442,13.5,,S +234,1,3,"Asplund, Miss. Lillian Gertrud",female,5,4,2,347077,31.3875,,S +235,0,2,"Leyson, Mr. Robert William Norman",male,24,0,0,C.A. 29566,10.5,,S +236,0,3,"Harknett, Miss. Alice Phoebe",female,,0,0,W./C. 6609,7.55,,S +237,0,2,"Hold, Mr. Stephen",male,44,1,0,26707,26,,S +238,1,2,"Collyer, Miss. Marjorie ""Lottie""",female,8,0,2,C.A. 31921,26.25,,S +239,0,2,"Pengelly, Mr. Frederick William",male,19,0,0,28665,10.5,,S +240,0,2,"Hunt, Mr. George Henry",male,33,0,0,SCO/W 1585,12.275,,S +241,0,3,"Zabour, Miss. Thamine",female,,1,0,2665,14.4542,,C +242,1,3,"Murphy, Miss. Katherine ""Kate""",female,,1,0,367230,15.5,,Q +243,0,2,"Coleridge, Mr. Reginald Charles",male,29,0,0,W./C. 14263,10.5,,S +244,0,3,"Maenpaa, Mr. Matti Alexanteri",male,22,0,0,STON/O 2. 3101275,7.125,,S +245,0,3,"Attalah, Mr. Sleiman",male,30,0,0,2694,7.225,,C +246,0,1,"Minahan, Dr. William Edward",male,44,2,0,19928,90,C78,Q +247,0,3,"Lindahl, Miss. Agda Thorilda Viktoria",female,25,0,0,347071,7.775,,S +248,1,2,"Hamalainen, Mrs. William (Anna)",female,24,0,2,250649,14.5,,S +249,1,1,"Beckwith, Mr. Richard Leonard",male,37,1,1,11751,52.5542,D35,S +250,0,2,"Carter, Rev. Ernest Courtenay",male,54,1,0,244252,26,,S +251,0,3,"Reed, Mr. James George",male,,0,0,362316,7.25,,S +252,0,3,"Strom, Mrs. Wilhelm (Elna Matilda Persson)",female,29,1,1,347054,10.4625,G6,S +253,0,1,"Stead, Mr. William Thomas",male,62,0,0,113514,26.55,C87,S +254,0,3,"Lobb, Mr. William Arthur",male,30,1,0,A/5. 3336,16.1,,S +255,0,3,"Rosblom, Mrs. Viktor (Helena Wilhelmina)",female,41,0,2,370129,20.2125,,S +256,1,3,"Touma, Mrs. Darwis (Hanne Youssef Razi)",female,29,0,2,2650,15.2458,,C +257,1,1,"Thorne, Mrs. Gertrude Maybelle",female,,0,0,PC 17585,79.2,,C +258,1,1,"Cherry, Miss. Gladys",female,30,0,0,110152,86.5,B77,S +259,1,1,"Ward, Miss. Anna",female,35,0,0,PC 17755,512.3292,,C +260,1,2,"Parrish, Mrs. (Lutie Davis)",female,50,0,1,230433,26,,S +261,0,3,"Smith, Mr. Thomas",male,,0,0,384461,7.75,,Q +262,1,3,"Asplund, Master. Edvin Rojj Felix",male,3,4,2,347077,31.3875,,S +263,0,1,"Taussig, Mr. Emil",male,52,1,1,110413,79.65,E67,S +264,0,1,"Harrison, Mr. William",male,40,0,0,112059,0,B94,S +265,0,3,"Henry, Miss. Delia",female,,0,0,382649,7.75,,Q +266,0,2,"Reeves, Mr. David",male,36,0,0,C.A. 17248,10.5,,S +267,0,3,"Panula, Mr. Ernesti Arvid",male,16,4,1,3101295,39.6875,,S +268,1,3,"Persson, Mr. Ernst Ulrik",male,25,1,0,347083,7.775,,S +269,1,1,"Graham, Mrs. William Thompson (Edith Junkins)",female,58,0,1,PC 17582,153.4625,C125,S +270,1,1,"Bissette, Miss. Amelia",female,35,0,0,PC 17760,135.6333,C99,S +271,0,1,"Cairns, Mr. Alexander",male,,0,0,113798,31,,S +272,1,3,"Tornquist, Mr. William Henry",male,25,0,0,LINE,0,,S +273,1,2,"Mellinger, Mrs. (Elizabeth Anne Maidment)",female,41,0,1,250644,19.5,,S +274,0,1,"Natsch, Mr. Charles H",male,37,0,1,PC 17596,29.7,C118,C +275,1,3,"Healy, Miss. Hanora ""Nora""",female,,0,0,370375,7.75,,Q +276,1,1,"Andrews, Miss. Kornelia Theodosia",female,63,1,0,13502,77.9583,D7,S +277,0,3,"Lindblom, Miss. Augusta Charlotta",female,45,0,0,347073,7.75,,S +278,0,2,"Parkes, Mr. Francis ""Frank""",male,,0,0,239853,0,,S +279,0,3,"Rice, Master. Eric",male,7,4,1,382652,29.125,,Q +280,1,3,"Abbott, Mrs. Stanton (Rosa Hunt)",female,35,1,1,C.A. 2673,20.25,,S +281,0,3,"Duane, Mr. Frank",male,65,0,0,336439,7.75,,Q +282,0,3,"Olsson, Mr. Nils Johan Goransson",male,28,0,0,347464,7.8542,,S +283,0,3,"de Pelsmaeker, Mr. Alfons",male,16,0,0,345778,9.5,,S +284,1,3,"Dorking, Mr. Edward Arthur",male,19,0,0,A/5. 10482,8.05,,S +285,0,1,"Smith, Mr. Richard William",male,,0,0,113056,26,A19,S +286,0,3,"Stankovic, Mr. Ivan",male,33,0,0,349239,8.6625,,C +287,1,3,"de Mulder, Mr. Theodore",male,30,0,0,345774,9.5,,S +288,0,3,"Naidenoff, Mr. Penko",male,22,0,0,349206,7.8958,,S +289,1,2,"Hosono, Mr. Masabumi",male,42,0,0,237798,13,,S +290,1,3,"Connolly, Miss. Kate",female,22,0,0,370373,7.75,,Q +291,1,1,"Barber, Miss. Ellen ""Nellie""",female,26,0,0,19877,78.85,,S +292,1,1,"Bishop, Mrs. Dickinson H (Helen Walton)",female,19,1,0,11967,91.0792,B49,C +293,0,2,"Levy, Mr. Rene Jacques",male,36,0,0,SC/Paris 2163,12.875,D,C +294,0,3,"Haas, Miss. Aloisia",female,24,0,0,349236,8.85,,S +295,0,3,"Mineff, Mr. Ivan",male,24,0,0,349233,7.8958,,S +296,0,1,"Lewy, Mr. Ervin G",male,,0,0,PC 17612,27.7208,,C +297,0,3,"Hanna, Mr. Mansour",male,23.5,0,0,2693,7.2292,,C +298,0,1,"Allison, Miss. Helen Loraine",female,2,1,2,113781,151.55,C22 C26,S +299,1,1,"Saalfeld, Mr. Adolphe",male,,0,0,19988,30.5,C106,S +300,1,1,"Baxter, Mrs. James (Helene DeLaudeniere Chaput)",female,50,0,1,PC 17558,247.5208,B58 B60,C +301,1,3,"Kelly, Miss. Anna Katherine ""Annie Kate""",female,,0,0,9234,7.75,,Q +302,1,3,"McCoy, Mr. Bernard",male,,2,0,367226,23.25,,Q +303,0,3,"Johnson, Mr. William Cahoone Jr",male,19,0,0,LINE,0,,S +304,1,2,"Keane, Miss. Nora A",female,,0,0,226593,12.35,E101,Q +305,0,3,"Williams, Mr. Howard Hugh ""Harry""",male,,0,0,A/5 2466,8.05,,S +306,1,1,"Allison, Master. Hudson Trevor",male,0.92,1,2,113781,151.55,C22 C26,S +307,1,1,"Fleming, Miss. Margaret",female,,0,0,17421,110.8833,,C +308,1,1,"Penasco y Castellana, Mrs. Victor de Satode (Maria Josefa Perez de Soto y Vallejo)",female,17,1,0,PC 17758,108.9,C65,C +309,0,2,"Abelson, Mr. Samuel",male,30,1,0,P/PP 3381,24,,C +310,1,1,"Francatelli, Miss. Laura Mabel",female,30,0,0,PC 17485,56.9292,E36,C +311,1,1,"Hays, Miss. Margaret Bechstein",female,24,0,0,11767,83.1583,C54,C +312,1,1,"Ryerson, Miss. Emily Borie",female,18,2,2,PC 17608,262.375,B57 B59 B63 B66,C +313,0,2,"Lahtinen, Mrs. William (Anna Sylfven)",female,26,1,1,250651,26,,S +314,0,3,"Hendekovic, Mr. Ignjac",male,28,0,0,349243,7.8958,,S +315,0,2,"Hart, Mr. Benjamin",male,43,1,1,F.C.C. 13529,26.25,,S +316,1,3,"Nilsson, Miss. Helmina Josefina",female,26,0,0,347470,7.8542,,S +317,1,2,"Kantor, Mrs. Sinai (Miriam Sternin)",female,24,1,0,244367,26,,S +318,0,2,"Moraweck, Dr. Ernest",male,54,0,0,29011,14,,S +319,1,1,"Wick, Miss. Mary Natalie",female,31,0,2,36928,164.8667,C7,S +320,1,1,"Spedden, Mrs. Frederic Oakley (Margaretta Corning Stone)",female,40,1,1,16966,134.5,E34,C +321,0,3,"Dennis, Mr. Samuel",male,22,0,0,A/5 21172,7.25,,S +322,0,3,"Danoff, Mr. Yoto",male,27,0,0,349219,7.8958,,S +323,1,2,"Slayter, Miss. Hilda Mary",female,30,0,0,234818,12.35,,Q +324,1,2,"Caldwell, Mrs. Albert Francis (Sylvia Mae Harbaugh)",female,22,1,1,248738,29,,S +325,0,3,"Sage, Mr. George John Jr",male,,8,2,CA. 2343,69.55,,S +326,1,1,"Young, Miss. Marie Grice",female,36,0,0,PC 17760,135.6333,C32,C +327,0,3,"Nysveen, Mr. Johan Hansen",male,61,0,0,345364,6.2375,,S +328,1,2,"Ball, Mrs. (Ada E Hall)",female,36,0,0,28551,13,D,S +329,1,3,"Goldsmith, Mrs. Frank John (Emily Alice Brown)",female,31,1,1,363291,20.525,,S +330,1,1,"Hippach, Miss. Jean Gertrude",female,16,0,1,111361,57.9792,B18,C +331,1,3,"McCoy, Miss. Agnes",female,,2,0,367226,23.25,,Q +332,0,1,"Partner, Mr. Austen",male,45.5,0,0,113043,28.5,C124,S +333,0,1,"Graham, Mr. George Edward",male,38,0,1,PC 17582,153.4625,C91,S +334,0,3,"Vander Planke, Mr. Leo Edmondus",male,16,2,0,345764,18,,S +335,1,1,"Frauenthal, Mrs. Henry William (Clara Heinsheimer)",female,,1,0,PC 17611,133.65,,S +336,0,3,"Denkoff, Mr. Mitto",male,,0,0,349225,7.8958,,S +337,0,1,"Pears, Mr. Thomas Clinton",male,29,1,0,113776,66.6,C2,S +338,1,1,"Burns, Miss. Elizabeth Margaret",female,41,0,0,16966,134.5,E40,C +339,1,3,"Dahl, Mr. Karl Edwart",male,45,0,0,7598,8.05,,S +340,0,1,"Blackwell, Mr. Stephen Weart",male,45,0,0,113784,35.5,T,S +341,1,2,"Navratil, Master. Edmond Roger",male,2,1,1,230080,26,F2,S +342,1,1,"Fortune, Miss. Alice Elizabeth",female,24,3,2,19950,263,C23 C25 C27,S +343,0,2,"Collander, Mr. Erik Gustaf",male,28,0,0,248740,13,,S +344,0,2,"Sedgwick, Mr. Charles Frederick Waddington",male,25,0,0,244361,13,,S +345,0,2,"Fox, Mr. Stanley Hubert",male,36,0,0,229236,13,,S +346,1,2,"Brown, Miss. Amelia ""Mildred""",female,24,0,0,248733,13,F33,S +347,1,2,"Smith, Miss. Marion Elsie",female,40,0,0,31418,13,,S +348,1,3,"Davison, Mrs. Thomas Henry (Mary E Finck)",female,,1,0,386525,16.1,,S +349,1,3,"Coutts, Master. William Loch ""William""",male,3,1,1,C.A. 37671,15.9,,S +350,0,3,"Dimic, Mr. Jovan",male,42,0,0,315088,8.6625,,S +351,0,3,"Odahl, Mr. Nils Martin",male,23,0,0,7267,9.225,,S +352,0,1,"Williams-Lambert, Mr. Fletcher Fellows",male,,0,0,113510,35,C128,S +353,0,3,"Elias, Mr. Tannous",male,15,1,1,2695,7.2292,,C +354,0,3,"Arnold-Franchi, Mr. Josef",male,25,1,0,349237,17.8,,S +355,0,3,"Yousif, Mr. Wazli",male,,0,0,2647,7.225,,C +356,0,3,"Vanden Steen, Mr. Leo Peter",male,28,0,0,345783,9.5,,S +357,1,1,"Bowerman, Miss. Elsie Edith",female,22,0,1,113505,55,E33,S +358,0,2,"Funk, Miss. Annie Clemmer",female,38,0,0,237671,13,,S +359,1,3,"McGovern, Miss. Mary",female,,0,0,330931,7.8792,,Q +360,1,3,"Mockler, Miss. Helen Mary ""Ellie""",female,,0,0,330980,7.8792,,Q +361,0,3,"Skoog, Mr. Wilhelm",male,40,1,4,347088,27.9,,S +362,0,2,"del Carlo, Mr. Sebastiano",male,29,1,0,SC/PARIS 2167,27.7208,,C +363,0,3,"Barbara, Mrs. (Catherine David)",female,45,0,1,2691,14.4542,,C +364,0,3,"Asim, Mr. Adola",male,35,0,0,SOTON/O.Q. 3101310,7.05,,S +365,0,3,"O'Brien, Mr. Thomas",male,,1,0,370365,15.5,,Q +366,0,3,"Adahl, Mr. Mauritz Nils Martin",male,30,0,0,C 7076,7.25,,S +367,1,1,"Warren, Mrs. Frank Manley (Anna Sophia Atkinson)",female,60,1,0,110813,75.25,D37,C +368,1,3,"Moussa, Mrs. (Mantoura Boulos)",female,,0,0,2626,7.2292,,C +369,1,3,"Jermyn, Miss. Annie",female,,0,0,14313,7.75,,Q +370,1,1,"Aubart, Mme. Leontine Pauline",female,24,0,0,PC 17477,69.3,B35,C +371,1,1,"Harder, Mr. George Achilles",male,25,1,0,11765,55.4417,E50,C +372,0,3,"Wiklund, Mr. Jakob Alfred",male,18,1,0,3101267,6.4958,,S +373,0,3,"Beavan, Mr. William Thomas",male,19,0,0,323951,8.05,,S +374,0,1,"Ringhini, Mr. Sante",male,22,0,0,PC 17760,135.6333,,C +375,0,3,"Palsson, Miss. Stina Viola",female,3,3,1,349909,21.075,,S +376,1,1,"Meyer, Mrs. Edgar Joseph (Leila Saks)",female,,1,0,PC 17604,82.1708,,C +377,1,3,"Landergren, Miss. Aurora Adelia",female,22,0,0,C 7077,7.25,,S +378,0,1,"Widener, Mr. Harry Elkins",male,27,0,2,113503,211.5,C82,C +379,0,3,"Betros, Mr. Tannous",male,20,0,0,2648,4.0125,,C +380,0,3,"Gustafsson, Mr. Karl Gideon",male,19,0,0,347069,7.775,,S +381,1,1,"Bidois, Miss. Rosalie",female,42,0,0,PC 17757,227.525,,C +382,1,3,"Nakid, Miss. Maria (""Mary"")",female,1,0,2,2653,15.7417,,C +383,0,3,"Tikkanen, Mr. Juho",male,32,0,0,STON/O 2. 3101293,7.925,,S +384,1,1,"Holverson, Mrs. Alexander Oskar (Mary Aline Towner)",female,35,1,0,113789,52,,S +385,0,3,"Plotcharsky, Mr. Vasil",male,,0,0,349227,7.8958,,S +386,0,2,"Davies, Mr. Charles Henry",male,18,0,0,S.O.C. 14879,73.5,,S +387,0,3,"Goodwin, Master. Sidney Leonard",male,1,5,2,CA 2144,46.9,,S +388,1,2,"Buss, Miss. Kate",female,36,0,0,27849,13,,S +389,0,3,"Sadlier, Mr. Matthew",male,,0,0,367655,7.7292,,Q +390,1,2,"Lehmann, Miss. Bertha",female,17,0,0,SC 1748,12,,C +391,1,1,"Carter, Mr. William Ernest",male,36,1,2,113760,120,B96 B98,S +392,1,3,"Jansson, Mr. Carl Olof",male,21,0,0,350034,7.7958,,S +393,0,3,"Gustafsson, Mr. Johan Birger",male,28,2,0,3101277,7.925,,S +394,1,1,"Newell, Miss. Marjorie",female,23,1,0,35273,113.275,D36,C +395,1,3,"Sandstrom, Mrs. Hjalmar (Agnes Charlotta Bengtsson)",female,24,0,2,PP 9549,16.7,G6,S +396,0,3,"Johansson, Mr. Erik",male,22,0,0,350052,7.7958,,S +397,0,3,"Olsson, Miss. Elina",female,31,0,0,350407,7.8542,,S +398,0,2,"McKane, Mr. Peter David",male,46,0,0,28403,26,,S +399,0,2,"Pain, Dr. Alfred",male,23,0,0,244278,10.5,,S +400,1,2,"Trout, Mrs. William H (Jessie L)",female,28,0,0,240929,12.65,,S +401,1,3,"Niskanen, Mr. Juha",male,39,0,0,STON/O 2. 3101289,7.925,,S +402,0,3,"Adams, Mr. John",male,26,0,0,341826,8.05,,S +403,0,3,"Jussila, Miss. Mari Aina",female,21,1,0,4137,9.825,,S +404,0,3,"Hakkarainen, Mr. Pekka Pietari",male,28,1,0,STON/O2. 3101279,15.85,,S +405,0,3,"Oreskovic, Miss. Marija",female,20,0,0,315096,8.6625,,S +406,0,2,"Gale, Mr. Shadrach",male,34,1,0,28664,21,,S +407,0,3,"Widegren, Mr. Carl/Charles Peter",male,51,0,0,347064,7.75,,S +408,1,2,"Richards, Master. William Rowe",male,3,1,1,29106,18.75,,S +409,0,3,"Birkeland, Mr. Hans Martin Monsen",male,21,0,0,312992,7.775,,S +410,0,3,"Lefebre, Miss. Ida",female,,3,1,4133,25.4667,,S +411,0,3,"Sdycoff, Mr. Todor",male,,0,0,349222,7.8958,,S +412,0,3,"Hart, Mr. Henry",male,,0,0,394140,6.8583,,Q +413,1,1,"Minahan, Miss. Daisy E",female,33,1,0,19928,90,C78,Q +414,0,2,"Cunningham, Mr. Alfred Fleming",male,,0,0,239853,0,,S +415,1,3,"Sundman, Mr. Johan Julian",male,44,0,0,STON/O 2. 3101269,7.925,,S +416,0,3,"Meek, Mrs. Thomas (Annie Louise Rowley)",female,,0,0,343095,8.05,,S +417,1,2,"Drew, Mrs. James Vivian (Lulu Thorne Christian)",female,34,1,1,28220,32.5,,S +418,1,2,"Silven, Miss. Lyyli Karoliina",female,18,0,2,250652,13,,S +419,0,2,"Matthews, Mr. William John",male,30,0,0,28228,13,,S +420,0,3,"Van Impe, Miss. Catharina",female,10,0,2,345773,24.15,,S +421,0,3,"Gheorgheff, Mr. Stanio",male,,0,0,349254,7.8958,,C +422,0,3,"Charters, Mr. David",male,21,0,0,A/5. 13032,7.7333,,Q +423,0,3,"Zimmerman, Mr. Leo",male,29,0,0,315082,7.875,,S +424,0,3,"Danbom, Mrs. Ernst Gilbert (Anna Sigrid Maria Brogren)",female,28,1,1,347080,14.4,,S +425,0,3,"Rosblom, Mr. Viktor Richard",male,18,1,1,370129,20.2125,,S +426,0,3,"Wiseman, Mr. Phillippe",male,,0,0,A/4. 34244,7.25,,S +427,1,2,"Clarke, Mrs. Charles V (Ada Maria Winfield)",female,28,1,0,2003,26,,S +428,1,2,"Phillips, Miss. Kate Florence (""Mrs Kate Louise Phillips Marshall"")",female,19,0,0,250655,26,,S +429,0,3,"Flynn, Mr. James",male,,0,0,364851,7.75,,Q +430,1,3,"Pickard, Mr. Berk (Berk Trembisky)",male,32,0,0,SOTON/O.Q. 392078,8.05,E10,S +431,1,1,"Bjornstrom-Steffansson, Mr. Mauritz Hakan",male,28,0,0,110564,26.55,C52,S +432,1,3,"Thorneycroft, Mrs. Percival (Florence Kate White)",female,,1,0,376564,16.1,,S +433,1,2,"Louch, Mrs. Charles Alexander (Alice Adelaide Slow)",female,42,1,0,SC/AH 3085,26,,S +434,0,3,"Kallio, Mr. Nikolai Erland",male,17,0,0,STON/O 2. 3101274,7.125,,S +435,0,1,"Silvey, Mr. William Baird",male,50,1,0,13507,55.9,E44,S +436,1,1,"Carter, Miss. Lucile Polk",female,14,1,2,113760,120,B96 B98,S +437,0,3,"Ford, Miss. Doolina Margaret ""Daisy""",female,21,2,2,W./C. 6608,34.375,,S +438,1,2,"Richards, Mrs. Sidney (Emily Hocking)",female,24,2,3,29106,18.75,,S +439,0,1,"Fortune, Mr. Mark",male,64,1,4,19950,263,C23 C25 C27,S +440,0,2,"Kvillner, Mr. Johan Henrik Johannesson",male,31,0,0,C.A. 18723,10.5,,S +441,1,2,"Hart, Mrs. Benjamin (Esther Ada Bloomfield)",female,45,1,1,F.C.C. 13529,26.25,,S +442,0,3,"Hampe, Mr. Leon",male,20,0,0,345769,9.5,,S +443,0,3,"Petterson, Mr. Johan Emil",male,25,1,0,347076,7.775,,S +444,1,2,"Reynaldo, Ms. Encarnacion",female,28,0,0,230434,13,,S +445,1,3,"Johannesen-Bratthammer, Mr. Bernt",male,,0,0,65306,8.1125,,S +446,1,1,"Dodge, Master. Washington",male,4,0,2,33638,81.8583,A34,S +447,1,2,"Mellinger, Miss. Madeleine Violet",female,13,0,1,250644,19.5,,S +448,1,1,"Seward, Mr. Frederic Kimber",male,34,0,0,113794,26.55,,S +449,1,3,"Baclini, Miss. Marie Catherine",female,5,2,1,2666,19.2583,,C +450,1,1,"Peuchen, Major. Arthur Godfrey",male,52,0,0,113786,30.5,C104,S +451,0,2,"West, Mr. Edwy Arthur",male,36,1,2,C.A. 34651,27.75,,S +452,0,3,"Hagland, Mr. Ingvald Olai Olsen",male,,1,0,65303,19.9667,,S +453,0,1,"Foreman, Mr. Benjamin Laventall",male,30,0,0,113051,27.75,C111,C +454,1,1,"Goldenberg, Mr. Samuel L",male,49,1,0,17453,89.1042,C92,C +455,0,3,"Peduzzi, Mr. Joseph",male,,0,0,A/5 2817,8.05,,S +456,1,3,"Jalsevac, Mr. Ivan",male,29,0,0,349240,7.8958,,C +457,0,1,"Millet, Mr. Francis Davis",male,65,0,0,13509,26.55,E38,S +458,1,1,"Kenyon, Mrs. Frederick R (Marion)",female,,1,0,17464,51.8625,D21,S +459,1,2,"Toomey, Miss. Ellen",female,50,0,0,F.C.C. 13531,10.5,,S +460,0,3,"O'Connor, Mr. Maurice",male,,0,0,371060,7.75,,Q +461,1,1,"Anderson, Mr. Harry",male,48,0,0,19952,26.55,E12,S +462,0,3,"Morley, Mr. William",male,34,0,0,364506,8.05,,S +463,0,1,"Gee, Mr. Arthur H",male,47,0,0,111320,38.5,E63,S +464,0,2,"Milling, Mr. Jacob Christian",male,48,0,0,234360,13,,S +465,0,3,"Maisner, Mr. Simon",male,,0,0,A/S 2816,8.05,,S +466,0,3,"Goncalves, Mr. Manuel Estanslas",male,38,0,0,SOTON/O.Q. 3101306,7.05,,S +467,0,2,"Campbell, Mr. William",male,,0,0,239853,0,,S +468,0,1,"Smart, Mr. John Montgomery",male,56,0,0,113792,26.55,,S +469,0,3,"Scanlan, Mr. James",male,,0,0,36209,7.725,,Q +470,1,3,"Baclini, Miss. Helene Barbara",female,0.75,2,1,2666,19.2583,,C +471,0,3,"Keefe, Mr. Arthur",male,,0,0,323592,7.25,,S +472,0,3,"Cacic, Mr. Luka",male,38,0,0,315089,8.6625,,S +473,1,2,"West, Mrs. Edwy Arthur (Ada Mary Worth)",female,33,1,2,C.A. 34651,27.75,,S +474,1,2,"Jerwan, Mrs. Amin S (Marie Marthe Thuillard)",female,23,0,0,SC/AH Basle 541,13.7917,D,C +475,0,3,"Strandberg, Miss. Ida Sofia",female,22,0,0,7553,9.8375,,S +476,0,1,"Clifford, Mr. George Quincy",male,,0,0,110465,52,A14,S +477,0,2,"Renouf, Mr. Peter Henry",male,34,1,0,31027,21,,S +478,0,3,"Braund, Mr. Lewis Richard",male,29,1,0,3460,7.0458,,S +479,0,3,"Karlsson, Mr. Nils August",male,22,0,0,350060,7.5208,,S +480,1,3,"Hirvonen, Miss. Hildur E",female,2,0,1,3101298,12.2875,,S +481,0,3,"Goodwin, Master. Harold Victor",male,9,5,2,CA 2144,46.9,,S +482,0,2,"Frost, Mr. Anthony Wood ""Archie""",male,,0,0,239854,0,,S +483,0,3,"Rouse, Mr. Richard Henry",male,50,0,0,A/5 3594,8.05,,S +484,1,3,"Turkula, Mrs. (Hedwig)",female,63,0,0,4134,9.5875,,S +485,1,1,"Bishop, Mr. Dickinson H",male,25,1,0,11967,91.0792,B49,C +486,0,3,"Lefebre, Miss. Jeannie",female,,3,1,4133,25.4667,,S +487,1,1,"Hoyt, Mrs. Frederick Maxfield (Jane Anne Forby)",female,35,1,0,19943,90,C93,S +488,0,1,"Kent, Mr. Edward Austin",male,58,0,0,11771,29.7,B37,C +489,0,3,"Somerton, Mr. Francis William",male,30,0,0,A.5. 18509,8.05,,S +490,1,3,"Coutts, Master. Eden Leslie ""Neville""",male,9,1,1,C.A. 37671,15.9,,S +491,0,3,"Hagland, Mr. Konrad Mathias Reiersen",male,,1,0,65304,19.9667,,S +492,0,3,"Windelov, Mr. Einar",male,21,0,0,SOTON/OQ 3101317,7.25,,S +493,0,1,"Molson, Mr. Harry Markland",male,55,0,0,113787,30.5,C30,S +494,0,1,"Artagaveytia, Mr. Ramon",male,71,0,0,PC 17609,49.5042,,C +495,0,3,"Stanley, Mr. Edward Roland",male,21,0,0,A/4 45380,8.05,,S +496,0,3,"Yousseff, Mr. Gerious",male,,0,0,2627,14.4583,,C +497,1,1,"Eustis, Miss. Elizabeth Mussey",female,54,1,0,36947,78.2667,D20,C +498,0,3,"Shellard, Mr. Frederick William",male,,0,0,C.A. 6212,15.1,,S +499,0,1,"Allison, Mrs. Hudson J C (Bessie Waldo Daniels)",female,25,1,2,113781,151.55,C22 C26,S +500,0,3,"Svensson, Mr. Olof",male,24,0,0,350035,7.7958,,S +501,0,3,"Calic, Mr. Petar",male,17,0,0,315086,8.6625,,S +502,0,3,"Canavan, Miss. Mary",female,21,0,0,364846,7.75,,Q +503,0,3,"O'Sullivan, Miss. Bridget Mary",female,,0,0,330909,7.6292,,Q +504,0,3,"Laitinen, Miss. Kristina Sofia",female,37,0,0,4135,9.5875,,S +505,1,1,"Maioni, Miss. Roberta",female,16,0,0,110152,86.5,B79,S +506,0,1,"Penasco y Castellana, Mr. Victor de Satode",male,18,1,0,PC 17758,108.9,C65,C +507,1,2,"Quick, Mrs. Frederick Charles (Jane Richards)",female,33,0,2,26360,26,,S +508,1,1,"Bradley, Mr. George (""George Arthur Brayton"")",male,,0,0,111427,26.55,,S +509,0,3,"Olsen, Mr. Henry Margido",male,28,0,0,C 4001,22.525,,S +510,1,3,"Lang, Mr. Fang",male,26,0,0,1601,56.4958,,S +511,1,3,"Daly, Mr. Eugene Patrick",male,29,0,0,382651,7.75,,Q +512,0,3,"Webber, Mr. James",male,,0,0,SOTON/OQ 3101316,8.05,,S +513,1,1,"McGough, Mr. James Robert",male,36,0,0,PC 17473,26.2875,E25,S +514,1,1,"Rothschild, Mrs. Martin (Elizabeth L. Barrett)",female,54,1,0,PC 17603,59.4,,C +515,0,3,"Coleff, Mr. Satio",male,24,0,0,349209,7.4958,,S +516,0,1,"Walker, Mr. William Anderson",male,47,0,0,36967,34.0208,D46,S +517,1,2,"Lemore, Mrs. (Amelia Milley)",female,34,0,0,C.A. 34260,10.5,F33,S +518,0,3,"Ryan, Mr. Patrick",male,,0,0,371110,24.15,,Q +519,1,2,"Angle, Mrs. William A (Florence ""Mary"" Agnes Hughes)",female,36,1,0,226875,26,,S +520,0,3,"Pavlovic, Mr. Stefo",male,32,0,0,349242,7.8958,,S +521,1,1,"Perreault, Miss. Anne",female,30,0,0,12749,93.5,B73,S +522,0,3,"Vovk, Mr. Janko",male,22,0,0,349252,7.8958,,S +523,0,3,"Lahoud, Mr. Sarkis",male,,0,0,2624,7.225,,C +524,1,1,"Hippach, Mrs. Louis Albert (Ida Sophia Fischer)",female,44,0,1,111361,57.9792,B18,C +525,0,3,"Kassem, Mr. Fared",male,,0,0,2700,7.2292,,C +526,0,3,"Farrell, Mr. James",male,40.5,0,0,367232,7.75,,Q +527,1,2,"Ridsdale, Miss. Lucy",female,50,0,0,W./C. 14258,10.5,,S +528,0,1,"Farthing, Mr. John",male,,0,0,PC 17483,221.7792,C95,S +529,0,3,"Salonen, Mr. Johan Werner",male,39,0,0,3101296,7.925,,S +530,0,2,"Hocking, Mr. Richard George",male,23,2,1,29104,11.5,,S +531,1,2,"Quick, Miss. Phyllis May",female,2,1,1,26360,26,,S +532,0,3,"Toufik, Mr. Nakli",male,,0,0,2641,7.2292,,C +533,0,3,"Elias, Mr. Joseph Jr",male,17,1,1,2690,7.2292,,C +534,1,3,"Peter, Mrs. Catherine (Catherine Rizk)",female,,0,2,2668,22.3583,,C +535,0,3,"Cacic, Miss. Marija",female,30,0,0,315084,8.6625,,S +536,1,2,"Hart, Miss. Eva Miriam",female,7,0,2,F.C.C. 13529,26.25,,S +537,0,1,"Butt, Major. Archibald Willingham",male,45,0,0,113050,26.55,B38,S +538,1,1,"LeRoy, Miss. Bertha",female,30,0,0,PC 17761,106.425,,C +539,0,3,"Risien, Mr. Samuel Beard",male,,0,0,364498,14.5,,S +540,1,1,"Frolicher, Miss. Hedwig Margaritha",female,22,0,2,13568,49.5,B39,C +541,1,1,"Crosby, Miss. Harriet R",female,36,0,2,WE/P 5735,71,B22,S +542,0,3,"Andersson, Miss. Ingeborg Constanzia",female,9,4,2,347082,31.275,,S +543,0,3,"Andersson, Miss. Sigrid Elisabeth",female,11,4,2,347082,31.275,,S +544,1,2,"Beane, Mr. Edward",male,32,1,0,2908,26,,S +545,0,1,"Douglas, Mr. Walter Donald",male,50,1,0,PC 17761,106.425,C86,C +546,0,1,"Nicholson, Mr. Arthur Ernest",male,64,0,0,693,26,,S +547,1,2,"Beane, Mrs. Edward (Ethel Clarke)",female,19,1,0,2908,26,,S +548,1,2,"Padro y Manent, Mr. Julian",male,,0,0,SC/PARIS 2146,13.8625,,C +549,0,3,"Goldsmith, Mr. Frank John",male,33,1,1,363291,20.525,,S +550,1,2,"Davies, Master. John Morgan Jr",male,8,1,1,C.A. 33112,36.75,,S +551,1,1,"Thayer, Mr. John Borland Jr",male,17,0,2,17421,110.8833,C70,C +552,0,2,"Sharp, Mr. Percival James R",male,27,0,0,244358,26,,S +553,0,3,"O'Brien, Mr. Timothy",male,,0,0,330979,7.8292,,Q +554,1,3,"Leeni, Mr. Fahim (""Philip Zenni"")",male,22,0,0,2620,7.225,,C +555,1,3,"Ohman, Miss. Velin",female,22,0,0,347085,7.775,,S +556,0,1,"Wright, Mr. George",male,62,0,0,113807,26.55,,S +557,1,1,"Duff Gordon, Lady. (Lucille Christiana Sutherland) (""Mrs Morgan"")",female,48,1,0,11755,39.6,A16,C +558,0,1,"Robbins, Mr. Victor",male,,0,0,PC 17757,227.525,,C +559,1,1,"Taussig, Mrs. Emil (Tillie Mandelbaum)",female,39,1,1,110413,79.65,E67,S +560,1,3,"de Messemaeker, Mrs. Guillaume Joseph (Emma)",female,36,1,0,345572,17.4,,S +561,0,3,"Morrow, Mr. Thomas Rowan",male,,0,0,372622,7.75,,Q +562,0,3,"Sivic, Mr. Husein",male,40,0,0,349251,7.8958,,S +563,0,2,"Norman, Mr. Robert Douglas",male,28,0,0,218629,13.5,,S +564,0,3,"Simmons, Mr. John",male,,0,0,SOTON/OQ 392082,8.05,,S +565,0,3,"Meanwell, Miss. (Marion Ogden)",female,,0,0,SOTON/O.Q. 392087,8.05,,S +566,0,3,"Davies, Mr. Alfred J",male,24,2,0,A/4 48871,24.15,,S +567,0,3,"Stoytcheff, Mr. Ilia",male,19,0,0,349205,7.8958,,S +568,0,3,"Palsson, Mrs. Nils (Alma Cornelia Berglund)",female,29,0,4,349909,21.075,,S +569,0,3,"Doharr, Mr. Tannous",male,,0,0,2686,7.2292,,C +570,1,3,"Jonsson, Mr. Carl",male,32,0,0,350417,7.8542,,S +571,1,2,"Harris, Mr. George",male,62,0,0,S.W./PP 752,10.5,,S +572,1,1,"Appleton, Mrs. Edward Dale (Charlotte Lamson)",female,53,2,0,11769,51.4792,C101,S +573,1,1,"Flynn, Mr. John Irwin (""Irving"")",male,36,0,0,PC 17474,26.3875,E25,S +574,1,3,"Kelly, Miss. Mary",female,,0,0,14312,7.75,,Q +575,0,3,"Rush, Mr. Alfred George John",male,16,0,0,A/4. 20589,8.05,,S +576,0,3,"Patchett, Mr. George",male,19,0,0,358585,14.5,,S +577,1,2,"Garside, Miss. Ethel",female,34,0,0,243880,13,,S +578,1,1,"Silvey, Mrs. William Baird (Alice Munger)",female,39,1,0,13507,55.9,E44,S +579,0,3,"Caram, Mrs. Joseph (Maria Elias)",female,,1,0,2689,14.4583,,C +580,1,3,"Jussila, Mr. Eiriik",male,32,0,0,STON/O 2. 3101286,7.925,,S +581,1,2,"Christy, Miss. Julie Rachel",female,25,1,1,237789,30,,S +582,1,1,"Thayer, Mrs. John Borland (Marian Longstreth Morris)",female,39,1,1,17421,110.8833,C68,C +583,0,2,"Downton, Mr. William James",male,54,0,0,28403,26,,S +584,0,1,"Ross, Mr. John Hugo",male,36,0,0,13049,40.125,A10,C +585,0,3,"Paulner, Mr. Uscher",male,,0,0,3411,8.7125,,C +586,1,1,"Taussig, Miss. Ruth",female,18,0,2,110413,79.65,E68,S +587,0,2,"Jarvis, Mr. John Denzil",male,47,0,0,237565,15,,S +588,1,1,"Frolicher-Stehli, Mr. Maxmillian",male,60,1,1,13567,79.2,B41,C +589,0,3,"Gilinski, Mr. Eliezer",male,22,0,0,14973,8.05,,S +590,0,3,"Murdlin, Mr. Joseph",male,,0,0,A./5. 3235,8.05,,S +591,0,3,"Rintamaki, Mr. Matti",male,35,0,0,STON/O 2. 3101273,7.125,,S +592,1,1,"Stephenson, Mrs. Walter Bertram (Martha Eustis)",female,52,1,0,36947,78.2667,D20,C +593,0,3,"Elsbury, Mr. William James",male,47,0,0,A/5 3902,7.25,,S +594,0,3,"Bourke, Miss. Mary",female,,0,2,364848,7.75,,Q +595,0,2,"Chapman, Mr. John Henry",male,37,1,0,SC/AH 29037,26,,S +596,0,3,"Van Impe, Mr. Jean Baptiste",male,36,1,1,345773,24.15,,S +597,1,2,"Leitch, Miss. Jessie Wills",female,,0,0,248727,33,,S +598,0,3,"Johnson, Mr. Alfred",male,49,0,0,LINE,0,,S +599,0,3,"Boulos, Mr. Hanna",male,,0,0,2664,7.225,,C +600,1,1,"Duff Gordon, Sir. Cosmo Edmund (""Mr Morgan"")",male,49,1,0,PC 17485,56.9292,A20,C +601,1,2,"Jacobsohn, Mrs. Sidney Samuel (Amy Frances Christy)",female,24,2,1,243847,27,,S +602,0,3,"Slabenoff, Mr. Petco",male,,0,0,349214,7.8958,,S +603,0,1,"Harrington, Mr. Charles H",male,,0,0,113796,42.4,,S +604,0,3,"Torber, Mr. Ernst William",male,44,0,0,364511,8.05,,S +605,1,1,"Homer, Mr. Harry (""Mr E Haven"")",male,35,0,0,111426,26.55,,C +606,0,3,"Lindell, Mr. Edvard Bengtsson",male,36,1,0,349910,15.55,,S +607,0,3,"Karaic, Mr. Milan",male,30,0,0,349246,7.8958,,S +608,1,1,"Daniel, Mr. Robert Williams",male,27,0,0,113804,30.5,,S +609,1,2,"Laroche, Mrs. Joseph (Juliette Marie Louise Lafargue)",female,22,1,2,SC/Paris 2123,41.5792,,C +610,1,1,"Shutes, Miss. Elizabeth W",female,40,0,0,PC 17582,153.4625,C125,S +611,0,3,"Andersson, Mrs. Anders Johan (Alfrida Konstantia Brogren)",female,39,1,5,347082,31.275,,S +612,0,3,"Jardin, Mr. Jose Neto",male,,0,0,SOTON/O.Q. 3101305,7.05,,S +613,1,3,"Murphy, Miss. Margaret Jane",female,,1,0,367230,15.5,,Q +614,0,3,"Horgan, Mr. John",male,,0,0,370377,7.75,,Q +615,0,3,"Brocklebank, Mr. William Alfred",male,35,0,0,364512,8.05,,S +616,1,2,"Herman, Miss. Alice",female,24,1,2,220845,65,,S +617,0,3,"Danbom, Mr. Ernst Gilbert",male,34,1,1,347080,14.4,,S +618,0,3,"Lobb, Mrs. William Arthur (Cordelia K Stanlick)",female,26,1,0,A/5. 3336,16.1,,S +619,1,2,"Becker, Miss. Marion Louise",female,4,2,1,230136,39,F4,S +620,0,2,"Gavey, Mr. Lawrence",male,26,0,0,31028,10.5,,S +621,0,3,"Yasbeck, Mr. Antoni",male,27,1,0,2659,14.4542,,C +622,1,1,"Kimball, Mr. Edwin Nelson Jr",male,42,1,0,11753,52.5542,D19,S +623,1,3,"Nakid, Mr. Sahid",male,20,1,1,2653,15.7417,,C +624,0,3,"Hansen, Mr. Henry Damsgaard",male,21,0,0,350029,7.8542,,S +625,0,3,"Bowen, Mr. David John ""Dai""",male,21,0,0,54636,16.1,,S +626,0,1,"Sutton, Mr. Frederick",male,61,0,0,36963,32.3208,D50,S +627,0,2,"Kirkland, Rev. Charles Leonard",male,57,0,0,219533,12.35,,Q +628,1,1,"Longley, Miss. Gretchen Fiske",female,21,0,0,13502,77.9583,D9,S +629,0,3,"Bostandyeff, Mr. Guentcho",male,26,0,0,349224,7.8958,,S +630,0,3,"O'Connell, Mr. Patrick D",male,,0,0,334912,7.7333,,Q +631,1,1,"Barkworth, Mr. Algernon Henry Wilson",male,80,0,0,27042,30,A23,S +632,0,3,"Lundahl, Mr. Johan Svensson",male,51,0,0,347743,7.0542,,S +633,1,1,"Stahelin-Maeglin, Dr. Max",male,32,0,0,13214,30.5,B50,C +634,0,1,"Parr, Mr. William Henry Marsh",male,,0,0,112052,0,,S +635,0,3,"Skoog, Miss. Mabel",female,9,3,2,347088,27.9,,S +636,1,2,"Davis, Miss. Mary",female,28,0,0,237668,13,,S +637,0,3,"Leinonen, Mr. Antti Gustaf",male,32,0,0,STON/O 2. 3101292,7.925,,S +638,0,2,"Collyer, Mr. Harvey",male,31,1,1,C.A. 31921,26.25,,S +639,0,3,"Panula, Mrs. Juha (Maria Emilia Ojala)",female,41,0,5,3101295,39.6875,,S +640,0,3,"Thorneycroft, Mr. Percival",male,,1,0,376564,16.1,,S +641,0,3,"Jensen, Mr. Hans Peder",male,20,0,0,350050,7.8542,,S +642,1,1,"Sagesser, Mlle. Emma",female,24,0,0,PC 17477,69.3,B35,C +643,0,3,"Skoog, Miss. Margit Elizabeth",female,2,3,2,347088,27.9,,S +644,1,3,"Foo, Mr. Choong",male,,0,0,1601,56.4958,,S +645,1,3,"Baclini, Miss. Eugenie",female,0.75,2,1,2666,19.2583,,C +646,1,1,"Harper, Mr. Henry Sleeper",male,48,1,0,PC 17572,76.7292,D33,C +647,0,3,"Cor, Mr. Liudevit",male,19,0,0,349231,7.8958,,S +648,1,1,"Simonius-Blumer, Col. Oberst Alfons",male,56,0,0,13213,35.5,A26,C +649,0,3,"Willey, Mr. Edward",male,,0,0,S.O./P.P. 751,7.55,,S +650,1,3,"Stanley, Miss. Amy Zillah Elsie",female,23,0,0,CA. 2314,7.55,,S +651,0,3,"Mitkoff, Mr. Mito",male,,0,0,349221,7.8958,,S +652,1,2,"Doling, Miss. Elsie",female,18,0,1,231919,23,,S +653,0,3,"Kalvik, Mr. Johannes Halvorsen",male,21,0,0,8475,8.4333,,S +654,1,3,"O'Leary, Miss. Hanora ""Norah""",female,,0,0,330919,7.8292,,Q +655,0,3,"Hegarty, Miss. Hanora ""Nora""",female,18,0,0,365226,6.75,,Q +656,0,2,"Hickman, Mr. Leonard Mark",male,24,2,0,S.O.C. 14879,73.5,,S +657,0,3,"Radeff, Mr. Alexander",male,,0,0,349223,7.8958,,S +658,0,3,"Bourke, Mrs. John (Catherine)",female,32,1,1,364849,15.5,,Q +659,0,2,"Eitemiller, Mr. George Floyd",male,23,0,0,29751,13,,S +660,0,1,"Newell, Mr. Arthur Webster",male,58,0,2,35273,113.275,D48,C +661,1,1,"Frauenthal, Dr. Henry William",male,50,2,0,PC 17611,133.65,,S +662,0,3,"Badt, Mr. Mohamed",male,40,0,0,2623,7.225,,C +663,0,1,"Colley, Mr. Edward Pomeroy",male,47,0,0,5727,25.5875,E58,S +664,0,3,"Coleff, Mr. Peju",male,36,0,0,349210,7.4958,,S +665,1,3,"Lindqvist, Mr. Eino William",male,20,1,0,STON/O 2. 3101285,7.925,,S +666,0,2,"Hickman, Mr. Lewis",male,32,2,0,S.O.C. 14879,73.5,,S +667,0,2,"Butler, Mr. Reginald Fenton",male,25,0,0,234686,13,,S +668,0,3,"Rommetvedt, Mr. Knud Paust",male,,0,0,312993,7.775,,S +669,0,3,"Cook, Mr. Jacob",male,43,0,0,A/5 3536,8.05,,S +670,1,1,"Taylor, Mrs. Elmer Zebley (Juliet Cummins Wright)",female,,1,0,19996,52,C126,S +671,1,2,"Brown, Mrs. Thomas William Solomon (Elizabeth Catherine Ford)",female,40,1,1,29750,39,,S +672,0,1,"Davidson, Mr. Thornton",male,31,1,0,F.C. 12750,52,B71,S +673,0,2,"Mitchell, Mr. Henry Michael",male,70,0,0,C.A. 24580,10.5,,S +674,1,2,"Wilhelms, Mr. Charles",male,31,0,0,244270,13,,S +675,0,2,"Watson, Mr. Ennis Hastings",male,,0,0,239856,0,,S +676,0,3,"Edvardsson, Mr. Gustaf Hjalmar",male,18,0,0,349912,7.775,,S +677,0,3,"Sawyer, Mr. Frederick Charles",male,24.5,0,0,342826,8.05,,S +678,1,3,"Turja, Miss. Anna Sofia",female,18,0,0,4138,9.8417,,S +679,0,3,"Goodwin, Mrs. Frederick (Augusta Tyler)",female,43,1,6,CA 2144,46.9,,S +680,1,1,"Cardeza, Mr. Thomas Drake Martinez",male,36,0,1,PC 17755,512.3292,B51 B53 B55,C +681,0,3,"Peters, Miss. Katie",female,,0,0,330935,8.1375,,Q +682,1,1,"Hassab, Mr. Hammad",male,27,0,0,PC 17572,76.7292,D49,C +683,0,3,"Olsvigen, Mr. Thor Anderson",male,20,0,0,6563,9.225,,S +684,0,3,"Goodwin, Mr. Charles Edward",male,14,5,2,CA 2144,46.9,,S +685,0,2,"Brown, Mr. Thomas William Solomon",male,60,1,1,29750,39,,S +686,0,2,"Laroche, Mr. Joseph Philippe Lemercier",male,25,1,2,SC/Paris 2123,41.5792,,C +687,0,3,"Panula, Mr. Jaako Arnold",male,14,4,1,3101295,39.6875,,S +688,0,3,"Dakic, Mr. Branko",male,19,0,0,349228,10.1708,,S +689,0,3,"Fischer, Mr. Eberhard Thelander",male,18,0,0,350036,7.7958,,S +690,1,1,"Madill, Miss. Georgette Alexandra",female,15,0,1,24160,211.3375,B5,S +691,1,1,"Dick, Mr. Albert Adrian",male,31,1,0,17474,57,B20,S +692,1,3,"Karun, Miss. Manca",female,4,0,1,349256,13.4167,,C +693,1,3,"Lam, Mr. Ali",male,,0,0,1601,56.4958,,S +694,0,3,"Saad, Mr. Khalil",male,25,0,0,2672,7.225,,C +695,0,1,"Weir, Col. John",male,60,0,0,113800,26.55,,S +696,0,2,"Chapman, Mr. Charles Henry",male,52,0,0,248731,13.5,,S +697,0,3,"Kelly, Mr. James",male,44,0,0,363592,8.05,,S +698,1,3,"Mullens, Miss. Katherine ""Katie""",female,,0,0,35852,7.7333,,Q +699,0,1,"Thayer, Mr. John Borland",male,49,1,1,17421,110.8833,C68,C +700,0,3,"Humblen, Mr. Adolf Mathias Nicolai Olsen",male,42,0,0,348121,7.65,F G63,S +701,1,1,"Astor, Mrs. John Jacob (Madeleine Talmadge Force)",female,18,1,0,PC 17757,227.525,C62 C64,C +702,1,1,"Silverthorne, Mr. Spencer Victor",male,35,0,0,PC 17475,26.2875,E24,S +703,0,3,"Barbara, Miss. Saiide",female,18,0,1,2691,14.4542,,C +704,0,3,"Gallagher, Mr. Martin",male,25,0,0,36864,7.7417,,Q +705,0,3,"Hansen, Mr. Henrik Juul",male,26,1,0,350025,7.8542,,S +706,0,2,"Morley, Mr. Henry Samuel (""Mr Henry Marshall"")",male,39,0,0,250655,26,,S +707,1,2,"Kelly, Mrs. Florence ""Fannie""",female,45,0,0,223596,13.5,,S +708,1,1,"Calderhead, Mr. Edward Pennington",male,42,0,0,PC 17476,26.2875,E24,S +709,1,1,"Cleaver, Miss. Alice",female,22,0,0,113781,151.55,,S +710,1,3,"Moubarek, Master. Halim Gonios (""William George"")",male,,1,1,2661,15.2458,,C +711,1,1,"Mayne, Mlle. Berthe Antonine (""Mrs de Villiers"")",female,24,0,0,PC 17482,49.5042,C90,C +712,0,1,"Klaber, Mr. Herman",male,,0,0,113028,26.55,C124,S +713,1,1,"Taylor, Mr. Elmer Zebley",male,48,1,0,19996,52,C126,S +714,0,3,"Larsson, Mr. August Viktor",male,29,0,0,7545,9.4833,,S +715,0,2,"Greenberg, Mr. Samuel",male,52,0,0,250647,13,,S +716,0,3,"Soholt, Mr. Peter Andreas Lauritz Andersen",male,19,0,0,348124,7.65,F G73,S +717,1,1,"Endres, Miss. Caroline Louise",female,38,0,0,PC 17757,227.525,C45,C +718,1,2,"Troutt, Miss. Edwina Celia ""Winnie""",female,27,0,0,34218,10.5,E101,S +719,0,3,"McEvoy, Mr. Michael",male,,0,0,36568,15.5,,Q +720,0,3,"Johnson, Mr. Malkolm Joackim",male,33,0,0,347062,7.775,,S +721,1,2,"Harper, Miss. Annie Jessie ""Nina""",female,6,0,1,248727,33,,S +722,0,3,"Jensen, Mr. Svend Lauritz",male,17,1,0,350048,7.0542,,S +723,0,2,"Gillespie, Mr. William Henry",male,34,0,0,12233,13,,S +724,0,2,"Hodges, Mr. Henry Price",male,50,0,0,250643,13,,S +725,1,1,"Chambers, Mr. Norman Campbell",male,27,1,0,113806,53.1,E8,S +726,0,3,"Oreskovic, Mr. Luka",male,20,0,0,315094,8.6625,,S +727,1,2,"Renouf, Mrs. Peter Henry (Lillian Jefferys)",female,30,3,0,31027,21,,S +728,1,3,"Mannion, Miss. Margareth",female,,0,0,36866,7.7375,,Q +729,0,2,"Bryhl, Mr. Kurt Arnold Gottfrid",male,25,1,0,236853,26,,S +730,0,3,"Ilmakangas, Miss. Pieta Sofia",female,25,1,0,STON/O2. 3101271,7.925,,S +731,1,1,"Allen, Miss. Elisabeth Walton",female,29,0,0,24160,211.3375,B5,S +732,0,3,"Hassan, Mr. Houssein G N",male,11,0,0,2699,18.7875,,C +733,0,2,"Knight, Mr. Robert J",male,,0,0,239855,0,,S +734,0,2,"Berriman, Mr. William John",male,23,0,0,28425,13,,S +735,0,2,"Troupiansky, Mr. Moses Aaron",male,23,0,0,233639,13,,S +736,0,3,"Williams, Mr. Leslie",male,28.5,0,0,54636,16.1,,S +737,0,3,"Ford, Mrs. Edward (Margaret Ann Watson)",female,48,1,3,W./C. 6608,34.375,,S +738,1,1,"Lesurer, Mr. Gustave J",male,35,0,0,PC 17755,512.3292,B101,C +739,0,3,"Ivanoff, Mr. Kanio",male,,0,0,349201,7.8958,,S +740,0,3,"Nankoff, Mr. Minko",male,,0,0,349218,7.8958,,S +741,1,1,"Hawksford, Mr. Walter James",male,,0,0,16988,30,D45,S +742,0,1,"Cavendish, Mr. Tyrell William",male,36,1,0,19877,78.85,C46,S +743,1,1,"Ryerson, Miss. Susan Parker ""Suzette""",female,21,2,2,PC 17608,262.375,B57 B59 B63 B66,C +744,0,3,"McNamee, Mr. Neal",male,24,1,0,376566,16.1,,S +745,1,3,"Stranden, Mr. Juho",male,31,0,0,STON/O 2. 3101288,7.925,,S +746,0,1,"Crosby, Capt. Edward Gifford",male,70,1,1,WE/P 5735,71,B22,S +747,0,3,"Abbott, Mr. Rossmore Edward",male,16,1,1,C.A. 2673,20.25,,S +748,1,2,"Sinkkonen, Miss. Anna",female,30,0,0,250648,13,,S +749,0,1,"Marvin, Mr. Daniel Warner",male,19,1,0,113773,53.1,D30,S +750,0,3,"Connaghton, Mr. Michael",male,31,0,0,335097,7.75,,Q +751,1,2,"Wells, Miss. Joan",female,4,1,1,29103,23,,S +752,1,3,"Moor, Master. Meier",male,6,0,1,392096,12.475,E121,S +753,0,3,"Vande Velde, Mr. Johannes Joseph",male,33,0,0,345780,9.5,,S +754,0,3,"Jonkoff, Mr. Lalio",male,23,0,0,349204,7.8958,,S +755,1,2,"Herman, Mrs. Samuel (Jane Laver)",female,48,1,2,220845,65,,S +756,1,2,"Hamalainen, Master. Viljo",male,0.67,1,1,250649,14.5,,S +757,0,3,"Carlsson, Mr. August Sigfrid",male,28,0,0,350042,7.7958,,S +758,0,2,"Bailey, Mr. Percy Andrew",male,18,0,0,29108,11.5,,S +759,0,3,"Theobald, Mr. Thomas Leonard",male,34,0,0,363294,8.05,,S +760,1,1,"Rothes, the Countess. of (Lucy Noel Martha Dyer-Edwards)",female,33,0,0,110152,86.5,B77,S +761,0,3,"Garfirth, Mr. John",male,,0,0,358585,14.5,,S +762,0,3,"Nirva, Mr. Iisakki Antino Aijo",male,41,0,0,SOTON/O2 3101272,7.125,,S +763,1,3,"Barah, Mr. Hanna Assi",male,20,0,0,2663,7.2292,,C +764,1,1,"Carter, Mrs. William Ernest (Lucile Polk)",female,36,1,2,113760,120,B96 B98,S +765,0,3,"Eklund, Mr. Hans Linus",male,16,0,0,347074,7.775,,S +766,1,1,"Hogeboom, Mrs. John C (Anna Andrews)",female,51,1,0,13502,77.9583,D11,S +767,0,1,"Brewe, Dr. Arthur Jackson",male,,0,0,112379,39.6,,C +768,0,3,"Mangan, Miss. Mary",female,30.5,0,0,364850,7.75,,Q +769,0,3,"Moran, Mr. Daniel J",male,,1,0,371110,24.15,,Q +770,0,3,"Gronnestad, Mr. Daniel Danielsen",male,32,0,0,8471,8.3625,,S +771,0,3,"Lievens, Mr. Rene Aime",male,24,0,0,345781,9.5,,S +772,0,3,"Jensen, Mr. Niels Peder",male,48,0,0,350047,7.8542,,S +773,0,2,"Mack, Mrs. (Mary)",female,57,0,0,S.O./P.P. 3,10.5,E77,S +774,0,3,"Elias, Mr. Dibo",male,,0,0,2674,7.225,,C +775,1,2,"Hocking, Mrs. Elizabeth (Eliza Needs)",female,54,1,3,29105,23,,S +776,0,3,"Myhrman, Mr. Pehr Fabian Oliver Malkolm",male,18,0,0,347078,7.75,,S +777,0,3,"Tobin, Mr. Roger",male,,0,0,383121,7.75,F38,Q +778,1,3,"Emanuel, Miss. Virginia Ethel",female,5,0,0,364516,12.475,,S +779,0,3,"Kilgannon, Mr. Thomas J",male,,0,0,36865,7.7375,,Q +780,1,1,"Robert, Mrs. Edward Scott (Elisabeth Walton McMillan)",female,43,0,1,24160,211.3375,B3,S +781,1,3,"Ayoub, Miss. Banoura",female,13,0,0,2687,7.2292,,C +782,1,1,"Dick, Mrs. Albert Adrian (Vera Gillespie)",female,17,1,0,17474,57,B20,S +783,0,1,"Long, Mr. Milton Clyde",male,29,0,0,113501,30,D6,S +784,0,3,"Johnston, Mr. Andrew G",male,,1,2,W./C. 6607,23.45,,S +785,0,3,"Ali, Mr. William",male,25,0,0,SOTON/O.Q. 3101312,7.05,,S +786,0,3,"Harmer, Mr. Abraham (David Lishin)",male,25,0,0,374887,7.25,,S +787,1,3,"Sjoblom, Miss. Anna Sofia",female,18,0,0,3101265,7.4958,,S +788,0,3,"Rice, Master. George Hugh",male,8,4,1,382652,29.125,,Q +789,1,3,"Dean, Master. Bertram Vere",male,1,1,2,C.A. 2315,20.575,,S +790,0,1,"Guggenheim, Mr. Benjamin",male,46,0,0,PC 17593,79.2,B82 B84,C +791,0,3,"Keane, Mr. Andrew ""Andy""",male,,0,0,12460,7.75,,Q +792,0,2,"Gaskell, Mr. Alfred",male,16,0,0,239865,26,,S +793,0,3,"Sage, Miss. Stella Anna",female,,8,2,CA. 2343,69.55,,S +794,0,1,"Hoyt, Mr. William Fisher",male,,0,0,PC 17600,30.6958,,C +795,0,3,"Dantcheff, Mr. Ristiu",male,25,0,0,349203,7.8958,,S +796,0,2,"Otter, Mr. Richard",male,39,0,0,28213,13,,S +797,1,1,"Leader, Dr. Alice (Farnham)",female,49,0,0,17465,25.9292,D17,S +798,1,3,"Osman, Mrs. Mara",female,31,0,0,349244,8.6833,,S +799,0,3,"Ibrahim Shawah, Mr. Yousseff",male,30,0,0,2685,7.2292,,C +800,0,3,"Van Impe, Mrs. Jean Baptiste (Rosalie Paula Govaert)",female,30,1,1,345773,24.15,,S +801,0,2,"Ponesell, Mr. Martin",male,34,0,0,250647,13,,S +802,1,2,"Collyer, Mrs. Harvey (Charlotte Annie Tate)",female,31,1,1,C.A. 31921,26.25,,S +803,1,1,"Carter, Master. William Thornton II",male,11,1,2,113760,120,B96 B98,S +804,1,3,"Thomas, Master. Assad Alexander",male,0.42,0,1,2625,8.5167,,C +805,1,3,"Hedman, Mr. Oskar Arvid",male,27,0,0,347089,6.975,,S +806,0,3,"Johansson, Mr. Karl Johan",male,31,0,0,347063,7.775,,S +807,0,1,"Andrews, Mr. Thomas Jr",male,39,0,0,112050,0,A36,S +808,0,3,"Pettersson, Miss. Ellen Natalia",female,18,0,0,347087,7.775,,S +809,0,2,"Meyer, Mr. August",male,39,0,0,248723,13,,S +810,1,1,"Chambers, Mrs. Norman Campbell (Bertha Griggs)",female,33,1,0,113806,53.1,E8,S +811,0,3,"Alexander, Mr. William",male,26,0,0,3474,7.8875,,S +812,0,3,"Lester, Mr. James",male,39,0,0,A/4 48871,24.15,,S +813,0,2,"Slemen, Mr. Richard James",male,35,0,0,28206,10.5,,S +814,0,3,"Andersson, Miss. Ebba Iris Alfrida",female,6,4,2,347082,31.275,,S +815,0,3,"Tomlin, Mr. Ernest Portage",male,30.5,0,0,364499,8.05,,S +816,0,1,"Fry, Mr. Richard",male,,0,0,112058,0,B102,S +817,0,3,"Heininen, Miss. Wendla Maria",female,23,0,0,STON/O2. 3101290,7.925,,S +818,0,2,"Mallet, Mr. Albert",male,31,1,1,S.C./PARIS 2079,37.0042,,C +819,0,3,"Holm, Mr. John Fredrik Alexander",male,43,0,0,C 7075,6.45,,S +820,0,3,"Skoog, Master. Karl Thorsten",male,10,3,2,347088,27.9,,S +821,1,1,"Hays, Mrs. Charles Melville (Clara Jennings Gregg)",female,52,1,1,12749,93.5,B69,S +822,1,3,"Lulic, Mr. Nikola",male,27,0,0,315098,8.6625,,S +823,0,1,"Reuchlin, Jonkheer. John George",male,38,0,0,19972,0,,S +824,1,3,"Moor, Mrs. (Beila)",female,27,0,1,392096,12.475,E121,S +825,0,3,"Panula, Master. Urho Abraham",male,2,4,1,3101295,39.6875,,S +826,0,3,"Flynn, Mr. John",male,,0,0,368323,6.95,,Q +827,0,3,"Lam, Mr. Len",male,,0,0,1601,56.4958,,S +828,1,2,"Mallet, Master. Andre",male,1,0,2,S.C./PARIS 2079,37.0042,,C +829,1,3,"McCormack, Mr. Thomas Joseph",male,,0,0,367228,7.75,,Q +830,1,1,"Stone, Mrs. George Nelson (Martha Evelyn)",female,62,0,0,113572,80,B28, +831,1,3,"Yasbeck, Mrs. Antoni (Selini Alexander)",female,15,1,0,2659,14.4542,,C +832,1,2,"Richards, Master. George Sibley",male,0.83,1,1,29106,18.75,,S +833,0,3,"Saad, Mr. Amin",male,,0,0,2671,7.2292,,C +834,0,3,"Augustsson, Mr. Albert",male,23,0,0,347468,7.8542,,S +835,0,3,"Allum, Mr. Owen George",male,18,0,0,2223,8.3,,S +836,1,1,"Compton, Miss. Sara Rebecca",female,39,1,1,PC 17756,83.1583,E49,C +837,0,3,"Pasic, Mr. Jakob",male,21,0,0,315097,8.6625,,S +838,0,3,"Sirota, Mr. Maurice",male,,0,0,392092,8.05,,S +839,1,3,"Chip, Mr. Chang",male,32,0,0,1601,56.4958,,S +840,1,1,"Marechal, Mr. Pierre",male,,0,0,11774,29.7,C47,C +841,0,3,"Alhomaki, Mr. Ilmari Rudolf",male,20,0,0,SOTON/O2 3101287,7.925,,S +842,0,2,"Mudd, Mr. Thomas Charles",male,16,0,0,S.O./P.P. 3,10.5,,S +843,1,1,"Serepeca, Miss. Augusta",female,30,0,0,113798,31,,C +844,0,3,"Lemberopolous, Mr. Peter L",male,34.5,0,0,2683,6.4375,,C +845,0,3,"Culumovic, Mr. Jeso",male,17,0,0,315090,8.6625,,S +846,0,3,"Abbing, Mr. Anthony",male,42,0,0,C.A. 5547,7.55,,S +847,0,3,"Sage, Mr. Douglas Bullen",male,,8,2,CA. 2343,69.55,,S +848,0,3,"Markoff, Mr. Marin",male,35,0,0,349213,7.8958,,C +849,0,2,"Harper, Rev. John",male,28,0,1,248727,33,,S +850,1,1,"Goldenberg, Mrs. Samuel L (Edwiga Grabowska)",female,,1,0,17453,89.1042,C92,C +851,0,3,"Andersson, Master. Sigvard Harald Elias",male,4,4,2,347082,31.275,,S +852,0,3,"Svensson, Mr. Johan",male,74,0,0,347060,7.775,,S +853,0,3,"Boulos, Miss. Nourelain",female,9,1,1,2678,15.2458,,C +854,1,1,"Lines, Miss. Mary Conover",female,16,0,1,PC 17592,39.4,D28,S +855,0,2,"Carter, Mrs. Ernest Courtenay (Lilian Hughes)",female,44,1,0,244252,26,,S +856,1,3,"Aks, Mrs. Sam (Leah Rosen)",female,18,0,1,392091,9.35,,S +857,1,1,"Wick, Mrs. George Dennick (Mary Hitchcock)",female,45,1,1,36928,164.8667,,S +858,1,1,"Daly, Mr. Peter Denis ",male,51,0,0,113055,26.55,E17,S +859,1,3,"Baclini, Mrs. Solomon (Latifa Qurban)",female,24,0,3,2666,19.2583,,C +860,0,3,"Razi, Mr. Raihed",male,,0,0,2629,7.2292,,C +861,0,3,"Hansen, Mr. Claus Peter",male,41,2,0,350026,14.1083,,S +862,0,2,"Giles, Mr. Frederick Edward",male,21,1,0,28134,11.5,,S +863,1,1,"Swift, Mrs. Frederick Joel (Margaret Welles Barron)",female,48,0,0,17466,25.9292,D17,S +864,0,3,"Sage, Miss. Dorothy Edith ""Dolly""",female,,8,2,CA. 2343,69.55,,S +865,0,2,"Gill, Mr. John William",male,24,0,0,233866,13,,S +866,1,2,"Bystrom, Mrs. (Karolina)",female,42,0,0,236852,13,,S +867,1,2,"Duran y More, Miss. Asuncion",female,27,1,0,SC/PARIS 2149,13.8583,,C +868,0,1,"Roebling, Mr. Washington Augustus II",male,31,0,0,PC 17590,50.4958,A24,S +869,0,3,"van Melkebeke, Mr. Philemon",male,,0,0,345777,9.5,,S +870,1,3,"Johnson, Master. Harold Theodor",male,4,1,1,347742,11.1333,,S +871,0,3,"Balkic, Mr. Cerin",male,26,0,0,349248,7.8958,,S +872,1,1,"Beckwith, Mrs. Richard Leonard (Sallie Monypeny)",female,47,1,1,11751,52.5542,D35,S +873,0,1,"Carlsson, Mr. Frans Olof",male,33,0,0,695,5,B51 B53 B55,S +874,0,3,"Vander Cruyssen, Mr. Victor",male,47,0,0,345765,9,,S +875,1,2,"Abelson, Mrs. Samuel (Hannah Wizosky)",female,28,1,0,P/PP 3381,24,,C +876,1,3,"Najib, Miss. Adele Kiamie ""Jane""",female,15,0,0,2667,7.225,,C +877,0,3,"Gustafsson, Mr. Alfred Ossian",male,20,0,0,7534,9.8458,,S +878,0,3,"Petroff, Mr. Nedelio",male,19,0,0,349212,7.8958,,S +879,0,3,"Laleff, Mr. Kristo",male,,0,0,349217,7.8958,,S +880,1,1,"Potter, Mrs. Thomas Jr (Lily Alexenia Wilson)",female,56,0,1,11767,83.1583,C50,C +881,1,2,"Shelley, Mrs. William (Imanita Parrish Hall)",female,25,0,1,230433,26,,S +882,0,3,"Markun, Mr. Johann",male,33,0,0,349257,7.8958,,S +883,0,3,"Dahlberg, Miss. Gerda Ulrika",female,22,0,0,7552,10.5167,,S +884,0,2,"Banfield, Mr. Frederick James",male,28,0,0,C.A./SOTON 34068,10.5,,S +885,0,3,"Sutehall, Mr. Henry Jr",male,25,0,0,SOTON/OQ 392076,7.05,,S +886,0,3,"Rice, Mrs. William (Margaret Norton)",female,39,0,5,382652,29.125,,Q +887,0,2,"Montvila, Rev. Juozas",male,27,0,0,211536,13,,S +888,1,1,"Graham, Miss. Margaret Edith",female,19,0,0,112053,30,B42,S +889,0,3,"Johnston, Miss. Catherine Helen ""Carrie""",female,,1,2,W./C. 6607,23.45,,S +890,1,1,"Behr, Mr. Karl Howell",male,26,0,0,111369,30,C148,C +891,0,3,"Dooley, Mr. Patrick",male,32,0,0,370376,7.75,,Q diff --git a/gradio/media_assets/images/avatar.png b/gradio/media_assets/images/avatar.png new file mode 100644 index 0000000..8f1df71 Binary files /dev/null and b/gradio/media_assets/images/avatar.png differ diff --git a/gradio/media_assets/images/bus.png b/gradio/media_assets/images/bus.png new file mode 100644 index 0000000..855b404 Binary files /dev/null and b/gradio/media_assets/images/bus.png differ diff --git a/gradio/media_assets/images/cheetah.jpg b/gradio/media_assets/images/cheetah.jpg new file mode 100644 index 0000000..d1fde62 Binary files /dev/null and b/gradio/media_assets/images/cheetah.jpg differ diff --git a/gradio/media_assets/images/cheetah1.jpg b/gradio/media_assets/images/cheetah1.jpg new file mode 100644 index 0000000..c510ff3 Binary files /dev/null and b/gradio/media_assets/images/cheetah1.jpg differ diff --git a/gradio/media_assets/images/groot.jpeg b/gradio/media_assets/images/groot.jpeg new file mode 100644 index 0000000..06b192f Binary files /dev/null and b/gradio/media_assets/images/groot.jpeg differ diff --git a/gradio/media_assets/images/hf-logo_transpng.png b/gradio/media_assets/images/hf-logo_transpng.png new file mode 100644 index 0000000..ccc7d9c Binary files /dev/null and b/gradio/media_assets/images/hf-logo_transpng.png differ diff --git a/gradio/media_assets/images/lion.jpg b/gradio/media_assets/images/lion.jpg new file mode 100644 index 0000000..e9bf9f5 Binary files /dev/null and b/gradio/media_assets/images/lion.jpg differ diff --git a/gradio/media_assets/images/logo.png b/gradio/media_assets/images/logo.png new file mode 100644 index 0000000..8f1df71 Binary files /dev/null and b/gradio/media_assets/images/logo.png differ diff --git a/gradio/media_assets/images/screenshot.gif b/gradio/media_assets/images/screenshot.gif new file mode 100644 index 0000000..620ec8f Binary files /dev/null and b/gradio/media_assets/images/screenshot.gif differ diff --git a/gradio/media_assets/images/tower.jpg b/gradio/media_assets/images/tower.jpg new file mode 100644 index 0000000..aec3fa9 Binary files /dev/null and b/gradio/media_assets/images/tower.jpg differ diff --git a/gradio/media_assets/models3d/Bunny.obj b/gradio/media_assets/models3d/Bunny.obj new file mode 100644 index 0000000..9baeb36 --- /dev/null +++ b/gradio/media_assets/models3d/Bunny.obj @@ -0,0 +1,7474 @@ +# OBJ file format with ext .obj +# vertex count = 2503 +# face count = 4968 +v -3.4101800e-003 1.3031957e-001 2.1754370e-002 +v -8.1719160e-002 1.5250145e-001 2.9656090e-002 +v -3.0543480e-002 1.2477885e-001 1.0983400e-003 +v -2.4901590e-002 1.1211138e-001 3.7560240e-002 +v -1.8405680e-002 1.7843055e-001 -2.4219580e-002 +v 1.9067940e-002 1.2144925e-001 3.1968440e-002 +v 6.0412000e-003 1.2494359e-001 3.2652890e-002 +v -1.3469030e-002 1.6299355e-001 -1.2000020e-002 +v -3.4393240e-002 1.7236688e-001 -9.8213000e-004 +v -8.4314160e-002 1.0957263e-001 3.7097300e-003 +v -4.2233540e-002 1.7211574e-001 -4.1799800e-003 +v -6.3308390e-002 1.5660615e-001 -1.3838790e-002 +v -7.6903950e-002 1.6708033e-001 -2.6931360e-002 +v -7.2253920e-002 1.1539550e-001 5.1670300e-002 +v 1.2981330e-002 1.1366375e-001 3.8302950e-002 +v -3.7857280e-002 1.7010102e-001 1.4236000e-003 +v 4.8689400e-003 3.7962370e-002 4.5867630e-002 +v -5.7180550e-002 4.0918830e-002 4.6301340e-002 +v -4.5209070e-002 3.8839100e-002 4.4503770e-002 +v -3.3761490e-002 1.2617876e-001 1.7132300e-003 +v -5.0242270e-002 1.5773747e-001 9.3944500e-003 +v -2.1216950e-002 1.5887938e-001 -4.6923700e-003 +v -5.6472950e-002 1.5778406e-001 8.1786500e-003 +v -5.2802060e-002 4.1319860e-002 4.6169800e-002 +v -4.9960340e-002 4.3101950e-002 4.4462650e-002 +v -2.9748750e-002 3.6539860e-002 5.2493310e-002 +v -3.5438900e-003 4.2659770e-002 4.7541530e-002 +v 4.9304900e-003 4.1982660e-002 4.5723390e-002 +v -3.9088180e-002 1.6872020e-001 -1.1924680e-002 +v -5.6901000e-002 4.5437000e-002 4.3236960e-002 +v -4.1244880e-002 4.3098890e-002 4.2129560e-002 +v -2.6471980e-002 4.5034530e-002 5.1219460e-002 +v -2.1866970e-002 4.4022930e-002 5.3243800e-002 +v -3.6996250e-002 1.6899301e-001 1.3256300e-003 +v -6.7216590e-002 1.6171340e-001 -1.3733710e-002 +v 4.9760060e-002 7.0235220e-002 2.3732020e-002 +v -4.9186640e-002 4.6411230e-002 4.1170040e-002 +v -4.4590380e-002 4.3797990e-002 4.2685460e-002 +v -4.3686470e-002 4.7154500e-002 4.0286310e-002 +v -2.2491950e-002 4.6513620e-002 5.1885310e-002 +v -6.5174200e-003 4.5036200e-002 4.7502780e-002 +v 3.7699000e-004 4.4935790e-002 4.6519930e-002 +v 3.4023920e-002 1.1353879e-001 2.4595280e-002 +v -2.6467900e-002 1.8104250e-001 -8.0811700e-003 +v -1.7533470e-002 4.7964250e-002 4.8829630e-002 +v -7.0012600e-003 4.6416520e-002 4.7485540e-002 +v 5.9862300e-003 4.6689140e-002 4.9073620e-002 +v 9.1007200e-003 4.8474490e-002 4.9353190e-002 +v -3.5453700e-002 1.1244769e-001 3.5055410e-002 +v -7.5983200e-002 1.3820800e-001 4.9216580e-002 +v 3.4838440e-002 4.3153410e-002 2.8954310e-002 +v -5.2655550e-002 4.8494220e-002 3.8731190e-002 +v -4.7378940e-002 4.8456670e-002 3.9126790e-002 +v -3.8933750e-002 4.6364270e-002 4.0364780e-002 +v -2.6468940e-002 4.7816430e-002 4.9322590e-002 +v -2.2365790e-002 4.8073650e-002 5.0126500e-002 +v -1.3373430e-002 4.7892410e-002 4.7883850e-002 +v -1.2193490e-002 4.9470300e-002 4.9484490e-002 +v -6.3364000e-004 4.7193060e-002 4.9136900e-002 +v 2.0656800e-003 5.0104680e-002 5.2290220e-002 +v -2.2749270e-002 4.9883880e-002 4.6605520e-002 +v -1.8002080e-002 4.9917850e-002 4.6947970e-002 +v -7.8036800e-003 5.0169310e-002 5.0988650e-002 +v -2.6843800e-003 5.1247420e-002 5.3186790e-002 +v -6.3875650e-002 1.6140094e-001 -2.0064210e-002 +v 3.2434000e-002 4.5333970e-002 3.0316760e-002 +v -8.8064570e-002 1.2496764e-001 5.7412000e-004 +v -4.1503710e-002 1.6748512e-001 3.2765900e-003 +v -6.4457010e-002 1.5342891e-001 -5.1180400e-003 +v -3.4303190e-002 5.0520150e-002 3.8286020e-002 +v -2.2949400e-002 5.1020650e-002 4.3926450e-002 +v -1.4354710e-002 5.4428200e-002 5.0710310e-002 +v 1.3773100e-003 5.2302710e-002 5.3149010e-002 +v 3.6285000e-003 5.3198640e-002 5.3422710e-002 +v 8.0723800e-003 5.1574140e-002 5.1773560e-002 +v -7.2665890e-002 1.3005582e-001 5.1668200e-002 +v 3.7992780e-002 4.9793200e-002 3.1902020e-002 +v 3.8497260e-002 4.8062400e-002 3.1737450e-002 +v 2.1503510e-002 1.2563988e-001 2.1252620e-002 +v -7.6481330e-002 1.4827412e-001 -8.9376200e-003 +v -8.7240410e-002 1.1967213e-001 -1.7813000e-004 +v -4.3719960e-002 1.6822738e-001 2.3425000e-003 +v -4.0652200e-002 1.2266506e-001 2.6290300e-002 +v -4.6686180e-002 5.4570720e-002 3.7587370e-002 +v -4.4071750e-002 5.1058250e-002 3.8977810e-002 +v -3.8144110e-002 5.0599600e-002 3.9302160e-002 +v -1.9875770e-002 5.1607710e-002 4.6142000e-002 +v -1.6911250e-002 5.1843550e-002 4.8459320e-002 +v -1.6249190e-002 5.4292110e-002 5.0306940e-002 +v -1.0446540e-002 5.3685970e-002 5.1958610e-002 +v -4.3090900e-003 5.4467500e-002 5.3908250e-002 +v 7.8152700e-003 5.5050680e-002 5.2750250e-002 +v 3.7955090e-002 1.0488710e-001 -3.2031800e-003 +v -7.9003790e-002 1.2850550e-001 5.3149340e-002 +v -7.9778990e-002 1.3448894e-001 5.0990290e-002 +v -5.9129700e-002 1.5039712e-001 3.4489540e-002 +v -6.5691790e-002 1.4961818e-001 3.8160980e-002 +v -3.1951660e-002 1.2518394e-001 1.9400580e-002 +v -6.9372590e-002 1.6061775e-001 -9.1905000e-003 +v -4.5225500e-002 1.2935459e-001 2.0377520e-002 +v -4.1879110e-002 5.6164390e-002 3.9796700e-002 +v -3.0614840e-002 5.4412650e-002 3.6694290e-002 +v -2.4787600e-002 5.2606220e-002 4.0839760e-002 +v -2.1588860e-002 5.6836920e-002 4.5467040e-002 +v -2.4264000e-004 5.4536020e-002 5.4641200e-002 +v -8.0900510e-002 1.2558713e-001 5.2155370e-002 +v -2.9996210e-002 1.7811137e-001 -5.2358200e-003 +v 3.5515390e-002 5.0449570e-002 3.1439830e-002 +v 4.3315550e-002 5.2145550e-002 3.2492110e-002 +v -6.3938540e-002 1.5262699e-001 3.4481070e-002 +v -4.4489440e-002 6.1077710e-002 3.9545320e-002 +v -3.8979900e-002 5.7996270e-002 4.0151390e-002 +v -7.9087730e-002 1.7044488e-001 -4.1373170e-002 +v -4.6247300e-003 5.7759650e-002 5.3990710e-002 +v -1.4985500e-003 5.5925480e-002 5.4630800e-002 +v 5.1981700e-003 5.7017990e-002 5.3423530e-002 +v 3.0920000e-005 1.2315746e-001 3.4749660e-002 +v 3.3568300e-002 1.1523716e-001 2.1798410e-002 +v 3.8686300e-002 5.6450590e-002 3.1188930e-002 +v -3.4385780e-002 5.4096000e-002 3.8060290e-002 +v -8.5308300e-003 6.0159420e-002 5.5308950e-002 +v -4.4024000e-004 5.8343410e-002 5.4483410e-002 +v -9.1078730e-002 1.1506037e-001 4.0141810e-002 +v 4.0775480e-002 5.4557490e-002 3.2014740e-002 +v 4.5636880e-002 5.7402620e-002 3.1992220e-002 +v 2.0358850e-002 1.2448747e-001 2.5906340e-002 +v -1.4169700e-002 1.2767892e-001 1.3080500e-003 +v -1.1987590e-002 5.7493210e-002 5.2752420e-002 +v 3.2514500e-003 5.9828640e-002 5.5464300e-002 +v -1.2395240e-002 1.2264726e-001 3.3588280e-002 +v 1.3813780e-002 1.2322188e-001 3.2502590e-002 +v -7.7004310e-002 1.5521281e-001 2.4534770e-002 +v -2.8001360e-002 6.1075420e-002 3.7471210e-002 +v -8.5480000e-004 6.0593520e-002 5.5824810e-002 +v -3.8050200e-002 1.1527068e-001 3.3178540e-002 +v -1.6231340e-002 1.2382942e-001 2.9576990e-002 +v -2.5373550e-002 1.5840012e-001 -1.4801300e-003 +v -6.7818590e-002 1.5454353e-001 3.0233720e-002 +v -4.3082600e-003 6.1418570e-002 5.5688490e-002 +v -3.1958900e-003 1.1912518e-001 3.8349580e-002 +v -6.4292400e-003 1.2201090e-001 3.5740890e-002 +v 4.2312960e-002 5.9099150e-002 3.0848420e-002 +v 4.8510010e-002 6.1780760e-002 3.0347250e-002 +v 5.0412290e-002 6.0312610e-002 3.0245060e-002 +v -3.9185590e-002 6.3074530e-002 4.1382890e-002 +v -3.4448660e-002 6.0780500e-002 3.9543990e-002 +v -1.4746030e-002 6.5583910e-002 5.3730860e-002 +v 2.6645200e-003 6.2700010e-002 5.6525210e-002 +v -1.3991610e-002 1.1962575e-001 3.6251540e-002 +v 1.9659170e-002 1.1236219e-001 3.7545270e-002 +v -3.2597160e-002 1.7498725e-001 -2.5953100e-003 +v -2.1513900e-003 9.9437380e-002 4.9849750e-002 +v -5.6001390e-002 6.1830670e-002 2.7931150e-002 +v -5.4707260e-002 6.3461570e-002 3.1670590e-002 +v -5.1307940e-002 6.0521660e-002 3.1434930e-002 +v -4.1979320e-002 6.9629980e-002 4.1824930e-002 +v -3.0272490e-002 6.2474660e-002 3.7982220e-002 +v -1.1387860e-002 6.4742460e-002 5.4918000e-002 +v 6.9544900e-003 6.4700130e-002 5.5599150e-002 +v 4.3015090e-002 9.7690960e-002 1.0258300e-003 +v 4.0635900e-002 6.1574860e-002 2.9841250e-002 +v 4.6183560e-002 6.1910110e-002 3.0223400e-002 +v 3.7552960e-002 1.0685291e-001 2.6303470e-002 +v -7.8640730e-002 1.6387238e-001 -2.8387790e-002 +v -6.1996240e-002 1.4761484e-001 -4.3256800e-003 +v -5.7499800e-003 6.5488980e-002 5.6173390e-002 +v 2.5369000e-004 6.5741170e-002 5.6569260e-002 +v -2.0542550e-002 1.1979518e-001 3.3003670e-002 +v 4.3155900e-003 1.2782561e-001 2.8646880e-002 +v -4.6549580e-002 6.7652130e-002 3.9635790e-002 +v -1.7420580e-002 6.9659490e-002 5.4089530e-002 +v -1.5242190e-002 7.0909900e-002 5.5004790e-002 +v -1.0282890e-002 6.8926360e-002 5.5289610e-002 +v -1.1289000e-004 6.9288200e-002 5.6579790e-002 +v -3.6309330e-002 1.1876943e-001 3.0674020e-002 +v -7.0325800e-002 6.3367770e-002 1.9809180e-002 +v 4.3023100e-002 6.3795810e-002 2.8039210e-002 +v 4.2831110e-002 8.5556040e-002 2.7873760e-002 +v 1.6981600e-002 1.2715003e-001 2.2931490e-002 +v -4.2121490e-002 1.2825104e-001 1.0751500e-003 +v 1.6329230e-002 1.2251895e-001 3.1375390e-002 +v -8.1264160e-002 1.5381172e-001 2.5897830e-002 +v -3.2257870e-002 8.8192600e-002 -2.5130960e-002 +v -1.3774950e-002 7.0887950e-002 5.4695630e-002 +v 5.2929600e-003 6.8006030e-002 5.5670490e-002 +v 7.6962500e-003 7.2375600e-002 5.6062150e-002 +v 3.4830600e-003 1.2002635e-001 3.6911950e-002 +v 6.6532500e-003 1.1673563e-001 3.8716340e-002 +v 4.6086570e-002 6.6473930e-002 2.6808990e-002 +v 5.2327290e-002 6.4327070e-002 2.8281890e-002 +v -6.1897630e-002 1.2297065e-001 -8.7725500e-003 +v -6.3934700e-003 1.0524472e-001 -2.2841900e-002 +v -3.5218330e-002 6.8559830e-002 4.1381470e-002 +v -3.2689880e-002 6.7729720e-002 4.0124390e-002 +v -2.9245440e-002 6.9551520e-002 3.9369010e-002 +v -5.0024500e-003 6.9655000e-002 5.6892510e-002 +v 1.6573960e-002 1.1890153e-001 3.5042300e-002 +v -8.9385100e-002 9.9024040e-002 1.7521830e-002 +v 4.5719230e-002 6.9489400e-002 2.3549340e-002 +v 5.4537210e-002 6.8796720e-002 2.4517690e-002 +v -4.4989450e-002 7.1577330e-002 4.1929250e-002 +v -4.2439400e-003 1.2914902e-001 2.5829230e-002 +v -7.3880090e-002 1.2091638e-001 5.3395800e-002 +v -7.4033870e-002 1.4406894e-001 4.4994970e-002 +v 5.0400010e-002 6.7292480e-002 2.6851470e-002 +v -5.4056890e-002 1.5671602e-001 -2.4865900e-003 +v 2.6148110e-002 1.2014725e-001 2.7308010e-002 +v -1.0736490e-002 1.2990285e-001 1.0993790e-002 +v -4.5078840e-002 8.7261130e-002 -2.1865520e-002 +v -3.8340900e-002 6.8843770e-002 4.1846470e-002 +v -2.9255580e-002 7.5169210e-002 4.1186430e-002 +v -4.7311210e-002 1.6296037e-001 6.0740300e-003 +v -1.1866030e-002 7.3183750e-002 5.6250050e-002 +v -6.3734600e-003 7.2184340e-002 5.7972980e-002 +v -2.9935300e-003 7.2186440e-002 5.8167190e-002 +v -2.5781060e-002 9.3778180e-002 -2.8388220e-002 +v -1.6692560e-002 1.1568553e-001 3.7853150e-002 +v -8.4123410e-002 1.0832050e-001 2.4730980e-002 +v -7.4294080e-002 1.6356850e-001 -1.5534220e-002 +v -9.4297150e-002 1.2617744e-001 1.9224650e-002 +v -3.5207090e-002 1.2505219e-001 2.1635690e-002 +v -4.9495940e-002 7.3436340e-002 4.1673570e-002 +v -3.3064160e-002 7.6654840e-002 4.1277900e-002 +v -7.3157300e-003 7.3919590e-002 5.7971690e-002 +v 2.1850000e-005 7.3496040e-002 5.7696650e-002 +v 4.1934400e-003 7.2915170e-002 5.6298730e-002 +v -7.7256080e-002 1.4565854e-001 4.3122930e-002 +v 4.1073260e-002 8.8724320e-002 -9.7879400e-003 +v 3.7418710e-002 1.0850822e-001 3.3973000e-004 +v -5.5111380e-002 7.4687840e-002 4.1939740e-002 +v -4.2740230e-002 7.6995340e-002 4.2804080e-002 +v -6.8531190e-002 1.5630045e-001 2.0997710e-002 +v -9.9440200e-003 7.6343100e-002 5.7388560e-002 +v -3.2479200e-003 7.5710690e-002 5.8714640e-002 +v 1.3414380e-002 9.3073740e-002 5.1467750e-002 +v -7.3504440e-002 9.3883340e-002 -1.4751720e-002 +v -7.4471830e-002 1.3507476e-001 5.0688900e-002 +v -2.5851310e-002 1.2182948e-001 2.6079670e-002 +v -3.4022940e-002 1.7597076e-001 -3.7271600e-003 +v -7.5405850e-002 1.6839072e-001 -2.6792980e-002 +v -3.6658410e-002 7.5087300e-002 4.2006940e-002 +v -1.7795480e-002 7.7486190e-002 5.6087240e-002 +v -1.1378660e-002 7.9877150e-002 5.7698880e-002 +v -1.0415000e-004 7.6881950e-002 5.8190740e-002 +v 2.7381400e-003 7.9105680e-002 5.6719190e-002 +v 5.5681200e-003 7.6397140e-002 5.6327220e-002 +v -6.1895860e-002 1.5424247e-001 -1.9018600e-002 +v -7.2646960e-002 1.4098943e-001 4.6976640e-002 +v 1.5799740e-002 1.2901416e-001 1.3236870e-002 +v -1.1703420e-002 9.7355720e-002 5.1592080e-002 +v -5.8922160e-002 7.7545490e-002 4.2961390e-002 +v -5.3121320e-002 7.7912430e-002 4.3334920e-002 +v -5.0745740e-002 7.6148400e-002 4.3137630e-002 +v -4.7401820e-002 7.5550340e-002 4.2630140e-002 +v -4.5055620e-002 7.8796280e-002 4.2341310e-002 +v -3.9517650e-002 7.8127780e-002 4.2918620e-002 +v -1.5245570e-002 8.2940770e-002 5.6934590e-002 +v -1.4557790e-002 7.6582160e-002 5.6493250e-002 +v -5.9406000e-003 7.9038240e-002 5.7969830e-002 +v 3.7176540e-002 1.1064404e-001 1.8811330e-002 +v 2.3929700e-003 1.3162713e-001 1.1955100e-002 +v -9.3644210e-002 1.1789378e-001 1.8662080e-002 +v -6.3939810e-002 7.8621830e-002 4.2083520e-002 +v -4.5376460e-002 8.2383550e-002 4.3282120e-002 +v -3.6505460e-002 8.1152260e-002 4.3162320e-002 +v -3.3244340e-002 8.2266590e-002 4.1852180e-002 +v -3.0800650e-002 8.0068420e-002 4.1798070e-002 +v -2.0578500e-003 8.0998290e-002 5.7553840e-002 +v 8.1848100e-003 8.0756170e-002 5.5374510e-002 +v -1.2953370e-002 1.1593580e-001 3.8920230e-002 +v -7.8081470e-002 1.2351940e-001 5.2136990e-002 +v -2.6580930e-002 1.5567694e-001 -4.1963400e-003 +v -8.2471600e-002 1.1624130e-001 -2.3236300e-003 +v -2.7538480e-002 7.9964780e-002 4.7697210e-002 +v 1.2556400e-003 8.3845570e-002 5.7446440e-002 +v 6.1508300e-003 8.3406240e-002 5.6463500e-002 +v -6.2433240e-002 8.4035270e-002 4.4203120e-002 +v -5.9867170e-002 8.0540510e-002 4.3277090e-002 +v -5.5238340e-002 8.1999450e-002 4.4984770e-002 +v -5.4000400e-002 8.0568410e-002 4.4601460e-002 +v -5.0027020e-002 8.1311330e-002 4.4264180e-002 +v -4.1996120e-002 8.1083670e-002 4.2456150e-002 +v -3.9357940e-002 8.3631380e-002 4.3502350e-002 +v -8.6161480e-002 1.0838594e-001 1.8244920e-002 +v -8.6723010e-002 9.9917250e-002 3.5537100e-003 +v -2.2413700e-002 8.3283520e-002 5.5590700e-002 +v -1.6993180e-002 8.2555820e-002 5.7523880e-002 +v -1.2406010e-002 8.5222570e-002 5.7267780e-002 +v -7.4442100e-003 1.1693417e-001 3.9283850e-002 +v -2.1452000e-003 1.1143287e-001 4.2436620e-002 +v -7.5718220e-002 1.2522734e-001 5.3087330e-002 +v -7.7056660e-002 1.3193469e-001 5.2462430e-002 +v -6.1121040e-002 1.5569660e-001 2.2517050e-002 +v -3.7538540e-002 1.2744127e-001 1.5320870e-002 +v -2.0516700e-003 1.0093469e-001 4.5625920e-002 +v -6.4992150e-002 8.4550900e-002 4.4120060e-002 +v -5.7861950e-002 8.3944360e-002 4.4186030e-002 +v -4.5681080e-002 8.4988010e-002 4.4159500e-002 +v -3.5022640e-002 8.2888160e-002 4.2912760e-002 +v -2.9982010e-002 8.5402300e-002 4.3745080e-002 +v -8.8892260e-002 9.9209100e-002 9.5703200e-003 +v -1.9135300e-002 8.3474800e-002 5.7217390e-002 +v -8.3489710e-002 1.0724729e-001 7.5790000e-004 +v -7.0112800e-002 1.1790350e-001 5.2714160e-002 +v -3.5526320e-002 1.7595563e-001 -4.8676200e-003 +v -7.0831390e-002 1.2254425e-001 5.3274880e-002 +v 4.5133810e-002 9.3630690e-002 6.2336800e-003 +v -5.3616700e-002 8.5346850e-002 4.5332470e-002 +v -4.9000840e-002 8.6221680e-002 4.5352040e-002 +v -3.6744880e-002 8.6083690e-002 4.3612890e-002 +v -1.0872600e-002 8.8826770e-002 5.6665490e-002 +v -3.8450200e-003 8.4787810e-002 5.7197980e-002 +v -4.9020070e-002 1.1771293e-001 3.1581430e-002 +v -4.2914400e-002 1.1835991e-001 3.0645040e-002 +v -5.7684530e-002 1.5561695e-001 1.2983110e-002 +v -2.5411730e-002 1.2472533e-001 1.2886000e-004 +v 1.9012230e-002 1.2736197e-001 1.7786580e-002 +v -5.9498600e-002 8.8845470e-002 4.5109290e-002 +v -5.6931050e-002 8.8101500e-002 4.4692930e-002 +v 3.5765600e-003 1.3138981e-001 7.2086000e-003 +v -1.6683350e-002 8.7266690e-002 5.6741190e-002 +v -8.4980800e-003 8.3990470e-002 5.7605220e-002 +v 3.5078200e-003 8.6339520e-002 5.7048320e-002 +v -2.8398700e-002 1.8070650e-001 -7.8469500e-003 +v -7.6565830e-002 1.1674037e-001 5.1489350e-002 +v 1.7869430e-002 9.0898610e-002 4.8712940e-002 +v -4.0342100e-002 1.1669551e-001 3.2460200e-002 +v 5.9105700e-003 1.3140929e-001 1.6823750e-002 +v -8.5777550e-002 9.1701370e-002 -4.6970000e-005 +v -5.0372230e-002 8.8844660e-002 4.5188000e-002 +v -4.4434130e-002 8.7654530e-002 4.3477620e-002 +v -4.2056390e-002 8.6711520e-002 4.2534630e-002 +v -3.3058460e-002 8.6185500e-002 4.2560350e-002 +v -2.9241910e-002 9.0453360e-002 4.4236610e-002 +v -6.8964100e-003 8.4432910e-002 5.7168580e-002 +v -6.6210600e-003 9.0415250e-002 5.6879750e-002 +v -1.2439100e-003 8.9093200e-002 5.6552120e-002 +v 9.4076000e-003 9.0328050e-002 5.4214140e-002 +v 4.0194810e-002 1.0231597e-001 -2.0048600e-003 +v -8.6227130e-002 1.1466841e-001 2.2102000e-003 +v -8.9495490e-002 9.5632430e-002 1.4234810e-002 +v -6.7132160e-002 1.5709447e-001 -6.2032000e-003 +v -5.2935640e-002 9.0913520e-002 4.4568870e-002 +v -3.6744910e-002 8.8886950e-002 4.3312050e-002 +v -1.3626110e-002 8.9787930e-002 5.6674380e-002 +v 2.3337130e-002 1.2353449e-001 2.4874140e-002 +v -3.7053790e-002 1.2715094e-001 3.5474000e-004 +v -7.3696690e-002 1.5613015e-001 1.4359790e-002 +v -6.5592380e-002 9.1042400e-002 4.4092080e-002 +v -5.8997380e-002 9.2030670e-002 4.5335270e-002 +v -3.3238910e-002 8.8573580e-002 4.3697040e-002 +v -3.1834990e-002 9.0722970e-002 4.4173460e-002 +v -2.0022170e-002 8.8032110e-002 5.5589350e-002 +v -1.1213830e-002 9.2366370e-002 5.6105260e-002 +v 3.9108440e-002 1.0829072e-001 1.3142330e-002 +v 2.8675700e-002 1.1959600e-001 2.4545910e-002 +v -6.8940210e-002 1.5652777e-001 -1.9716000e-003 +v -6.2615110e-002 9.1126880e-002 4.5090730e-002 +v 3.0444560e-002 1.1886441e-001 2.0821750e-002 +v -1.5241090e-002 9.1821720e-002 5.5817230e-002 +v -5.6221700e-003 9.3235010e-002 5.5893630e-002 +v 4.7989900e-003 9.1654840e-002 5.4715170e-002 +v -6.8282400e-002 9.2376840e-002 4.2388730e-002 +v -5.5623730e-002 9.2187420e-002 4.5054970e-002 +v -5.1901030e-002 9.5457620e-002 4.3937650e-002 +v -4.8809030e-002 9.1083890e-002 4.4456690e-002 +v -4.5411560e-002 9.1002130e-002 4.3252770e-002 +v -4.4514550e-002 9.4860420e-002 4.2972490e-002 +v -3.9430320e-002 8.9597620e-002 4.3177890e-002 +v -3.5642240e-002 9.2617410e-002 4.4238490e-002 +v -1.2246000e-004 9.3201160e-002 5.5398380e-002 +v 9.5104600e-003 9.5483870e-002 5.0910600e-002 +v 2.1441660e-002 9.1354960e-002 4.8043360e-002 +v -8.9830300e-003 1.6926449e-001 -2.2683480e-002 +v -7.3019050e-002 1.5602104e-001 2.2419340e-002 +v -6.4760430e-002 1.5311588e-001 -2.0371200e-003 +v -6.9368510e-002 9.5242790e-002 4.2129000e-002 +v -6.0117140e-002 9.5552910e-002 4.4183820e-002 +v -2.9241690e-002 9.4290440e-002 4.4821190e-002 +v -2.6561430e-002 9.3289510e-002 4.4975420e-002 +v -1.4394030e-002 9.4587640e-002 5.3993500e-002 +v -8.8691600e-003 9.5400260e-002 5.4445980e-002 +v -1.2188700e-003 9.6201750e-002 5.3815910e-002 +v 4.0479000e-003 9.5817360e-002 5.2936770e-002 +v -4.6019400e-003 1.2428544e-001 3.3471960e-002 +v -7.8436460e-002 1.3928013e-001 4.8329360e-002 +v 1.0774610e-002 1.3079162e-001 1.4341740e-002 +v -5.6623730e-002 9.6322170e-002 4.3667910e-002 +v -3.6298870e-002 9.5695620e-002 4.3580310e-002 +v -2.4379930e-002 9.5866450e-002 4.4434530e-002 +v 1.0915500e-002 1.2633629e-001 2.9857020e-002 +v -5.8622700e-003 9.7350210e-002 5.2743650e-002 +v 1.6973450e-002 9.7106620e-002 4.7440920e-002 +v -6.7231980e-002 9.9173950e-002 4.1593880e-002 +v -5.4994210e-002 9.9640820e-002 4.2955230e-002 +v -4.8617990e-002 9.6452700e-002 4.4183060e-002 +v -5.5369000e-002 1.5442476e-001 1.6160650e-002 +v -9.4243550e-002 1.2207432e-001 2.3568470e-002 +v 1.3242990e-002 9.6738240e-002 4.8750160e-002 +v 2.0639290e-002 9.6602480e-002 4.6971000e-002 +v 7.3429700e-003 1.2098188e-001 3.5973430e-002 +v -1.3493870e-002 1.2882438e-001 5.9690700e-003 +v -2.0110640e-002 1.2504545e-001 2.3588310e-002 +v -6.9438450e-002 1.6479930e-001 -1.7218700e-002 +v -6.4028050e-002 9.7838670e-002 4.2565330e-002 +v -5.1996350e-002 9.9707850e-002 4.2716590e-002 +v -4.3990880e-002 9.9425460e-002 4.2383430e-002 +v -3.9738250e-002 1.0215357e-001 4.0574410e-002 +v -3.5931490e-002 9.9809950e-002 4.2335800e-002 +v -3.0867600e-002 9.6914680e-002 4.4651400e-002 +v -2.8342070e-002 9.7782680e-002 4.3761280e-002 +v -2.5622580e-002 9.8713420e-002 4.4210890e-002 +v -8.5236620e-002 1.1077356e-001 2.4537670e-002 +v 7.1936000e-003 9.8859470e-002 4.8419510e-002 +v 9.6509200e-003 1.0108782e-001 4.7373080e-002 +v 1.3487100e-002 1.0076420e-001 4.7454290e-002 +v 7.7389800e-003 1.3147500e-001 1.1682970e-002 +v 8.0905000e-004 1.1633319e-001 4.0167560e-002 +v -7.2652570e-002 1.6567918e-001 -1.8212480e-002 +v -5.6009400e-003 1.3076674e-001 1.0516060e-002 +v -2.6303720e-002 1.2518875e-001 1.7392980e-002 +v -4.7590430e-002 1.0081180e-001 4.2349150e-002 +v -4.1460830e-002 9.8544800e-002 4.1778620e-002 +v -3.3582070e-002 1.0383908e-001 4.0737990e-002 +v -2.2870240e-002 1.0284737e-001 4.3544750e-002 +v -2.2361970e-002 9.8207610e-002 4.4765940e-002 +v -1.8870510e-002 9.8973200e-002 4.4489280e-002 +v -7.1433690e-002 7.7573520e-002 3.8060760e-002 +v -7.3001150e-002 1.1826712e-001 5.3034590e-002 +v -6.8466430e-002 1.3498146e-001 -8.3359800e-003 +v -7.4683810e-002 1.0786100e-001 -9.0477100e-003 +v -6.4958960e-002 1.5852021e-001 -1.2595320e-002 +v -7.8931700e-002 1.5093057e-001 3.5151900e-002 +v -7.4113550e-002 9.9442520e-002 3.8337710e-002 +v -7.0456930e-002 1.0098777e-001 3.9794060e-002 +v -5.9058760e-002 1.0041260e-001 4.2725130e-002 +v -4.9187330e-002 1.0452012e-001 4.0301390e-002 +v -2.9151180e-002 1.0197369e-001 4.2633060e-002 +v -1.1599720e-002 1.0107813e-001 4.4191660e-002 +v 5.1450400e-003 1.0163906e-001 4.5423010e-002 +v -5.1495700e-002 1.0496738e-001 4.0347210e-002 +v -2.0218210e-002 1.0214391e-001 4.3701160e-002 +v 4.2515900e-003 1.0523743e-001 4.2563550e-002 +v 1.6832800e-002 1.0337487e-001 4.5287270e-002 +v -2.5661080e-002 1.2562669e-001 4.5537500e-003 +v -7.2141950e-002 1.0536685e-001 3.7523210e-002 +v -6.4984570e-002 1.0371550e-001 4.0647810e-002 +v -6.0652480e-002 1.0467197e-001 4.0906390e-002 +v -5.5308980e-002 1.0365394e-001 4.1516690e-002 +v -4.4243240e-002 1.0431726e-001 4.1339990e-002 +v -1.5513340e-002 1.0436131e-001 4.2919420e-002 +v -7.6323200e-003 1.0304531e-001 4.3710640e-002 +v -7.8046900e-003 1.0516619e-001 4.3825460e-002 +v 9.7163200e-003 1.0523506e-001 4.3603830e-002 +v 3.0300390e-002 1.1553645e-001 2.8685010e-002 +v -4.7496910e-002 1.0635662e-001 4.0165640e-002 +v -3.8978950e-002 1.0683037e-001 3.8247660e-002 +v -2.5869310e-002 1.0426705e-001 4.2207540e-002 +v -1.8057930e-002 1.0503919e-001 4.2802830e-002 +v -1.5180030e-002 1.0807750e-001 4.2350430e-002 +v -3.8981500e-003 1.0566175e-001 4.4047190e-002 +v 2.6820000e-005 1.0446731e-001 4.3775910e-002 +v 1.1978350e-002 1.0403629e-001 4.5396310e-002 +v 1.5004970e-002 1.0726898e-001 4.1811990e-002 +v 2.6488060e-002 1.2230287e-001 2.0398110e-002 +v -3.6225630e-002 1.0634244e-001 3.8644860e-002 +v -2.1126780e-002 1.0932290e-001 4.0715320e-002 +v -1.2819810e-002 1.0457100e-001 4.3465690e-002 +v 5.2847900e-003 1.0943666e-001 4.1674980e-002 +v 8.9403700e-003 1.0710645e-001 4.1243400e-002 +v -5.1839670e-002 1.6062039e-001 7.1421300e-003 +v -5.4201370e-002 1.1451730e-001 3.4843990e-002 +v 1.3226250e-002 1.2958070e-001 1.9689610e-002 +v -6.9382410e-002 1.0865787e-001 3.7507800e-002 +v -6.7691040e-002 1.0734145e-001 3.8018440e-002 +v -6.3782400e-002 1.1037270e-001 3.7579790e-002 +v -5.0749390e-002 1.0928682e-001 3.8297580e-002 +v -9.3936200e-003 1.0742813e-001 4.3454570e-002 +v 1.1760100e-003 1.0932531e-001 4.2662800e-002 +v 9.8020300e-003 1.1003994e-001 3.9945400e-002 +v 2.0131290e-002 1.0732778e-001 4.0323840e-002 +v -2.7872800e-003 1.0577531e-001 -2.2459030e-002 +v -5.4996890e-002 1.0774199e-001 3.9424590e-002 +v -4.5966740e-002 1.0905146e-001 3.8754110e-002 +v -4.2324540e-002 1.0737278e-001 3.9456440e-002 +v -3.2161240e-002 1.0896504e-001 3.8102720e-002 +v -3.0770180e-002 1.1597313e-001 3.2858800e-002 +v -1.1608610e-002 1.0983707e-001 4.2475330e-002 +v -2.9428320e-002 9.3166620e-002 -2.4931860e-002 +v -8.0043570e-002 9.2080160e-002 -9.4198200e-003 +v -4.9797430e-002 1.1342104e-001 3.5117920e-002 +v -4.3723850e-002 1.6191369e-001 5.7713400e-003 +v -5.7981740e-002 1.0943152e-001 3.7997640e-002 +v -4.1491180e-002 1.1224766e-001 3.5873450e-002 +v -2.4929830e-002 1.1592775e-001 3.4094730e-002 +v -2.0881690e-002 1.1409528e-001 3.7872990e-002 +v -7.5519700e-003 1.1183813e-001 4.2039690e-002 +v 3.7667200e-003 1.1240547e-001 4.1494710e-002 +v -6.2829620e-002 1.5189480e-001 -9.2373400e-003 +v -5.9195950e-002 1.1320797e-001 3.6234680e-002 +v -5.1079080e-002 9.3892810e-002 -2.1761690e-002 +v -7.3945370e-002 8.4374880e-002 -1.5154490e-002 +v -7.2146240e-002 1.3486431e-001 -7.7592200e-003 +v -1.9408870e-002 1.7041104e-001 -2.0994830e-002 +v -5.5530450e-002 1.4905531e-001 -1.9602100e-003 +v 1.6688460e-002 3.6976600e-002 4.3000600e-002 +v -5.2277330e-002 1.1775075e-001 3.3769460e-002 +v -6.9201380e-002 9.3039200e-002 -1.6486120e-002 +v 2.6579210e-002 1.1702438e-001 3.0867940e-002 +v -2.3574310e-002 3.7036910e-002 5.4144750e-002 +v -7.3775100e-003 3.8988430e-002 4.8929450e-002 +v 1.3234660e-002 3.8453060e-002 4.4501470e-002 +v 1.9487350e-002 4.0809290e-002 4.2641060e-002 +v -6.3953930e-002 1.4694729e-001 3.8484200e-002 +v -4.9579470e-002 3.6096540e-002 4.5955360e-002 +v -4.3323650e-002 3.6286400e-002 4.4042360e-002 +v -2.9047200e-002 1.2556338e-001 7.7617700e-003 +v -1.7343100e-003 3.9476800e-002 4.7262900e-002 +v -3.1358130e-002 1.5362199e-001 -4.6738900e-003 +v 2.5822000e-003 1.0747582e-001 -2.0606030e-002 +v -5.6802300e-002 1.4514674e-001 3.1740300e-002 +v -5.6464330e-002 3.7683110e-002 4.6819640e-002 +v -5.0964750e-002 3.8312290e-002 4.6286140e-002 +v -5.0980410e-002 1.3486613e-001 2.7585000e-002 +v -2.5647410e-002 3.8860730e-002 5.4161390e-002 +v -2.2542110e-002 4.0615780e-002 5.3986030e-002 +v -1.7618010e-002 3.8911170e-002 5.2403440e-002 +v -1.9711750e-002 1.6829145e-001 -1.3020960e-002 +v 2.3780070e-002 9.5222940e-002 4.6347330e-002 +v 1.4744290e-002 4.2716950e-002 4.4510310e-002 +v 2.1691360e-002 4.0161530e-002 4.0846450e-002 +v -6.4067240e-002 9.0172190e-002 -1.8855520e-002 +v 2.0319150e-002 1.0041961e-001 4.5760520e-002 +v -3.6425000e-002 9.3630690e-002 -2.3534630e-002 +v -1.4981170e-002 4.2571420e-002 5.1404530e-002 +v -5.7335340e-002 1.2340101e-001 4.0231470e-002 +v -5.4172560e-002 1.2337919e-001 3.7576440e-002 +v 2.2625210e-002 4.3621680e-002 4.0904580e-002 +v 2.8810520e-002 4.3352290e-002 3.2157720e-002 +v -4.2764160e-002 1.5727487e-001 5.2016200e-003 +v 9.2231900e-003 4.4125090e-002 4.5057440e-002 +v 1.5048210e-002 4.5755840e-002 4.3793870e-002 +v -6.3757290e-002 1.0251144e-001 -1.7484400e-002 +v -3.4070430e-002 1.6148975e-001 -1.3786960e-002 +v -8.2191500e-002 7.5610200e-002 1.6542620e-002 +v -6.6299420e-002 1.2337119e-001 5.0615920e-002 +v -1.5510100e-002 4.5283110e-002 5.0653040e-002 +v 1.8928020e-002 4.4249610e-002 4.3009830e-002 +v 2.5821800e-002 4.6326610e-002 3.8277230e-002 +v 2.7268700e-002 4.4547790e-002 3.6152520e-002 +v -4.5301340e-002 1.5695057e-001 7.2036900e-003 +v 2.3855760e-002 1.0616625e-001 3.9378080e-002 +v 2.1632670e-002 4.8127270e-002 4.0694430e-002 +v 4.3785360e-002 4.8803700e-002 3.1343420e-002 +v 4.8074790e-002 4.8969960e-002 2.8165490e-002 +v 5.2663090e-002 4.7673620e-002 2.1201270e-002 +v -5.2722450e-002 4.4722850e-002 4.4143250e-002 +v -3.0071610e-002 1.7258324e-001 -6.3597700e-003 +v -3.4508050e-002 1.5447469e-001 1.6504600e-003 +v 1.0629710e-002 4.6711810e-002 4.6472020e-002 +v 1.6743440e-002 4.8439000e-002 4.3678630e-002 +v 2.8827050e-002 9.2133370e-002 4.3920090e-002 +v -5.9937100e-002 1.2726188e-001 4.0771270e-002 +v -3.6752090e-002 1.5802075e-001 4.1862000e-003 +v -3.7885390e-002 1.6199719e-001 2.4686000e-004 +v -2.2047790e-002 1.8348586e-001 -1.2094990e-002 +v -2.4364620e-002 1.8096836e-001 -9.8312000e-003 +v -4.4882280e-002 1.5052959e-001 7.6451700e-003 +v 2.6996760e-002 5.1317780e-002 3.8752040e-002 +v 4.7735750e-002 5.2751040e-002 3.0797290e-002 +v 5.1703790e-002 4.8857380e-002 2.4147970e-002 +v -6.7504360e-002 1.1424088e-001 4.8036050e-002 +v -1.6257520e-002 1.6031250e-001 -9.6926000e-003 +v -6.3926300e-002 1.6792441e-001 -4.0730420e-002 +v -4.1665290e-002 1.4996141e-001 4.5405000e-003 +v -3.5203230e-002 1.6493551e-001 -2.6810000e-003 +v 4.1318770e-002 9.9496740e-002 2.4275750e-002 +v 1.4055220e-002 5.2523910e-002 4.8593880e-002 +v 1.9421220e-002 5.1321300e-002 4.4798910e-002 +v 2.3677990e-002 5.1474390e-002 4.1053270e-002 +v 3.4258130e-002 5.1930810e-002 3.2757880e-002 +v 5.5957340e-002 5.3147410e-002 2.3197720e-002 +v -3.9937960e-002 1.4922850e-001 1.6017200e-003 +v -4.6988800e-002 1.2600802e-001 2.6985500e-002 +v -2.7708370e-002 9.0081290e-002 -3.1911460e-002 +v 1.9204630e-002 5.5166510e-002 4.7722150e-002 +v 2.1886000e-002 5.3927560e-002 4.5102460e-002 +v 3.1286270e-002 5.2863840e-002 3.6913620e-002 +v 4.6661160e-002 5.4719230e-002 3.1976810e-002 +v 5.1823730e-002 5.3276700e-002 2.7927010e-002 +v -2.9264880e-002 1.6140418e-001 -2.1039500e-003 +v -6.8700770e-002 1.4463537e-001 4.3041630e-002 +v -5.6070060e-002 1.5000706e-001 2.9867640e-002 +v 4.4717850e-002 9.4802660e-002 1.2024710e-002 +v -4.1804090e-002 1.5582081e-001 6.4548200e-003 +v -6.8369340e-002 1.2289287e-001 5.2437860e-002 +v -6.4114810e-002 9.5509880e-002 -1.8114610e-002 +v -1.8383130e-002 1.8543664e-001 -1.7136370e-002 +v 1.1745400e-002 5.6678340e-002 5.1914060e-002 +v -5.9375360e-002 1.1998238e-001 4.0548240e-002 +v 5.9092080e-002 5.7956980e-002 2.0270120e-002 +v 4.3547740e-002 9.7389400e-002 1.7314650e-002 +v -2.6291780e-002 1.5963381e-001 -5.1845000e-004 +v 1.4904780e-002 5.6350380e-002 4.9522780e-002 +v 2.4286200e-002 5.4958580e-002 4.3086850e-002 +v 2.8952610e-002 5.6125250e-002 4.0388970e-002 +v -4.9507770e-002 1.2949500e-001 3.0259270e-002 +v 4.0824790e-002 9.5170220e-002 2.8657920e-002 +v 1.7774800e-002 5.8243780e-002 4.8864720e-002 +v 3.3573840e-002 5.8515260e-002 3.8310990e-002 +v 3.6385040e-002 5.6996480e-002 3.3601460e-002 +v -6.4205010e-002 1.2243894e-001 4.8008340e-002 +v -6.5424500e-002 1.4011279e-001 4.1308960e-002 +v 5.0801340e-002 5.7308080e-002 3.0001390e-002 +v 5.6671750e-002 5.6970820e-002 2.4291920e-002 +v -4.9349930e-002 1.4913519e-001 1.1274060e-002 +v -6.9760570e-002 1.3442855e-001 4.8265220e-002 +v 1.9537060e-002 6.0003780e-002 4.8576140e-002 +v 2.7013910e-002 5.9952790e-002 4.3454420e-002 +v 5.7679430e-002 6.1392970e-002 2.4201790e-002 +v -5.6916540e-002 1.2623512e-001 3.9426610e-002 +v 2.3469280e-002 1.1656262e-001 3.3537270e-002 +v -5.8298640e-002 1.3885500e-001 3.2937460e-002 +v 6.4598400e-003 6.0297430e-002 5.4780030e-002 +v 1.0406020e-002 5.9162400e-002 5.2484370e-002 +v 2.3183950e-002 5.8654360e-002 4.5871060e-002 +v 3.3040360e-002 6.1773840e-002 3.9781440e-002 +v -6.4348220e-002 1.2628088e-001 4.6650200e-002 +v -5.7031440e-002 1.1562007e-001 3.6494880e-002 +v 5.4451560e-002 5.8342890e-002 2.7653010e-002 +v -3.0134400e-002 1.7011322e-001 -7.3591600e-003 +v -3.7077100e-002 1.5986369e-001 1.6096500e-003 +v -5.6032760e-002 1.3731083e-001 3.1970590e-002 +v -6.7676470e-002 1.4150325e-001 4.3868140e-002 +v 9.9911700e-003 6.2735270e-002 5.4009240e-002 +v 1.4521510e-002 6.1382890e-002 5.0500900e-002 +v 3.0051740e-002 6.2169610e-002 4.1545810e-002 +v 3.7519170e-002 6.1062710e-002 3.4366020e-002 +v 5.3944010e-002 6.1391550e-002 2.8268530e-002 +v 5.9119900e-002 6.3128810e-002 2.1561830e-002 +v -2.4366390e-002 1.7693266e-001 -1.1719630e-002 +v -1.3253420e-002 1.6627152e-001 -1.4120370e-002 +v 3.9218740e-002 1.0669250e-001 2.0450190e-002 +v -1.7968980e-002 1.8078031e-001 -1.8103430e-002 +v 2.1902390e-002 6.0875970e-002 4.7282360e-002 +v 3.5341750e-002 6.1630030e-002 3.7606020e-002 +v -6.2145620e-002 1.3599775e-001 3.6700970e-002 +v 5.6820620e-002 6.3691150e-002 2.5286090e-002 +v -3.2800040e-002 1.5948699e-001 2.1962800e-003 +v 1.1212140e-002 6.6584120e-002 5.3982180e-002 +v 1.2919590e-002 6.4203580e-002 5.2441150e-002 +v 2.0126950e-002 6.3851330e-002 4.7919660e-002 +v 3.5971760e-002 6.6669610e-002 3.7781400e-002 +v 3.9906940e-002 6.4361260e-002 3.1686660e-002 +v -6.6702350e-002 1.3210600e-001 4.5480940e-002 +v -4.1601430e-002 1.5978000e-001 3.5374700e-003 +v 3.3044580e-002 1.0766252e-001 3.1916150e-002 +v 2.4672100e-002 6.3694500e-002 4.5204640e-002 +v 2.6108660e-002 6.8007640e-002 4.3902690e-002 +v 3.3363940e-002 6.7054760e-002 3.9729480e-002 +v 4.2915790e-002 6.6707700e-002 2.6994720e-002 +v 5.4714960e-002 6.4697160e-002 2.6979680e-002 +v -1.6530940e-002 1.6325000e-001 -9.2475200e-003 +v -1.7891600e-002 1.6113800e-001 -6.7072700e-003 +v 4.1118120e-002 9.7491260e-002 -3.9756700e-003 +v 2.3386770e-002 7.0075990e-002 4.7012620e-002 +v 3.8102900e-002 6.5678440e-002 3.5132520e-002 +v 1.0145240e-002 1.2221678e-001 3.4718950e-002 +v 5.8392410e-002 6.6741240e-002 2.1979460e-002 +v 3.8302050e-002 8.4549140e-002 -1.4478830e-002 +v 3.4126440e-002 9.7053980e-002 3.7590390e-002 +v -3.1355740e-002 1.5809888e-001 1.9128800e-003 +v -5.8259510e-002 1.4099493e-001 3.2440640e-002 +v -6.6817230e-002 1.1951525e-001 5.1490220e-002 +v -6.8090040e-002 1.1647050e-001 5.1151230e-002 +v 1.6568300e-002 6.6269890e-002 5.1009890e-002 +v 2.9362870e-002 6.6509780e-002 4.2289380e-002 +v 3.7027180e-002 9.3949630e-002 -1.1674040e-002 +v 5.6412730e-002 6.7659930e-002 2.3969320e-002 +v -6.1295740e-002 1.4519988e-001 3.7137830e-002 +v 8.3873000e-003 1.1336223e-001 3.9792610e-002 +v 1.1807030e-002 7.0920980e-002 5.4240490e-002 +v 2.9741730e-002 7.0647100e-002 4.1653890e-002 +v 3.6294410e-002 7.1220700e-002 3.7114610e-002 +v 3.9899680e-002 7.0294820e-002 3.2720020e-002 +v -6.2763130e-002 1.3778012e-001 3.6678590e-002 +v -1.5815440e-002 1.7504938e-001 -1.8654160e-002 +v -9.2268990e-002 1.1475156e-001 1.7017380e-002 +v -9.4964000e-004 1.0141111e-001 4.4290070e-002 +v -6.3712920e-002 1.1274250e-001 3.8006760e-002 +v -6.1096020e-002 1.1701650e-001 3.9654020e-002 +v 2.0991870e-002 6.9335450e-002 4.9003540e-002 +v 2.5658530e-002 7.0550460e-002 4.4539930e-002 +v 3.2978560e-002 7.3500690e-002 4.0486510e-002 +v 4.2156130e-002 6.9717580e-002 2.8318230e-002 +v -5.5516860e-002 1.2956070e-001 3.6598450e-002 +v -4.0802290e-002 1.6436059e-001 3.7448800e-003 +v -6.2546500e-003 1.0121650e-001 4.4322030e-002 +v -1.0986820e-002 1.6621199e-001 -1.6047550e-002 +v -3.0351420e-002 1.6448158e-001 -5.3291400e-003 +v 2.6110920e-002 1.0088990e-001 4.1733260e-002 +v -6.5599940e-002 1.1329504e-001 4.2318710e-002 +v 2.8814660e-002 9.6712680e-002 4.2257700e-002 +v 1.5263280e-002 7.1571940e-002 5.2717390e-002 +v 2.8982400e-002 7.4088480e-002 4.3447240e-002 +v 4.4872540e-002 7.5516710e-002 2.3155250e-002 +v -7.8225230e-002 1.4962481e-001 -2.5019400e-003 +v -4.6094940e-002 1.5296850e-001 9.0029700e-003 +v -5.2369030e-002 1.4682913e-001 1.8934650e-002 +v -2.1592100e-002 1.5763440e-001 -6.8623600e-003 +v 1.7176770e-002 7.3066230e-002 5.1826600e-002 +v 2.2687500e-002 7.5149180e-002 4.9312500e-002 +v 3.5472040e-002 7.3076670e-002 3.8482270e-002 +v -8.9480840e-002 1.3839976e-001 2.5061450e-002 +v -5.3216730e-002 1.3221978e-001 3.2978380e-002 +v -3.7776780e-002 1.5551947e-001 4.3700800e-003 +v -9.0549380e-002 1.3511875e-001 2.1680550e-002 +v -6.3366580e-002 1.3037076e-001 4.1669940e-002 +v 1.4074270e-002 7.6651720e-002 5.4221350e-002 +v 1.8109790e-002 7.5806590e-002 5.2488260e-002 +v 4.2209940e-002 7.8861480e-002 2.9187200e-002 +v -5.2115930e-002 1.4179906e-001 2.0510310e-002 +v 2.9063090e-002 1.1149602e-001 3.3805790e-002 +v -5.4731460e-002 1.4267229e-001 2.8980480e-002 +v 2.5903640e-002 7.5536040e-002 4.6416650e-002 +v 3.1298760e-002 7.5907440e-002 4.2699060e-002 +v 3.8446170e-002 7.5649430e-002 3.5050640e-002 +v 4.6351670e-002 7.4079520e-002 1.8354320e-002 +v -4.7656560e-002 1.3077525e-001 2.5523570e-002 +v -1.1447430e-002 1.7131059e-001 -1.9602980e-002 +v -3.6647240e-002 1.6640131e-001 -2.8167000e-004 +v -4.6653530e-002 1.5917824e-001 7.8019000e-003 +v -4.5569890e-002 1.4663612e-001 5.6514200e-003 +v 4.1438880e-002 9.2365100e-002 -7.4587000e-003 +v -6.4287420e-002 1.3463625e-001 3.9945640e-002 +v -6.1128890e-002 1.3178328e-001 3.8915910e-002 +v -4.7843540e-002 1.2215063e-001 2.8833160e-002 +v -4.9536830e-002 1.2491344e-001 3.1778440e-002 +v -7.1135380e-002 1.3817656e-001 4.7853960e-002 +v 1.0113870e-002 7.6468110e-002 5.5256790e-002 +v 1.7897450e-002 7.9516550e-002 5.2759530e-002 +v 2.1740850e-002 8.0250650e-002 5.0425390e-002 +v 2.5271590e-002 7.8724920e-002 4.8026570e-002 +v 3.0885040e-002 7.8999480e-002 4.3388770e-002 +v -6.2441930e-002 1.4084781e-001 3.6965840e-002 +v -6.2165060e-002 1.5666850e-001 -1.7837760e-002 +v 2.0657260e-002 1.0416830e-001 4.3004680e-002 +v -6.3602800e-002 1.1571453e-001 4.2572290e-002 +v 1.4424020e-002 8.0085500e-002 5.3755600e-002 +v 2.8779340e-002 8.2553250e-002 4.4527350e-002 +v 4.4450130e-002 8.1846900e-002 2.4552920e-002 +v 4.5541990e-002 8.3338380e-002 1.9700850e-002 +v -4.9665810e-002 1.2063801e-001 3.2163270e-002 +v -2.9177290e-002 1.7619959e-001 -5.6241100e-003 +v -5.8203130e-002 1.3270975e-001 3.6918680e-002 +v 3.8997050e-002 9.7088220e-002 -7.7799300e-003 +v -5.4725800e-002 1.2071262e-001 3.7451450e-002 +v 1.3189120e-002 8.4211180e-002 5.3065830e-002 +v -1.9926300e-002 1.6489742e-001 -9.9900200e-003 +v 2.0153130e-002 1.1849719e-001 3.4271250e-002 +v -5.5859940e-002 1.1774313e-001 3.7253480e-002 +v 1.8045260e-002 8.3623160e-002 5.1285840e-002 +v -6.3757130e-002 1.5912175e-001 -5.0155730e-002 +v -1.8527620e-002 1.7653197e-001 -1.7043540e-002 +v 2.8734400e-002 1.0360053e-001 3.8035240e-002 +v 4.1414010e-002 1.0284216e-001 1.6578920e-002 +v 2.4411730e-002 9.8016880e-002 4.4687400e-002 +v 2.0925180e-002 8.6311430e-002 4.9433120e-002 +v 3.0445010e-002 8.4959560e-002 4.3011090e-002 +v 3.3030090e-002 8.3781640e-002 4.1636930e-002 +v 3.6975090e-002 7.9876480e-002 3.7198390e-002 +v -7.7721460e-002 1.1355888e-001 4.8155990e-002 +v 2.9250000e-002 1.0651935e-001 3.6590330e-002 +v -5.3078180e-002 1.3754688e-001 2.8266470e-002 +v -6.2990590e-002 1.1999459e-001 4.5235530e-002 +v -6.5398320e-002 1.1751956e-001 4.8735570e-002 +v 3.3373910e-002 1.1227890e-001 2.7788130e-002 +v 3.8413590e-002 8.7489930e-002 3.5185850e-002 +v -6.1945930e-002 1.6479234e-001 -5.6647670e-002 +v -2.2876480e-002 1.7392813e-001 -1.3431140e-002 +v 4.3766230e-002 8.8390020e-002 -3.5708800e-003 +v 3.9291530e-002 1.0125969e-001 2.7550520e-002 +v 1.0936230e-002 8.6027290e-002 5.4732670e-002 +v 2.4108720e-002 8.4492600e-002 4.8292310e-002 +v 3.6758390e-002 9.9195470e-002 3.2837670e-002 +v -5.1941640e-002 1.2565987e-001 3.4587860e-002 +v -3.1582110e-002 1.6641850e-001 -5.7320000e-003 +v 7.6405900e-003 8.6427230e-002 5.6117850e-002 +v 1.6771020e-002 8.8644690e-002 5.0522960e-002 +v 3.4404610e-002 8.6932850e-002 4.0574270e-002 +v 3.6143820e-002 8.4439200e-002 3.7936930e-002 +v 4.1258830e-002 1.0361081e-001 2.6760600e-003 +v 2.4766140e-002 1.1081111e-001 3.6728360e-002 +v -2.2601590e-002 1.6250449e-001 -6.0717000e-003 +v -1.2893670e-002 1.7879041e-001 -2.2624750e-002 +v -2.4939150e-002 1.7031135e-001 -1.1329700e-002 +v -4.8468630e-002 1.4559606e-001 8.3661500e-003 +v 1.2534490e-002 8.9593930e-002 5.3394630e-002 +v 2.5872860e-002 8.8482290e-002 4.6655260e-002 +v 3.2756470e-002 8.8969130e-002 4.2215450e-002 +v -2.3343620e-002 1.6103450e-001 -3.1862400e-003 +v -9.2594970e-002 1.1943826e-001 2.6802950e-002 +v -7.4314840e-002 1.3761738e-001 -6.6698800e-003 +v -9.2499230e-002 1.2131500e-001 2.9256200e-002 +v -7.7378260e-002 1.5764266e-001 -1.4133650e-002 +v -9.2907340e-002 1.2307021e-001 3.6523230e-002 +v 2.8423340e-002 8.8011080e-002 4.4234200e-002 +v 3.5251680e-002 9.0836820e-002 3.9183920e-002 +v 1.5760560e-002 9.3203560e-002 4.9939310e-002 +v 3.8785530e-002 9.4954300e-002 3.2520220e-002 +v -6.1511220e-002 1.2373565e-001 4.3062680e-002 +v -6.8145120e-002 1.2748676e-001 5.0148970e-002 +v -2.0616710e-002 1.8237588e-001 -1.4299100e-002 +v 1.5137190e-002 1.1571495e-001 3.7031980e-002 +v -5.0718270e-002 1.5276300e-001 1.1816680e-002 +v 3.0168690e-002 1.0048686e-001 3.9404710e-002 +v -8.7426500e-002 9.5469530e-002 4.0312400e-003 +v -6.0010390e-002 1.4284463e-001 3.5449690e-002 +v -5.8603310e-002 1.4637237e-001 3.3808800e-002 +v 3.2411650e-002 9.3736150e-002 4.0890240e-002 +v -7.5917780e-002 1.4997690e-001 -1.6842050e-002 +v 1.8596570e-002 3.5293940e-002 -8.6782200e-003 +v 1.7209800e-002 3.5259400e-002 -1.4685160e-002 +v 4.4326540e-002 9.0818120e-002 2.2097520e-002 +v 3.8335910e-002 3.8830830e-002 3.0938100e-003 +v 2.2192920e-002 3.6775320e-002 -2.0919300e-003 +v 1.9636020e-002 3.8234010e-002 -1.2507670e-002 +v 2.3682120e-002 3.9762540e-002 3.7148760e-002 +v 4.6693280e-002 4.2465320e-002 6.5649500e-003 +v 2.1621110e-002 3.7657240e-002 -4.7021600e-003 +v 1.6638610e-002 3.8196090e-002 -1.9884930e-002 +v -9.0253980e-002 1.1366307e-001 3.7720210e-002 +v -9.0593870e-002 1.1373094e-001 1.0276770e-002 +v -6.2541690e-002 1.7679461e-001 -5.7821820e-002 +v -1.1091940e-002 1.7992082e-001 -2.5996430e-002 +v -6.2263130e-002 1.5219935e-001 -2.2578880e-002 +v -4.2276760e-002 9.4982570e-002 -2.2562420e-002 +v 4.3293410e-002 4.1864140e-002 2.0634400e-003 +v 4.3779590e-002 4.4530720e-002 -1.2622500e-003 +v 2.1696990e-002 4.0427270e-002 -9.4629500e-003 +v -1.1183700e-002 1.6450000e-001 -1.6151690e-002 +v -6.2372570e-002 1.5313041e-001 -2.8997120e-002 +v -9.2489300e-003 1.7725850e-001 -2.8270200e-002 +v 4.1477400e-002 8.5509410e-002 -9.1575000e-003 +v -8.1268710e-002 1.0879438e-001 2.9440660e-002 +v 4.9575680e-002 4.3815900e-002 1.4582960e-002 +v 5.2987960e-002 4.7747690e-002 5.0420000e-003 +v 2.1977540e-002 4.2855330e-002 -1.4536230e-002 +v 1.8505700e-002 3.8294100e-002 -1.7136500e-002 +v -3.5100500e-002 1.5203437e-001 -1.3279000e-004 +v 4.8749130e-002 4.5265000e-002 2.3023500e-003 +v 3.1912900e-002 9.9870060e-002 -1.4620980e-002 +v -1.4222520e-002 1.6167426e-001 -1.3349060e-002 +v -4.8663640e-002 1.3638523e-001 6.8063900e-003 +v -9.5837200e-003 1.7426102e-001 -2.8390760e-002 +v 5.2801850e-002 4.6539940e-002 1.0427720e-002 +v 5.1433800e-002 4.8485200e-002 1.0401000e-003 +v 2.3911240e-002 9.8021670e-002 -2.0807290e-002 +v 2.4567060e-002 4.4130110e-002 -1.0820840e-002 +v 2.0356810e-002 4.3662400e-002 -2.0456280e-002 +v -2.1882420e-002 1.1087418e-001 -1.9695320e-002 +v -5.3831800e-002 1.4981693e-001 2.5066610e-002 +v 5.4114210e-002 4.7773090e-002 1.7484000e-002 +v 5.6730570e-002 5.0515740e-002 1.0627080e-002 +v 4.5941820e-002 4.8138820e-002 -3.8715700e-003 +v -8.3817760e-002 1.1109094e-001 2.8524490e-002 +v 2.9207770e-002 4.7450250e-002 -8.5081800e-003 +v 2.8454920e-002 4.8067390e-002 -1.2847240e-002 +v 2.6637260e-002 4.7607100e-002 -1.6427740e-002 +v 2.2040110e-002 4.4992500e-002 -1.7528500e-002 +v 1.9120080e-002 4.7167750e-002 -2.2114680e-002 +v -1.5782200e-002 1.0072957e-001 -2.3724130e-002 +v -6.2514170e-002 1.7213119e-001 -5.2788100e-002 +v -6.2345600e-002 1.4745498e-001 -7.6600200e-003 +v 4.5598180e-002 8.8151720e-002 1.3124070e-002 +v -4.9422610e-002 1.4283525e-001 8.9728300e-003 +v -8.2761860e-002 1.1162341e-001 4.4221460e-002 +v -5.2166220e-002 1.5013661e-001 1.7448750e-002 +v -6.3616740e-002 1.4801371e-001 -2.0170260e-002 +v -5.1492690e-002 1.3796388e-001 2.3662180e-002 +v -6.1517580e-002 1.7517449e-001 -6.0631700e-002 +v 5.6524870e-002 5.0125660e-002 1.5564490e-002 +v 5.5257900e-002 5.1416260e-002 3.2062600e-003 +v 5.0318130e-002 5.2786370e-002 -3.4166300e-003 +v -6.2681950e-002 1.6744086e-001 -4.5713890e-002 +v 5.6520150e-002 5.1179900e-002 1.9940560e-002 +v 5.6907980e-002 5.1578130e-002 7.2538300e-003 +v 5.2854160e-002 5.1898670e-002 -6.2070000e-004 +v -3.8921140e-002 3.3767390e-002 -2.9042560e-002 +v 2.9740700e-002 5.0324690e-002 -1.3990860e-002 +v -6.8796190e-002 3.5117720e-002 -5.2067400e-003 +v 5.8826020e-002 5.5503780e-002 1.8647920e-002 +v -2.6160570e-002 1.2309988e-001 -4.4735500e-003 +v -5.3341960e-002 1.4401200e-001 2.4261390e-002 +v 5.8177390e-002 5.2821320e-002 1.5182420e-002 +v 5.9798140e-002 5.6840180e-002 1.3342730e-002 +v 5.4549870e-002 5.6044630e-002 -6.6158000e-004 +v 2.6775460e-002 5.1423450e-002 -2.0234060e-002 +v -8.6960400e-003 1.7291588e-001 -2.6708770e-002 +v -7.7039560e-002 7.1967020e-002 2.6405070e-002 +v -6.3069890e-002 1.5897471e-001 -4.2951850e-002 +v 3.5706690e-002 5.6083040e-002 -8.9993300e-003 +v 3.2600380e-002 5.3707520e-002 -1.1006150e-002 +v 2.9739960e-002 5.2538430e-002 -1.6224950e-002 +v 5.9238530e-002 5.6362780e-002 9.4530800e-003 +v 5.7421750e-002 5.6012210e-002 4.0245600e-003 +v 2.9062990e-002 5.5210580e-002 -1.8042060e-002 +v -1.7224410e-002 9.5214090e-002 -3.2085300e-002 +v -8.5911380e-002 1.0968787e-001 7.6582400e-003 +v 6.0594930e-002 6.1677210e-002 1.5591560e-002 +v 5.9531640e-002 6.0504600e-002 5.8397000e-003 +v 5.7306470e-002 5.9944620e-002 1.8886400e-003 +v 3.8829380e-002 5.9839830e-002 -6.4252500e-003 +v 3.0662770e-002 5.7300390e-002 -1.6518370e-002 +v -2.7762070e-002 1.2068537e-001 -9.0152900e-003 +v -8.8194590e-002 1.0314633e-001 1.7509020e-002 +v 6.0778800e-002 6.1646560e-002 1.0463990e-002 +v 3.5915080e-002 5.9916380e-002 -1.1966510e-002 +v 2.4251860e-002 5.6457470e-002 -2.4254800e-002 +v -6.1954390e-002 1.6865320e-001 -5.2621160e-002 +v -9.0557930e-002 1.1275994e-001 1.6141030e-002 +v -8.8469220e-002 1.1124294e-001 1.2679160e-002 +v 5.9558010e-002 6.3099260e-002 5.9471000e-003 +v 3.0940440e-002 6.0518080e-002 -1.8132720e-002 +v -9.3575750e-002 1.2474629e-001 2.6213300e-002 +v -9.3189820e-002 1.2019919e-001 3.7913720e-002 +v -9.2296100e-003 1.7314463e-001 -2.4197660e-002 +v -8.1739460e-002 7.6861340e-002 2.3313610e-002 +v -3.6992750e-002 1.5063932e-001 -2.0372300e-003 +v 6.0093570e-002 6.5693450e-002 1.8533320e-002 +v 5.9837240e-002 6.6423180e-002 8.5139400e-003 +v 4.0706180e-002 6.4475310e-002 -5.5920300e-003 +v 3.4745940e-002 6.3261340e-002 -1.4646740e-002 +v -6.1879660e-002 1.6000450e-001 -2.5806250e-002 +v -7.6537810e-002 1.5344875e-001 -1.2898750e-002 +v 3.8111070e-002 6.4811810e-002 -1.1142000e-002 +v 3.1909340e-002 6.4657050e-002 -1.8473410e-002 +v -8.3159350e-002 1.4674277e-001 3.0757900e-003 +v -8.7055900e-002 1.0562761e-001 9.7651100e-003 +v -7.1448330e-002 1.8105301e-001 -5.5478550e-002 +v -8.5632110e-002 1.2461094e-001 -2.7335800e-003 +v 6.0728970e-002 6.5806600e-002 1.3974830e-002 +v 3.9909650e-002 6.8171740e-002 -9.5698200e-003 +v 3.4981790e-002 6.7740790e-002 -1.5683210e-002 +v -9.1822030e-002 1.2747346e-001 3.6458650e-002 +v -6.2425420e-002 1.6366637e-001 -4.9667290e-002 +v -7.1168950e-002 1.4740156e-001 -2.7590940e-002 +v -5.0364760e-002 1.3715763e-001 1.9526100e-003 +v -5.0492650e-002 1.4159899e-001 1.6291740e-002 +v 5.9886670e-002 6.8513050e-002 1.6171610e-002 +v -6.1406990e-002 1.7268822e-001 -5.8265750e-002 +v 2.4990740e-002 6.5897320e-002 -2.3568270e-002 +v -7.4852750e-002 1.4993112e-001 -2.7752940e-002 +v -6.2225690e-002 6.0265200e-002 2.0449290e-002 +v -6.2001940e-002 3.6435020e-002 4.3918940e-002 +v 5.8374570e-002 7.1186410e-002 1.3072740e-002 +v -3.6125040e-002 1.2286688e-001 -8.2927900e-003 +v 2.9216510e-002 6.7850250e-002 -2.0418570e-002 +v -4.1681700e-002 1.2575112e-001 -7.0193300e-003 +v -7.4226550e-002 1.6437012e-001 -3.8240340e-002 +v -9.7845700e-003 1.6928488e-001 -2.4756660e-002 +v -8.9577950e-002 1.2078310e-001 3.5229100e-003 +v -6.2311930e-002 1.6371109e-001 -4.0623990e-002 +v 4.3514770e-002 9.1519890e-002 -2.6468100e-003 +v -4.8434350e-002 1.3754973e-001 1.3244980e-002 +v -8.9313160e-002 1.3653006e-001 3.0458750e-002 +v -7.4230190e-002 1.5652681e-001 -2.5167090e-002 +v 3.7378600e-002 7.3093410e-002 -1.2635370e-002 +v 2.6321810e-002 7.0240650e-002 -2.3878680e-002 +v -4.8023620e-002 1.4426649e-001 4.2498600e-003 +v -9.2019580e-002 1.1611534e-001 3.5842730e-002 +v -7.1305510e-002 7.3899020e-002 3.5969780e-002 +v -6.2059290e-002 1.5697807e-001 -3.3784580e-002 +v -9.7015300e-003 1.6738863e-001 -1.9360250e-002 +v 4.3342140e-002 7.1676120e-002 -2.2304600e-003 +v 4.1772460e-002 6.9568020e-002 -6.1596000e-003 +v 3.3505410e-002 7.2809860e-002 -1.7034800e-002 +v 2.9665000e-002 7.1506830e-002 -2.1282340e-002 +v -2.9460160e-002 1.5550263e-001 -1.1914700e-003 +v -8.6396440e-002 1.0479356e-001 5.9820600e-003 +v -5.4910700e-002 1.4662313e-001 2.8438970e-002 +v 4.4203810e-002 8.5204260e-002 -2.1170500e-003 +v 4.3264350e-002 7.5810540e-002 -3.8843900e-003 +v 1.3096990e-002 9.1126480e-002 -2.9269770e-002 +v -6.7069210e-002 9.1144610e-002 -1.7425950e-002 +v -9.0821680e-002 1.2276896e-001 6.0998500e-003 +v 4.5620000e-002 7.4684430e-002 2.6073900e-003 +v -9.3039800e-002 1.2026416e-001 1.1216820e-002 +v 4.4635590e-002 9.2794290e-002 1.7832070e-002 +v -1.1243390e-002 1.6457514e-001 -1.8240780e-002 +v 4.5511190e-002 8.6953050e-002 3.8865500e-003 +v 4.6252720e-002 7.7373870e-002 6.9140800e-003 +v 4.0281640e-002 7.2637130e-002 -9.2881000e-003 +v 4.3218200e-002 9.9486740e-002 5.0153300e-003 +v -5.1108270e-002 1.4520219e-001 1.4279480e-002 +v 4.4692980e-002 9.2688550e-002 2.2466700e-003 +v 4.3422540e-002 9.1860370e-002 2.4538450e-002 +v 4.0751360e-002 1.0554729e-001 7.5074100e-003 +v -8.5613030e-002 9.6277110e-002 -6.6514000e-004 +v 4.0721470e-002 7.8475530e-002 -8.2130000e-003 +v 3.5538080e-002 7.6062960e-002 -1.4434750e-002 +v -9.2736510e-002 1.2073095e-001 3.2692730e-002 +v -6.2278520e-002 1.5166598e-001 -1.4672730e-002 +v 4.4960220e-002 8.0942630e-002 6.1119000e-004 +v 3.7814740e-002 7.9698150e-002 -1.3289630e-002 +v 3.3864490e-002 7.8656690e-002 -1.7632490e-002 +v -9.1044280e-002 1.4199862e-001 2.1729630e-002 +v -7.4004450e-002 1.7818523e-001 -5.3916320e-002 +v -6.1768650e-002 1.6067957e-001 -3.4046350e-002 +v -4.9747450e-002 1.4112519e-001 5.2937500e-003 +v 4.1065440e-002 9.0460700e-002 2.9888620e-002 +v -7.2916360e-002 6.5057400e-002 1.8794620e-002 +v -9.0949690e-002 1.3895375e-001 1.7371130e-002 +v 4.2879050e-002 1.0093777e-001 9.4753200e-003 +v -7.2455480e-002 1.7610676e-001 -5.3535420e-002 +v -7.5862940e-002 1.5071299e-001 -9.0209000e-003 +v -8.5269820e-002 1.0267793e-001 1.3935600e-003 +v -7.7025570e-002 1.1396763e-001 -4.6168100e-003 +v 4.6280880e-002 7.8702020e-002 1.4786330e-002 +v 4.2106910e-002 8.1533160e-002 -6.6690900e-003 +v 3.6523880e-002 8.1991750e-002 -1.6229590e-002 +v -3.7420220e-002 4.5428500e-002 -2.4226790e-002 +v -8.5148910e-002 1.3965520e-001 2.4808500e-003 +v -6.3313300e-002 1.6503258e-001 -3.2895120e-002 +v -6.1591410e-002 1.5681572e-001 -2.5945630e-002 +v 4.5918540e-002 8.7036220e-002 8.4236300e-003 +v 4.4631140e-002 8.4178380e-002 8.2665000e-004 +v -4.4842870e-002 1.4629393e-001 1.7114800e-003 +v -6.4124180e-002 1.7953625e-001 -5.8730420e-002 +v -6.7070300e-002 1.8072682e-001 -5.6618620e-002 +v -6.4793760e-002 1.7885275e-001 -5.5883250e-002 +v -6.4371030e-002 1.7296209e-001 -4.9225660e-002 +v -7.0381530e-002 1.8071180e-001 -5.3172590e-002 +v -7.5269270e-002 1.5232949e-001 3.4374060e-002 +v -1.6273090e-002 1.2844514e-001 1.6683610e-002 +v -6.2116150e-002 1.5600787e-001 1.8034420e-002 +v -5.6010790e-002 1.5381662e-001 2.5369280e-002 +v -3.7277920e-002 1.7289068e-001 -8.6627000e-004 +v -7.4158700e-002 1.7987275e-001 -5.0794750e-002 +v -7.9039960e-002 1.5537445e-001 1.5141810e-002 +v -7.2505530e-002 1.5459529e-001 2.9588830e-002 +v -6.7738180e-002 1.7728865e-001 -5.0375960e-002 +v -7.5346900e-003 1.0021302e-001 4.7488700e-002 +v -5.9575620e-002 1.5472401e-001 2.6373250e-002 +v -7.7382710e-002 1.5346600e-001 3.0894990e-002 +v -8.1496670e-002 1.5473104e-001 1.9697340e-002 +v -7.2223320e-002 1.5896734e-001 -5.4242300e-003 +v -1.3708500e-002 1.8491150e-001 -2.5549550e-002 +v -4.3465340e-002 1.2451145e-001 2.2518890e-002 +v -6.9103650e-002 1.5559479e-001 1.6370800e-003 +v -7.3748080e-002 1.5539253e-001 2.3491700e-003 +v -6.8192410e-002 1.7439828e-001 -4.5365870e-002 +v -6.0052850e-002 1.5280350e-001 3.2887630e-002 +v -2.3459490e-002 1.2615386e-001 1.6613770e-002 +v -7.2777220e-002 1.7854465e-001 -4.8208800e-002 +v -7.6595580e-002 1.7753227e-001 -4.7118080e-002 +v 1.3906410e-002 1.2790838e-001 2.5110240e-002 +v -8.6367510e-002 1.0906537e-001 1.1980640e-002 +v -3.1358850e-002 1.2140977e-001 2.5971090e-002 +v -4.9104590e-002 1.3666879e-001 1.9314030e-002 +v -4.2930640e-002 1.2928436e-001 9.2700700e-003 +v -6.5320350e-002 1.5390322e-001 9.1386000e-004 +v -3.7606490e-002 1.2422605e-001 2.4313530e-002 +v 9.5078400e-003 1.3041865e-001 2.0715020e-002 +v -1.7976800e-003 1.3117283e-001 1.6360660e-002 +v 3.6231700e-003 1.3076791e-001 2.1168600e-002 +v -9.2674700e-002 1.1701945e-001 1.1889520e-002 +v -6.5739720e-002 1.5565338e-001 2.6017600e-002 +v -8.6561940e-002 1.4249188e-001 8.4326800e-003 +v -7.0731530e-002 1.5569959e-001 6.9058200e-003 +v -8.0840700e-003 1.3030537e-001 1.6872280e-002 +v -4.4286250e-002 1.2606625e-001 2.0795220e-002 +v -7.0222260e-002 1.5143521e-001 3.6718910e-002 +v -1.5210690e-002 1.8463639e-001 -2.2057240e-002 +v -1.7270750e-002 1.8699602e-001 -1.9977570e-002 +v -8.3560950e-002 1.5255943e-001 7.6806700e-003 +v -8.8130280e-002 9.7540510e-002 5.6788000e-003 +v -8.8399240e-002 1.3899000e-001 1.0640660e-002 +v -6.7780550e-002 1.5614453e-001 1.4276320e-002 +v -6.5864600e-003 1.2641717e-001 3.0226390e-002 +v -8.8746180e-002 1.3625578e-001 7.1477800e-003 +v -7.7206730e-002 1.5639950e-001 -1.8972540e-002 +v -9.3176480e-002 1.1821016e-001 2.3362360e-002 +v -2.3506850e-002 1.2672006e-001 1.0996900e-002 +v -6.6546650e-002 1.7171115e-001 -4.2127770e-002 +v -6.9136000e-002 1.7247836e-001 -3.9013330e-002 +v 5.7180270e-002 7.1107690e-002 8.0307600e-003 +v -7.5390870e-002 1.7952824e-001 -5.2402050e-002 +v -3.1828840e-002 1.2639115e-001 1.0013410e-002 +v -8.9888800e-003 1.2952269e-001 2.2026810e-002 +v 3.4325880e-002 1.1193312e-001 -2.2406500e-003 +v -8.1414950e-002 9.7100250e-002 -6.8745800e-003 +v -2.3298830e-002 1.8324307e-001 -1.7923000e-002 +v -6.1641660e-002 1.5582039e-001 1.1099820e-002 +v -8.8826450e-002 9.0483320e-002 2.1204700e-002 +v 5.8373130e-002 6.8067590e-002 5.7247600e-003 +v -4.3045630e-002 1.2785122e-001 1.6842260e-002 +v 3.0835720e-002 1.1554234e-001 -3.1785500e-003 +v -8.8631270e-002 9.4881200e-002 7.9337600e-003 +v -9.1715140e-002 1.1709957e-001 3.0809400e-002 +v -7.2083780e-002 1.7499844e-001 -4.1930320e-002 +v -6.9540630e-002 1.5308527e-001 3.3865720e-002 +v 6.0078690e-002 6.8129260e-002 1.1454500e-002 +v -4.0081060e-002 1.2628381e-001 1.9607250e-002 +v 3.2819930e-002 1.1655625e-001 4.4458600e-003 +v -7.2823220e-002 1.4510601e-001 -1.5654680e-002 +v -8.5270210e-002 1.0551770e-001 2.3290940e-002 +v -7.6051320e-002 1.1103825e-001 -6.2722100e-003 +v -8.6537730e-002 1.5154801e-001 2.5875370e-002 +v 5.5888480e-002 7.2579250e-002 1.0669650e-002 +v -5.4642360e-002 1.5522963e-001 1.2612400e-002 +v 3.6729960e-002 1.1116756e-001 3.8670600e-003 +v 3.1501870e-002 1.1725172e-001 1.6855100e-003 +v -7.8751550e-002 9.5240290e-002 -1.0600670e-002 +v -8.9408160e-002 1.4352815e-001 3.0924750e-002 +v -2.0891130e-002 1.8595338e-001 -1.5037360e-002 +v -7.0863560e-002 1.6136525e-001 -9.7324600e-003 +v -7.0919760e-002 1.7136688e-001 -3.2763750e-002 +v -3.0771290e-002 1.2564075e-001 1.6594770e-002 +v -5.4454180e-002 1.5297699e-001 2.2505190e-002 +v -1.5539500e-003 1.2754717e-001 2.9232870e-002 +v 2.9130550e-002 1.2027445e-001 6.1117500e-003 +v 2.5725940e-002 1.2122705e-001 -3.6150000e-005 +v -8.9318970e-002 9.9546980e-002 1.3418110e-002 +v -7.5429500e-002 1.7095605e-001 -3.2879890e-002 +v -2.8596020e-002 1.1901156e-001 2.9888170e-002 +v 2.1069780e-002 1.2497756e-001 1.0998100e-003 +v -9.2240760e-002 1.1816838e-001 4.1201730e-002 +v 2.4094600e-003 1.0016785e-001 4.6938070e-002 +v -5.6627620e-002 1.5270606e-001 2.9629030e-002 +v -5.7264800e-002 1.5506250e-001 1.9322430e-002 +v -3.6452070e-002 1.2199869e-001 2.7670650e-002 +v -7.4108160e-002 1.7355729e-001 -3.7986840e-002 +v 5.1537130e-002 7.3496690e-002 1.2698700e-002 +v -6.6096040e-002 1.5532529e-001 7.1561800e-003 +v 3.6102000e-002 1.1266103e-001 1.0491780e-002 +v 1.6715210e-002 1.2689851e-001 2.2331000e-004 +v -8.0767920e-002 1.4301400e-001 -1.5312800e-003 +v -9.1757600e-002 1.4334588e-001 1.7790710e-002 +v -8.6824940e-002 1.5280775e-001 1.5521450e-002 +v -6.5808100e-002 1.6764344e-001 -3.0558670e-002 +v -7.8217340e-002 1.6873975e-001 -3.3564250e-002 +v -7.2567060e-002 1.4753230e-001 4.1714090e-002 +v 5.8439960e-002 7.0200810e-002 1.7779620e-002 +v 5.6847560e-002 7.2017160e-002 1.7139380e-002 +v 5.4919390e-002 7.3161610e-002 1.5223590e-002 +v 4.7446900e-002 7.3691410e-002 1.2430020e-002 +v 1.2319360e-002 1.2903768e-001 1.3336200e-003 +v -7.9790640e-002 1.0351662e-001 -6.6275400e-003 +v -7.6655210e-002 1.5509766e-001 7.9686300e-003 +v 2.1747320e-002 1.2118456e-001 3.0878810e-002 +v -7.5260490e-002 1.4938613e-001 3.9175980e-002 +v -2.5919610e-002 1.8272826e-001 -1.3541090e-002 +v -6.7983790e-002 1.6974781e-001 -3.1627490e-002 +v 1.6831110e-002 1.2487146e-001 2.8425580e-002 +v 5.4016490e-002 7.2883850e-002 1.8678010e-002 +v 5.0522750e-002 7.3397910e-002 1.6166890e-002 +v -5.9582440e-002 1.5623338e-001 7.9209900e-003 +v 2.5343500e-002 1.2374750e-001 9.9818800e-003 +v 1.9262750e-002 1.2689390e-001 5.5552100e-003 +v -9.0758520e-002 1.4223375e-001 2.6008130e-002 +v -4.6548490e-002 1.3320769e-001 1.6889630e-002 +v -2.4106950e-002 1.8380887e-001 -1.1544760e-002 +v 8.6784400e-003 1.2894574e-001 2.6156880e-002 +v 2.4919200e-003 1.2983563e-001 2.4847110e-002 +v 5.7345150e-002 6.9482720e-002 2.1153510e-002 +v -8.5329840e-002 1.5339912e-001 2.0378290e-002 +v 3.2877320e-002 1.1691463e-001 9.2957500e-003 +v 2.4246630e-002 1.2377758e-001 4.8764500e-003 +v -4.7765650e-002 1.3301969e-001 2.2874020e-002 +v -6.3541830e-002 1.6332115e-001 -2.5912990e-002 +v -6.6605200e-002 1.6477375e-001 -2.0670760e-002 +v -6.8504220e-002 1.6732018e-001 -2.3959570e-002 +v -7.2759160e-002 1.6965906e-001 -2.7013420e-002 +v 4.8206850e-002 7.2698580e-002 1.6994630e-002 +v -2.7383180e-002 1.2324257e-001 2.1658860e-002 +v -4.5077500e-002 1.3124443e-001 1.1145770e-002 +v 2.9253150e-002 1.2057701e-001 1.2299330e-002 +v 1.3677610e-002 1.2967262e-001 6.9327400e-003 +v 8.4210900e-003 1.3090986e-001 6.2754400e-003 +v 9.6836000e-004 1.3064303e-001 2.5865900e-003 +v 3.0802000e-003 9.8307360e-002 5.0535640e-002 +v -5.2420170e-002 1.5310101e-001 1.2927370e-002 +v -7.0359720e-002 1.6906988e-001 -2.6144260e-002 +v 5.4359390e-002 7.1467260e-002 2.1381250e-002 +v 4.5161440e-002 7.1030380e-002 2.2530690e-002 +v 1.9320440e-002 1.2738348e-001 1.1296310e-002 +v -9.3281210e-002 1.2691094e-001 1.3505010e-002 +v -8.7405060e-002 1.0593990e-001 1.3645920e-002 +v -2.2851640e-002 9.0635040e-002 5.2280460e-002 +v -6.2099370e-002 1.5406697e-001 3.0837360e-002 +v -4.5851560e-002 1.2072981e-001 2.7665040e-002 +v 5.0781670e-002 7.2155170e-002 2.0680180e-002 +v -8.9607270e-002 1.3971105e-001 2.9308560e-002 +v -5.3323050e-002 1.5273520e-001 1.6213860e-002 +v -1.5227080e-002 1.2784878e-001 2.1545200e-002 +v 3.3663540e-002 1.1574212e-001 1.7181290e-002 +v 2.4000260e-002 1.2468761e-001 1.5517930e-002 +v -8.4166840e-002 9.7756820e-002 -3.2761900e-003 +v -3.6223590e-002 1.2777519e-001 9.8501500e-003 +v -3.9189580e-002 1.2828193e-001 5.0346300e-003 +v -3.3674050e-002 1.7774449e-001 -8.1799500e-003 +v -7.4488620e-002 1.5649443e-001 -2.5954600e-003 +v -4.6755620e-002 1.3284294e-001 8.1212800e-003 +v -8.4970410e-002 1.5322309e-001 1.2654460e-002 +v -1.0866210e-002 1.2691699e-001 2.7575440e-002 +v -3.1074000e-003 1.3072898e-001 5.6428500e-003 +v -8.8760540e-002 9.7037440e-002 2.1079040e-002 +v -6.4811320e-002 3.4530640e-002 1.5508440e-002 +v -6.4300260e-002 3.5086450e-002 2.4272050e-002 +v -6.6727020e-002 3.5895770e-002 3.3849430e-002 +v 1.9838510e-002 9.6518890e-002 -2.2785880e-002 +v -3.8670510e-002 1.6070199e-001 -1.2357760e-002 +v -7.6890090e-002 1.3041906e-001 -6.9570100e-003 +v -7.2539730e-002 3.5399270e-002 7.0298800e-003 +v -6.9209050e-002 3.5454810e-002 1.2042140e-002 +v -6.4160810e-002 3.5900770e-002 1.7687570e-002 +v -6.6804150e-002 3.7377740e-002 3.3296290e-002 +v -6.2928350e-002 3.9061660e-002 4.2707680e-002 +v -7.1752230e-002 3.6789350e-002 8.6966700e-003 +v -6.5171380e-002 3.7289500e-002 2.5953770e-002 +v -6.6392030e-002 3.7712350e-002 2.9621950e-002 +v -6.4558720e-002 3.9639900e-002 3.9411530e-002 +v -6.0145790e-002 4.1202050e-002 4.4293830e-002 +v -6.0318430e-002 3.8442990e-002 4.5245950e-002 +v -3.6756310e-002 8.8663360e-002 -2.3868800e-002 +v -3.9494750e-002 3.7551570e-002 4.2870900e-002 +v -7.2016030e-002 3.7572700e-002 3.9789400e-003 +v -7.1693630e-002 3.9461000e-002 6.0145000e-003 +v -7.1165950e-002 3.9366310e-002 8.1142100e-003 +v -6.9000300e-002 3.8467710e-002 1.0768900e-002 +v -6.7253420e-002 3.8142160e-002 1.3533960e-002 +v -6.1125670e-002 3.7790050e-002 1.9710900e-002 +v -3.9179680e-002 4.2406740e-002 4.1476070e-002 +v -3.5145960e-002 3.8585920e-002 4.7732690e-002 +v -2.8950940e-002 3.9285940e-002 5.3309090e-002 +v -1.8223900e-002 9.7494570e-002 4.6847940e-002 +v -6.6916260e-002 1.2278907e-001 -8.9077400e-003 +v -6.3754640e-002 3.8250120e-002 1.6593500e-002 +v -6.4415760e-002 4.1283840e-002 2.8243480e-002 +v -8.5856340e-002 9.7025390e-002 2.7414960e-002 +v -3.7501130e-002 4.0221900e-002 4.4296550e-002 +v -3.4333970e-002 4.0923630e-002 4.8425810e-002 +v -3.1172890e-002 4.0294330e-002 5.1312460e-002 +v -6.9997320e-002 4.2073080e-002 6.6897800e-003 +v -8.0379330e-002 9.7800660e-002 3.3645750e-002 +v -2.6273160e-002 7.7631160e-002 4.8356180e-002 +v -3.7501450e-002 4.2736690e-002 4.2988400e-002 +v -2.6177500e-002 4.2498930e-002 5.3315220e-002 +v -6.9637250e-002 4.1881270e-002 3.1825800e-003 +v -6.7156510e-002 4.1972860e-002 1.0240940e-002 +v -8.7405510e-002 1.0205209e-001 2.2020360e-002 +v -2.3944380e-002 7.8800140e-002 5.3534730e-002 +v -6.0902360e-002 4.3429500e-002 4.2678530e-002 +v -3.1217880e-002 4.3847510e-002 4.9780920e-002 +v -7.5729440e-002 1.0354026e-001 3.6070970e-002 +v -6.2425320e-002 4.1885720e-002 1.4646770e-002 +v -6.1051660e-002 4.4392230e-002 1.2421940e-002 +v 2.5855060e-002 8.9610660e-002 -2.2701840e-002 +v -7.7644960e-002 8.2214940e-002 3.5797660e-002 +v -6.0381270e-002 4.5921420e-002 4.0088740e-002 +v -2.4982010e-002 8.1777650e-002 5.3421060e-002 +v -3.4453850e-002 4.4563960e-002 4.5422990e-002 +v -2.9842910e-002 4.6782280e-002 4.7746920e-002 +v -1.5119580e-002 9.9930020e-002 4.4500270e-002 +v -6.7306470e-002 4.4176830e-002 7.5958300e-003 +v -5.7852990e-002 4.6444500e-002 1.1062610e-002 +v -5.1815260e-002 1.6392582e-001 1.7488800e-003 +v -5.5174130e-002 4.8383880e-002 3.8517780e-002 +v -7.8849150e-002 1.1867375e-001 5.0622870e-002 +v -2.7229070e-002 8.7991480e-002 4.7909730e-002 +v -7.5536880e-002 1.5977062e-001 -1.0438650e-002 +v -3.6151280e-002 4.6505140e-002 4.0740900e-002 +v -2.5439220e-002 9.0677870e-002 4.8852330e-002 +v -8.0050370e-002 1.1670406e-001 4.8762460e-002 +v -5.2513640e-002 4.7577880e-002 1.4858440e-002 +v -3.2043560e-002 5.0461830e-002 3.9341520e-002 +v -3.1487770e-002 4.6930210e-002 4.5253210e-002 +v -2.0321500e-002 9.3999570e-002 5.1588540e-002 +v -7.2145040e-002 9.1556450e-002 4.1494780e-002 +v -5.3644200e-002 4.9358170e-002 1.2201850e-002 +v -8.2403890e-002 1.2186563e-001 4.9365030e-002 +v -4.9754420e-002 4.9738300e-002 3.7037110e-002 +v -3.2332060e-002 4.8672840e-002 4.2523960e-002 +v -2.3122950e-002 9.4515900e-002 4.7358870e-002 +v -8.6347140e-002 9.1722090e-002 2.6811080e-002 +v -5.7713110e-002 4.8717820e-002 7.2765100e-003 +v -8.6970360e-002 8.8912090e-002 2.4879860e-002 +v -9.2237750e-002 1.2488519e-001 4.0786530e-002 +v -1.5862800e-002 9.7021620e-002 5.0139360e-002 +v -2.7720040e-002 5.0502090e-002 4.3340720e-002 +v -8.5918770e-002 1.4263412e-001 3.9849810e-002 +v -7.5097360e-002 9.0073560e-002 3.9581000e-002 +v -8.9430840e-002 1.4730552e-001 2.7694960e-002 +v -5.3288350e-002 5.1925760e-002 1.1730350e-002 +v -5.0168720e-002 5.3462260e-002 1.6255440e-002 +v -8.5986050e-002 1.4670902e-001 3.4827030e-002 +v -6.9937250e-002 8.6076860e-002 4.2175690e-002 +v -5.0399320e-002 5.1831330e-002 3.4037400e-002 +v -8.3298980e-002 1.4960772e-001 3.3740890e-002 +v -2.9174820e-002 5.2264530e-002 3.7637320e-002 +v -8.8763730e-002 1.1944938e-001 4.6560090e-002 +v -7.7693460e-002 1.7367969e-001 -4.1478670e-002 +v -8.3418140e-002 9.4127440e-002 3.0898450e-002 +v -5.6067510e-002 5.3470630e-002 7.3718200e-003 +v -7.8935630e-002 1.4817228e-001 3.9463070e-002 +v -6.7902770e-002 8.7817230e-002 4.3526990e-002 +v -4.4111240e-002 9.2883990e-002 -2.2373210e-002 +v -8.6605100e-002 1.3226807e-001 4.6783020e-002 +v -9.2654280e-002 1.2084025e-001 4.1629650e-002 +v -5.0887310e-002 5.2727900e-002 1.4455790e-002 +v -4.9763410e-002 5.6241200e-002 3.3624250e-002 +v -8.9771330e-002 1.2904861e-001 4.3022990e-002 +v -2.8054240e-002 5.4551030e-002 3.6786850e-002 +v -2.5867080e-002 5.6689210e-002 3.9182240e-002 +v -8.3702200e-002 1.2226381e-001 -3.7301400e-003 +v -8.1455470e-002 1.3012213e-001 5.2117660e-002 +v -5.1458550e-002 5.5878150e-002 1.5900350e-002 +v -7.8597700e-002 1.7441574e-001 -4.6607580e-002 +v -5.2909820e-002 5.7043070e-002 2.0988410e-002 +v -5.2978500e-002 5.9553770e-002 2.6211920e-002 +v -5.2130640e-002 5.6302970e-002 2.6672460e-002 +v -4.7714500e-002 6.1944520e-002 3.6705820e-002 +v -8.3539790e-002 8.1169560e-002 2.7014070e-002 +v -1.8340000e-002 5.7489970e-002 4.9763020e-002 +v -8.0069810e-002 9.0586130e-002 3.4593070e-002 +v -8.3812250e-002 8.6337700e-002 2.9223270e-002 +v -5.5436650e-002 5.9420250e-002 2.3018970e-002 +v -8.2227680e-002 1.4513771e-001 4.0600080e-002 +v -2.4187580e-002 7.2269150e-002 4.7681090e-002 +v -2.5353150e-002 6.2567200e-002 4.0642170e-002 +v -9.1132110e-002 1.2282100e-001 4.4115160e-002 +v -4.6076290e-002 1.6819719e-001 7.3744000e-004 +v -8.7829280e-002 1.4351461e-001 3.5707670e-002 +v -8.6990640e-002 1.3812326e-001 4.2316550e-002 +v -1.5715900e-002 6.0822970e-002 5.2365440e-002 +v -8.3803580e-002 1.2561100e-001 5.0440490e-002 +v -6.2786680e-002 1.1274190e-001 -1.3605440e-002 +v -8.1033840e-002 8.4698180e-002 3.3106400e-002 +v -8.8563540e-002 1.1624535e-001 4.5392840e-002 +v -2.0268380e-002 6.2266810e-002 4.8212120e-002 +v -1.2619630e-002 6.1635030e-002 5.4424080e-002 +v -7.0491190e-002 8.1818160e-002 4.0609890e-002 +v -8.3882520e-002 1.3331465e-001 4.9113540e-002 +v -5.6560350e-002 4.8355540e-002 3.6607050e-002 +v 9.9444900e-003 1.0919723e-001 -1.9472810e-002 +v -5.5928250e-002 3.5917310e-002 4.6376100e-002 +v -7.6003260e-002 1.6361344e-001 -1.8021110e-002 +v -8.3798850e-002 1.0290691e-001 2.8038330e-002 +v -8.8252110e-002 1.2692730e-001 4.6141300e-002 +v -7.9126720e-002 1.0619883e-001 3.2050700e-002 +v -8.8206230e-002 9.4485700e-002 2.3744010e-002 +v -8.9110330e-002 1.3851394e-001 3.7658780e-002 +v -1.9321360e-002 9.2123890e-002 5.3820650e-002 +v -5.8265630e-002 9.0926390e-002 -2.0948690e-002 +v -2.7046310e-002 6.7014450e-002 3.9672140e-002 +v -2.1416300e-002 1.7977662e-001 -2.1732520e-002 +v -7.8240000e-003 1.0924112e-001 -2.2185670e-002 +v -2.3988340e-002 8.5995590e-002 5.3716430e-002 +v -6.0483580e-002 1.5567975e-001 4.3343800e-003 +v -8.6389150e-002 1.2168475e-001 4.8412440e-002 +v -7.4084360e-002 1.4987744e-001 -3.2610050e-002 +v -2.0580600e-002 7.9572500e-002 5.6013880e-002 +v -8.3837500e-002 1.3927865e-001 4.4893850e-002 +v -2.2933960e-002 3.5632910e-002 5.2865490e-002 +v -8.6153620e-002 1.2735612e-001 4.8563960e-002 +v -6.5728590e-002 1.0709818e-001 -1.4317670e-002 +v -2.1481090e-002 7.4194460e-002 5.2857680e-002 +v -7.6423900e-002 1.5736285e-001 -9.0354600e-003 +v -7.7216010e-002 8.5594880e-002 3.7420770e-002 +v -8.4150830e-002 1.2955013e-001 5.0483700e-002 +v -8.1221440e-002 8.1003250e-002 3.1255840e-002 +v -8.1704000e-002 1.0167226e-001 3.0939660e-002 +v -8.6252730e-002 1.0106846e-001 2.5413770e-002 +v -8.0944970e-002 1.3903572e-001 4.7359080e-002 +v -7.8908350e-002 9.4830900e-002 3.5435500e-002 +v -7.3440160e-002 9.5412600e-002 4.0210650e-002 +v -5.2675780e-002 8.8220740e-002 -2.1886300e-002 +v -7.6440670e-002 7.7511060e-002 3.3748300e-002 +v -2.1791140e-002 1.0658035e-001 -2.2327000e-002 +v -8.8360940e-002 1.4996706e-001 2.6044170e-002 +v -2.4078870e-002 6.7906700e-002 4.5178370e-002 +v -2.0018090e-002 6.7569300e-002 5.1565340e-002 +v -8.3577750e-002 1.2052625e-001 4.9177500e-002 +v -1.4655950e-002 1.7456543e-001 -2.5972690e-002 +v -2.7395940e-002 8.4108300e-002 4.8745680e-002 +v -4.1933580e-002 8.8463400e-002 -2.2126350e-002 +v -3.1693900e-002 1.0261265e-001 -2.2352310e-002 +v -2.7890200e-002 1.0440703e-001 -2.2830920e-002 +v -7.3790400e-002 1.2016662e-001 -7.8851200e-003 +v -4.6124160e-002 1.0506369e-001 -2.0457580e-002 +v -2.7412650e-002 7.3269450e-002 4.2641380e-002 +v -4.5532880e-002 3.4736480e-002 -2.1363200e-002 +v -4.4993030e-002 3.9017010e-002 -2.1097830e-002 +v -4.6462610e-002 3.6800270e-002 -1.7778710e-002 +v -8.8366460e-002 1.1361863e-001 5.8227800e-003 +v 5.1746240e-002 7.2897250e-002 9.0647400e-003 +v -7.0385250e-002 3.7450300e-002 -9.3190000e-004 +v -6.0923170e-002 3.8621820e-002 2.2468850e-002 +v -7.7696720e-002 1.7027889e-001 -4.3117910e-002 +v -4.3793210e-002 1.6955506e-001 -7.3026400e-003 +v -7.7587180e-002 1.7717875e-001 -5.0221090e-002 +v -4.0541880e-002 3.8886010e-002 -2.7364950e-002 +v -4.4215850e-002 3.6131460e-002 -2.4252210e-002 +v -6.6634880e-002 4.0430310e-002 -5.0180700e-003 +v -6.9242120e-002 4.1474050e-002 1.9289000e-004 +v -7.5640690e-002 1.5930400e-001 -2.6908460e-002 +v -6.3087030e-002 3.9614170e-002 2.5181560e-002 +v -7.2303020e-002 1.5186699e-001 -4.1544310e-002 +v -4.1051490e-002 4.1528620e-002 -2.4061000e-002 +v -4.6990580e-002 3.8892380e-002 -1.4016920e-002 +v -8.9559690e-002 1.2851666e-001 4.5457500e-003 +v -7.6987340e-002 1.5369375e-001 -2.2970800e-003 +v -7.0121670e-002 1.6882633e-001 -5.1173650e-002 +v -6.4792610e-002 4.1724530e-002 3.1616900e-002 +v -4.2148060e-002 1.2409627e-001 -9.5602500e-003 +v -4.8069700e-002 1.2493027e-001 -8.4076400e-003 +v -4.2150480e-002 4.3343970e-002 -2.1508710e-002 +v -6.7315160e-002 4.4034000e-002 1.5741800e-003 +v -7.3386640e-002 1.5463418e-001 -2.9943830e-002 +v -5.5352770e-002 4.2936210e-002 1.9135490e-002 +v -6.0067770e-002 4.1419500e-002 2.2953280e-002 +v -6.5488460e-002 4.0937780e-002 3.5315470e-002 +v -8.0066400e-002 1.5039650e-001 6.0518000e-004 +v -4.4031300e-002 4.1949070e-002 -1.7993960e-002 +v -4.5186510e-002 4.2453420e-002 -1.4193620e-002 +v -8.3109430e-002 1.0265445e-001 -3.2933400e-003 +v -6.5472800e-002 4.5627570e-002 4.5575400e-003 +v -7.5427730e-002 1.5201213e-001 -1.4393690e-002 +v -5.4473420e-002 4.5937510e-002 2.3612600e-002 +v -6.2464100e-002 4.3722000e-002 2.8493310e-002 +v -6.2832600e-002 4.5182750e-002 3.4622890e-002 +v -6.3538130e-002 4.3524020e-002 3.7974010e-002 +v -6.0255260e-002 4.4749620e-002 -4.1316200e-003 +v -6.3242050e-002 4.5549700e-002 4.8428000e-004 +v -6.2249430e-002 4.6540050e-002 7.1903500e-003 +v -9.1003650e-002 1.4885725e-001 2.1507030e-002 +v -5.7094130e-002 4.5996540e-002 2.6865280e-002 +v -5.7276490e-002 4.7299580e-002 2.9889950e-002 +v -3.9519900e-002 1.7385855e-001 -7.5752600e-003 +v -8.9641110e-002 1.3841920e-001 3.4141800e-002 +v -9.2601430e-002 1.3018652e-001 2.5183580e-002 +v -9.2280860e-002 1.2762053e-001 2.9751670e-002 +v -3.3957310e-002 4.1025060e-002 -2.9660250e-002 +v -9.0199540e-002 1.1657506e-001 5.6754900e-003 +v -5.8515890e-002 4.7731310e-002 2.1246000e-004 +v -7.1723560e-002 1.4617438e-001 -2.1567820e-002 +v -5.2389820e-002 4.5449130e-002 1.7686300e-002 +v -5.9414350e-002 4.7277990e-002 3.4172420e-002 +v -5.7520620e-002 1.5877600e-001 4.1621200e-003 +v -8.0959140e-002 1.0926674e-001 -2.0189900e-003 +v -5.1904000e-002 4.6100060e-002 1.9421290e-002 +v -5.1830050e-002 4.8568730e-002 2.1647030e-002 +v -7.7650400e-002 1.5658012e-001 -1.6599150e-002 +v -3.7416450e-002 4.7682130e-002 -1.7147280e-002 +v -7.8876110e-002 1.5347012e-001 3.9875800e-003 +v -5.7635420e-002 5.0425540e-002 4.6108400e-003 +v -5.2625440e-002 5.0434620e-002 2.9046740e-002 +v -5.2998720e-002 4.9169020e-002 3.3967600e-002 +v -7.3502600e-002 1.6871934e-001 -4.4791800e-002 +v -5.4420720e-002 4.7836520e-002 -5.9186900e-003 +v -5.2312740e-002 5.1085350e-002 2.4485690e-002 +v -7.9129930e-002 1.6736568e-001 -3.5506230e-002 +v 9.4115700e-003 1.2350285e-001 -9.8291000e-003 +v -3.2715700e-002 1.0896631e-001 -1.8941410e-002 +v -3.1133380e-002 4.9607260e-002 -1.9406940e-002 +v 4.5997330e-002 6.9814450e-002 3.0143300e-003 +v 3.3525460e-002 1.0966209e-001 -6.9894800e-003 +v -5.5047160e-002 5.2767560e-002 -3.9461300e-003 +v -5.6897890e-002 4.9655570e-002 -1.5319000e-003 +v -5.0290500e-002 4.9098930e-002 1.7164780e-002 +v -5.0595170e-002 4.9923270e-002 1.9174130e-002 +v -5.1887420e-002 5.3324670e-002 2.8705560e-002 +v -6.7684480e-002 1.6533627e-001 -5.5466400e-002 +v -3.0271440e-002 5.2106080e-002 -1.7676140e-002 +v -9.1087300e-003 1.1141669e-001 -2.0543230e-002 +v -5.7069360e-002 5.4424380e-002 2.3395500e-003 +v -3.2748380e-002 1.7759875e-001 -1.1627470e-002 +v -2.9009580e-002 5.1265290e-002 -2.2175780e-002 +v -3.1383130e-002 5.1791310e-002 -1.3886800e-002 +v -5.5673960e-002 5.6983850e-002 -3.3510400e-003 +v -5.0916050e-002 5.3813610e-002 1.9753140e-002 +v -8.8875380e-002 1.5169443e-001 2.0086580e-002 +v -7.7153050e-002 1.7378676e-001 -4.7867620e-002 +v -7.8577770e-002 1.6420639e-001 -3.1825860e-002 +v -2.7545910e-002 5.4021570e-002 -2.5147390e-002 +v -5.4463660e-002 5.5357450e-002 1.0326840e-002 +v -8.7041410e-002 1.3058932e-001 9.1161000e-004 +v -9.0009340e-002 1.3278082e-001 5.9220600e-003 +v -9.2232620e-002 1.3195400e-001 1.5430650e-002 +v -4.8639980e-002 1.6472475e-001 -5.0591500e-003 +v -5.4066480e-002 5.9959350e-002 -7.5992200e-003 +v -5.7434090e-002 5.7683500e-002 8.7259700e-003 +v -8.6794730e-002 1.3850688e-001 4.5575900e-003 +v -9.2989530e-002 1.3092307e-001 1.9919290e-002 +v -9.1282030e-002 1.3311897e-001 2.4688630e-002 +v 2.1815020e-002 1.1770533e-001 -1.0015300e-002 +v -2.9647120e-002 5.8104260e-002 -2.1311320e-002 +v -3.1289530e-002 5.5208570e-002 -1.4387840e-002 +v -5.9002160e-002 5.9234620e-002 2.6140800e-003 +v -9.0241700e-002 1.3575994e-001 1.4149160e-002 +v -6.1569420e-002 1.7084875e-001 -6.1679170e-002 +v -6.6070180e-002 1.6557822e-001 -5.8644080e-002 +v -2.4539930e-002 1.8005865e-001 -1.8726950e-002 +v -1.6131750e-002 1.8298848e-001 -2.6037190e-002 +v -3.0809390e-002 5.6998040e-002 -1.7835020e-002 +v 1.0464280e-002 9.6180450e-002 -2.5898970e-002 +v -5.7491630e-002 5.9530160e-002 -1.0786100e-003 +v -8.9146460e-002 1.3650500e-001 2.5952780e-002 +v 4.3714500e-003 1.0391901e-001 -2.1515100e-002 +v -9.0377040e-002 1.3252490e-001 3.1082650e-002 +v -9.0795450e-002 1.3855232e-001 2.0562560e-002 +v -9.4237710e-002 1.2615419e-001 2.2201450e-002 +v -9.0336910e-002 1.3119830e-001 3.8138790e-002 +v -4.5082610e-002 1.2218447e-001 -1.1569430e-002 +v 1.1348010e-002 9.8243750e-002 -2.3024250e-002 +v -3.9227920e-002 9.9184630e-002 -2.1912720e-002 +v -6.5509530e-002 1.5857325e-001 -5.5600270e-002 +v -7.7409510e-002 1.6260515e-001 -2.0754580e-002 +v -4.8580010e-002 1.6689211e-001 -2.5256100e-003 +v -7.6922910e-002 1.5351394e-001 -9.0785600e-003 +v -6.7750580e-002 1.5734825e-001 -5.3982110e-002 +v 5.2906410e-002 6.5230450e-002 -5.1112000e-004 +v -2.9054820e-002 6.1084120e-002 -2.4918230e-002 +v -3.1066920e-002 6.5058860e-002 -2.2751080e-002 +v 2.4249720e-002 1.0266151e-001 -1.8313830e-002 +v -5.5473660e-002 1.6050213e-001 1.3763500e-003 +v -6.6642850e-002 1.6040875e-001 -5.6842680e-002 +v -7.8200320e-002 1.6073213e-001 -2.3999690e-002 +v -1.8320680e-002 1.1968625e-001 -1.1110660e-002 +v 2.1712970e-002 1.0956342e-001 -1.5081090e-002 +v -6.8382640e-002 1.5980248e-001 -5.4208800e-002 +v -2.5445620e-002 6.0208550e-002 -3.0864700e-002 +v -2.6540330e-002 6.5084000e-002 -3.1664870e-002 +v -2.8425710e-002 6.2199610e-002 -2.7938500e-002 +v -3.2605750e-002 6.1264600e-002 -1.5453010e-002 +v -7.0872290e-002 1.1611638e-001 -7.9563700e-003 +v -6.9780530e-002 1.5938570e-001 -4.9418240e-002 +v -3.0324870e-002 6.7694720e-002 -2.7654950e-002 +v -3.2977370e-002 6.6365180e-002 -1.8385530e-002 +v 1.3533490e-002 1.0255388e-001 -2.1579310e-002 +v 4.4408530e-002 6.9758860e-002 9.4765000e-004 +v -2.1999000e-003 1.1215881e-001 -1.9658660e-002 +v -7.2028500e-002 6.7046610e-002 -7.2256000e-004 +v -7.8699630e-002 1.7313910e-001 -4.2720470e-002 +v -8.3211970e-002 1.5072131e-001 4.2128500e-003 +v -8.7439060e-002 1.3374875e-001 2.3974700e-003 +v 2.6348020e-002 8.4562230e-002 -2.3151710e-002 +v -7.4901490e-002 7.0419350e-002 -2.2854300e-003 +v -5.4576350e-002 9.1562950e-002 -2.2098700e-002 +v -7.3242520e-002 1.5231332e-001 -3.5703520e-002 +v -7.4550960e-002 1.7218738e-001 -4.7551010e-002 +v -2.8680680e-002 6.8283500e-002 -3.0610160e-002 +v 1.7372900e-002 1.0246037e-001 -2.1487700e-002 +v -8.1257430e-002 7.3025200e-002 7.1020400e-003 +v -7.4982300e-002 1.5407794e-001 -1.8974470e-002 +v -9.1556500e-002 1.3196262e-001 1.0638150e-002 +v -8.2448000e-004 9.5165120e-002 -3.2056320e-002 +v -7.7618830e-002 7.3999130e-002 -5.3263500e-003 +v -7.9858790e-002 7.2755040e-002 3.0420200e-003 +v -8.1627470e-002 7.3470610e-002 1.1161690e-002 +v -7.3679290e-002 1.4785987e-001 -2.0236290e-002 +v -9.1309820e-002 1.4848588e-001 1.6270070e-002 +v -9.0850140e-002 1.4625613e-001 1.4809050e-002 +v -6.8543890e-002 1.7513008e-001 -5.7187900e-002 +v -2.7253960e-002 1.0747453e-001 -2.1279680e-002 +v 2.1443580e-002 1.2273826e-001 -2.9316700e-003 +v -7.9061200e-002 7.3724300e-002 -8.4521000e-004 +v -8.2063500e-002 7.5993670e-002 1.7615500e-003 +v -8.3736580e-002 7.6771840e-002 8.9586000e-003 +v -9.0205720e-002 1.4947775e-001 1.3035090e-002 +v 8.4818000e-004 1.1670025e-001 -1.7337090e-002 +v -7.4577550e-002 1.5164041e-001 -2.8647990e-002 +v -2.9087460e-002 7.2924630e-002 -3.3354470e-002 +v -3.1184020e-002 7.3989530e-002 -3.0339870e-002 +v -3.2606620e-002 7.1955620e-002 -2.4866580e-002 +v -8.0575990e-002 7.6607800e-002 -2.9879400e-003 +v -8.9491020e-002 1.4392581e-001 1.2488490e-002 +v -7.7388410e-002 1.4656426e-001 -4.3543000e-003 +v -7.2896160e-002 1.5834962e-001 -3.4109420e-002 +v 7.1346500e-003 1.1468229e-001 -1.8345640e-002 +v -3.4502610e-002 7.6130020e-002 -2.2373150e-002 +v -8.3890740e-002 8.0789530e-002 2.2951400e-003 +v -8.3740480e-002 7.7240270e-002 4.6673300e-003 +v -8.6204620e-002 8.0930750e-002 1.0535420e-002 +v -8.6061500e-002 7.9931100e-002 1.4440780e-002 +v -8.1542760e-002 7.7950660e-002 2.6727280e-002 +v 2.6666170e-002 1.1268609e-001 -1.0509540e-002 +v -7.6041430e-002 1.5663068e-001 -2.1420480e-002 +v -9.0012110e-002 1.5083344e-001 1.5752740e-002 +v -7.1156510e-002 1.6335125e-001 -4.5360530e-002 +v -3.3210960e-002 7.6873190e-002 -2.7708380e-002 +v -7.3263090e-002 7.9983830e-002 -1.3749940e-002 +v -7.9285950e-002 8.0048830e-002 -7.0125500e-003 +v -8.6034510e-002 8.2645720e-002 1.9542680e-002 +v -8.4335410e-002 8.0729950e-002 2.2180460e-002 +v -7.1351460e-002 1.5727092e-001 -4.2183090e-002 +v -7.3548450e-002 1.6120822e-001 -3.5288420e-002 +v 1.6732620e-002 1.0991230e-001 -1.7020040e-002 +v -3.0978770e-002 7.7020860e-002 -3.2816490e-002 +v -6.2359240e-002 1.7544824e-001 -6.1485990e-002 +v -1.7587870e-002 1.1491318e-001 -1.7205040e-002 +v -8.2354050e-002 8.0876320e-002 -2.4038900e-003 +v -7.8578910e-002 1.4050129e-001 -4.6031000e-003 +v -2.8931080e-002 7.9247620e-002 -3.5049800e-002 +v -3.1225710e-002 8.0413100e-002 -3.2182320e-002 +v -3.3258680e-002 7.9621670e-002 -2.7146060e-002 +v -4.4697400e-002 1.1791537e-001 -1.4725860e-002 +v -7.9723740e-002 8.4226660e-002 -8.7608600e-003 +v -8.5042160e-002 8.3817830e-002 -7.7640000e-005 +v -8.6776400e-002 8.4344860e-002 1.2419030e-002 +v -8.6674670e-002 8.2665010e-002 1.5174340e-002 +v -8.5106250e-002 8.5176580e-002 2.5679440e-002 +v -7.6975760e-002 8.2935940e-002 -1.1450630e-002 +v -8.2776390e-002 8.3430890e-002 -4.3687000e-003 +v -8.6180440e-002 8.2572150e-002 6.3639000e-003 +v -9.1160820e-002 1.4144362e-001 1.5673910e-002 +v -7.4638800e-002 1.4398484e-001 -7.1504600e-003 +v -8.3448500e-002 1.3393299e-001 -1.6873200e-003 +v -7.5804700e-002 1.5134475e-001 -1.9881200e-002 +v -7.4924140e-002 1.5273013e-001 -1.9397440e-002 +v -5.2314440e-002 1.2159646e-001 -1.0798060e-002 +v -3.0734050e-002 8.5427560e-002 -3.0506670e-002 +v -3.2590560e-002 8.1942660e-002 -2.9100210e-002 +v -8.6454830e-002 8.6940490e-002 9.1667000e-004 +v -1.2501820e-002 1.0634409e-001 -2.2360190e-002 +v -8.8585880e-002 1.4605869e-001 9.8780000e-003 +v -8.5609750e-002 1.4712513e-001 6.5981100e-003 +v -8.7511210e-002 1.5061504e-001 1.0152460e-002 +v -6.0113540e-002 3.5550440e-002 4.4907580e-002 +v -8.8284200e-002 8.6869110e-002 8.1029200e-003 +v -8.8812560e-002 8.7765490e-002 1.4226540e-002 +v -8.8001070e-002 8.6626430e-002 1.5466680e-002 +v -8.6991110e-002 8.6444700e-002 2.2420950e-002 +v -7.4609990e-002 1.4727815e-001 -1.4172380e-002 +v -3.4707910e-002 8.4035880e-002 -2.4302260e-002 +v -8.4964900e-002 8.9962540e-002 -3.0068000e-003 +v -8.8091450e-002 8.7741580e-002 4.8489900e-003 +v -9.1490470e-002 1.4543178e-001 2.2277220e-002 +v -9.4380420e-002 1.2183919e-001 1.7904340e-002 +v -2.9164530e-002 8.5393440e-002 -3.3666780e-002 +v -3.0557790e-002 8.8625920e-002 -2.7550670e-002 +v -7.7770550e-002 8.7844840e-002 -1.1694810e-002 +v -8.0728260e-002 8.8204150e-002 -7.8003100e-003 +v -8.3272540e-002 8.9476690e-002 -5.6502900e-003 +v -8.9398710e-002 8.9539000e-002 1.1645550e-002 +v -8.9698390e-002 1.3971257e-001 1.3774760e-002 +v -7.7134890e-002 1.5151225e-001 -5.5823000e-003 +v -5.1121410e-002 1.6374125e-001 -2.6640500e-003 +v -8.6442960e-002 1.2767438e-001 -1.4864100e-003 +v -6.9605590e-002 1.5490763e-001 -5.0188670e-002 +v -8.7265180e-002 9.2110030e-002 4.2059000e-003 +v -8.9086250e-002 9.2377120e-002 1.0569860e-002 +v -8.9612340e-002 9.1599880e-002 1.7812280e-002 +v -8.2732460e-002 1.4196856e-001 1.2529100e-003 +v -7.2618370e-002 1.4368135e-001 -1.0987100e-002 +v -7.7677230e-002 1.6610992e-001 -3.6777320e-002 +v -1.5078060e-002 9.3863440e-002 -3.4317310e-002 +v -7.1057280e-002 1.5476885e-001 -4.5778530e-002 +v -9.2331920e-002 1.2523886e-001 9.1589500e-003 +v -7.6046700e-002 9.1037250e-002 -1.3643150e-002 +v -8.2942810e-002 9.3291700e-002 -6.1856300e-003 +v -1.0411170e-002 9.4592340e-002 -3.3784850e-002 +v -2.9331140e-002 1.1476230e-001 -1.5844640e-002 +v -3.7218250e-002 1.1594244e-001 -1.5173050e-002 +v -1.2429920e-002 1.0286006e-001 -2.3822480e-002 +v 6.6509600e-003 8.8144500e-002 -3.2945810e-002 +v -6.4119900e-003 9.2876210e-002 -3.4817640e-002 +v 1.5800150e-002 1.1996558e-001 -1.1415630e-002 +v 2.9102740e-002 1.0247506e-001 -1.5768380e-002 +v 4.2080690e-002 6.3480630e-002 -2.5405300e-003 +v 2.8723120e-002 9.7943220e-002 -1.7497350e-002 +v -1.9987640e-002 1.0278313e-001 -2.3392920e-002 +v 3.3748350e-002 8.3644140e-002 -1.8630450e-002 +v -1.8685680e-002 1.8689625e-001 -2.0248700e-002 +v 6.4154900e-003 1.1790181e-001 -1.6282740e-002 +v 5.6305210e-002 6.7769910e-002 2.6525000e-003 +v -5.3608300e-003 1.1289400e-001 -1.9613290e-002 +v 4.5769430e-002 6.4628800e-002 -1.2166100e-003 +v -1.0090870e-002 9.8229650e-002 -2.7731360e-002 +v -6.0458520e-002 1.1755645e-001 -1.1354580e-002 +v 1.2933940e-002 1.1887250e-001 -1.3979370e-002 +v 1.5235680e-002 9.4977900e-002 -2.4437140e-002 +v -3.0892950e-002 4.7409030e-002 -2.4954000e-002 +v -1.7766190e-002 1.8572344e-001 -2.3049280e-002 +v -1.3034890e-002 1.1002855e-001 -2.0161170e-002 +v -7.1206550e-002 3.8608570e-002 7.7218000e-004 +v 1.7904800e-002 1.0627709e-001 -1.7729250e-002 +v -3.3623490e-002 1.1840428e-001 -1.1927480e-002 +v -4.9906840e-002 1.1788332e-001 -1.4402480e-002 +v -6.6878100e-003 1.1747209e-001 -1.5359280e-002 +v -1.5451470e-002 1.8597600e-001 -2.4795870e-002 +v -3.0603900e-002 3.8038460e-002 -3.0123840e-002 +v -1.3220270e-002 1.8397188e-001 -2.7519460e-002 +v -4.7859450e-002 1.1162729e-001 -1.7482120e-002 +v -1.3098990e-002 9.0776040e-002 -3.6659270e-002 +v -6.3117340e-002 1.5425437e-001 2.9730400e-003 +v -5.5139750e-002 1.1051601e-001 -1.7672740e-002 +v -1.1096770e-002 1.8202324e-001 -2.8042450e-002 +v -2.6568900e-002 3.4695830e-002 -2.9113750e-002 +v -6.6396600e-003 1.0222209e-001 -2.3519320e-002 +v -5.6996400e-002 1.5741713e-001 6.0244000e-004 +v 1.9076550e-002 9.1870620e-002 -2.4890230e-002 +v 1.3473090e-002 1.2429893e-001 -6.8361400e-003 +v -2.1730490e-002 9.8410960e-002 -2.4306850e-002 +v -1.7142170e-002 9.8057460e-002 -2.4924330e-002 +v -5.8698110e-002 1.5137318e-001 -6.5801000e-004 +v 3.5641100e-003 1.2764883e-001 -4.4672400e-003 +v -8.5369800e-003 9.9921220e-002 -2.4351070e-002 +v -1.2171980e-002 1.8125102e-001 -2.9061170e-002 +v -6.1113980e-002 1.5305212e-001 9.9983000e-004 +v -2.9570620e-002 1.1713871e-001 -1.3675530e-002 +v 3.0530110e-002 1.1221207e-001 -8.1860600e-003 +v -3.1714100e-002 3.5111530e-002 -3.0658990e-002 +v -1.3691130e-002 1.7914707e-001 -2.8126410e-002 +v 1.1620840e-002 1.1548972e-001 -1.6385680e-002 +v -6.1993570e-002 1.5028063e-001 -1.6297100e-003 +v 3.6684020e-002 1.0099570e-001 -9.8485900e-003 +v 4.8512670e-002 7.1798180e-002 6.0005000e-003 +v -4.6583000e-004 1.1983662e-001 -1.3610580e-002 +v 1.6747170e-002 9.0113950e-002 -2.7127190e-002 +v 6.9832400e-003 9.7730080e-002 -2.4800310e-002 +v -4.3226830e-002 4.6263570e-002 -1.1771730e-002 +v -8.3562500e-003 1.1373600e-001 -1.8239810e-002 +v -1.2354410e-002 1.1556773e-001 -1.6486930e-002 +v 4.6834470e-002 7.4354100e-002 1.0139500e-002 +v 2.5319170e-002 1.0931725e-001 -1.3579660e-002 +v -4.2459500e-002 1.1392482e-001 -1.6188050e-002 +v 5.7744640e-002 6.4158440e-002 2.6277600e-003 +v -5.9710530e-002 3.6535780e-002 -9.4949000e-003 +v -3.2078400e-003 1.0962100e-001 -2.1523850e-002 +v 2.7020740e-002 6.1345700e-002 -2.2292060e-002 +v 7.1030200e-003 1.0191162e-001 -2.1230990e-002 +v -3.8225680e-002 1.2465525e-001 -7.3257400e-003 +v 2.5941540e-002 1.1576352e-001 -8.2193900e-003 +v -6.1297960e-002 3.3900220e-002 -9.3216600e-003 +v -5.9466670e-002 1.4743956e-001 -1.8885400e-003 +v 1.0506610e-002 1.0087700e-001 -2.2109510e-002 +v 3.3081340e-002 1.0273382e-001 -1.2787210e-002 +v 1.2517840e-002 1.0475378e-001 -1.9915960e-002 +v 2.3087990e-002 9.3998720e-002 -2.2210680e-002 +v 3.1555430e-002 9.2484730e-002 -1.8204280e-002 +v 6.2723100e-003 9.9910370e-002 -2.2296890e-002 +v -4.0917240e-002 4.6121780e-002 -1.7942580e-002 +v 3.5407360e-002 9.8188850e-002 -1.2008970e-002 +v 9.4135900e-003 1.2121902e-001 -1.2937780e-002 +v 5.3735190e-002 7.2027350e-002 6.8010000e-003 +v 2.5620340e-002 1.1880719e-001 -5.0330800e-003 +v -3.8150260e-002 4.2466610e-002 -2.6893990e-002 +v -2.8212410e-002 1.1116862e-001 -1.8001930e-002 +v -6.0253590e-002 1.4339100e-001 -3.7906300e-003 +v 1.9016880e-002 1.0401450e-001 -1.9333120e-002 +v 7.5446700e-003 9.1682150e-002 -3.1643140e-002 +v -7.0760800e-003 1.2240119e-001 -1.1364410e-002 +v -1.9047500e-002 9.6562130e-002 -2.7579900e-002 +v -1.6953390e-002 1.0669256e-001 -2.2002990e-002 +v -6.7307000e-004 1.0119875e-001 -2.2857770e-002 +v -9.0179300e-003 1.2528031e-001 -7.7912000e-003 +v -6.8136180e-002 1.8006113e-001 -5.8816050e-002 +v -2.3600190e-002 1.1513818e-001 -1.5577390e-002 +v -5.9831220e-002 4.2842260e-002 -6.6469100e-003 +v 5.3124070e-002 5.9012380e-002 -2.8853800e-003 +v -3.6931840e-002 3.7107370e-002 -2.9714170e-002 +v -5.6215140e-002 1.4139213e-001 -2.8027300e-003 +v 3.6695880e-002 1.0372844e-001 -7.9621500e-003 +v -3.5885070e-002 1.2040038e-001 -1.0640470e-002 +v -9.3569500e-003 8.5423730e-002 -3.8112540e-002 +v -6.0127340e-002 1.2041391e-001 -9.3791100e-003 +v -3.9842790e-002 1.2156113e-001 -1.1570310e-002 +v 2.8322200e-002 1.0847957e-001 -1.2623390e-002 +v -1.8733500e-003 1.1593910e-001 -1.7169430e-002 +v 3.8648150e-002 9.0153340e-002 -1.2549680e-002 +v -1.7359200e-003 9.2244170e-002 -3.4310460e-002 +v 5.0000820e-002 6.1612070e-002 -3.4649900e-003 +v 5.5858960e-002 6.2910170e-002 6.9037000e-004 +v 2.0461520e-002 1.1515372e-001 -1.3103780e-002 +v -1.5165840e-002 1.1798075e-001 -1.4465520e-002 +v -7.0859540e-002 7.1510150e-002 3.3895100e-002 +v 2.2674030e-002 8.6606050e-002 -2.4925490e-002 +v 3.5358840e-002 8.7438890e-002 -1.7109050e-002 +v 1.8400920e-002 1.2145507e-001 -7.6804200e-003 +v -2.5425900e-002 4.1421010e-002 -2.9204830e-002 +v -8.2085100e-003 9.6777440e-002 -3.0809780e-002 +v -5.6810660e-002 3.3873940e-002 -1.1166310e-002 +v -3.4588640e-002 4.4744960e-002 -2.7122900e-002 +v -4.0251680e-002 1.1827531e-001 -1.3674080e-002 +v 1.6387020e-002 1.1402346e-001 -1.5496900e-002 +v 4.2635280e-002 6.0797460e-002 -3.4583700e-003 +v -5.0687200e-002 3.5935870e-002 -1.2380790e-002 +v 7.3446800e-003 9.4509570e-002 -2.9683220e-002 +v -1.9706700e-002 9.2917340e-002 -3.4636880e-002 +v -1.2083040e-002 1.2219229e-001 -9.7120900e-003 +v 4.8805930e-002 6.8457810e-002 1.6952900e-003 +v -3.0869700e-003 9.8402500e-002 -2.7403170e-002 +v -5.3198790e-002 1.3672896e-001 -1.6580500e-003 +v -4.7290060e-002 1.3055355e-001 1.6909100e-003 +v 4.4651700e-003 1.2044039e-001 -1.3931400e-002 +v -2.3850100e-003 1.2290534e-001 -1.0382460e-002 +v -2.4833330e-002 9.5858030e-002 -2.5162110e-002 +v -4.2296900e-002 3.6291920e-002 -2.7253600e-002 +v -5.4388260e-002 1.3404922e-001 -3.9920400e-003 +v -5.0539380e-002 1.3336659e-001 -1.0872200e-003 +v 2.6040300e-003 9.6942660e-002 -2.8407060e-002 +v -7.8163100e-003 1.2821209e-001 -1.9430400e-003 +v 6.5111700e-003 1.3002517e-001 9.2881000e-004 +v 3.4742860e-002 9.2274140e-002 -1.5654590e-002 +v -6.7787700e-002 1.8088887e-001 -5.8191050e-002 +v -3.3715410e-002 1.1151566e-001 -1.8078440e-002 +v 4.4630400e-003 1.2427294e-001 -9.4291400e-003 +v -2.3370170e-002 9.3392760e-002 -3.2031820e-002 +v -4.8982070e-002 1.2980647e-001 -1.3229400e-003 +v -7.8164000e-004 1.2822918e-001 -3.2490000e-003 +v 2.4960400e-003 8.9857600e-002 -3.3628450e-002 +v 7.4553300e-003 1.1196790e-001 -1.9554260e-002 +v 2.8791140e-002 9.1157340e-002 -2.0370210e-002 +v -5.3590150e-002 1.2437450e-001 -7.3470400e-003 +v -4.7743630e-002 1.2064432e-001 -1.2812990e-002 +v -1.9616230e-002 1.2109197e-001 -9.5487700e-003 +v -6.5047370e-002 1.7999148e-001 -5.9758600e-002 +v -5.1704160e-002 3.7620360e-002 -1.1763450e-002 +v -5.2124270e-002 1.2929832e-001 -4.1187000e-003 +v -4.5334450e-002 1.2891494e-001 1.5819100e-003 +v -3.0471200e-003 1.2919453e-001 -1.0688000e-003 +v 7.2129600e-003 1.2721957e-001 -5.2073700e-003 +v 1.1669320e-002 1.2720154e-001 -3.1850900e-003 +v 5.3056400e-002 6.9708830e-002 3.1291400e-003 +v -6.3021150e-002 1.7810951e-001 -6.0393570e-002 +v 2.8204800e-002 6.4391270e-002 -2.0698040e-002 +v 3.4400180e-002 1.0503000e-001 -1.0224920e-002 +v 3.0975190e-002 1.0790250e-001 -1.1058430e-002 +v -4.8984390e-002 1.1480518e-001 -1.5966690e-002 +v -3.2821710e-002 1.2300500e-001 -5.9088300e-003 +v -5.0792860e-002 1.2716487e-001 -4.8183200e-003 +v -3.5301670e-002 1.2547815e-001 -3.1542800e-003 +v 5.6455250e-002 6.9951490e-002 4.9191700e-003 +v -1.6240450e-002 1.2512177e-001 -3.6499700e-003 +v -1.6970400e-002 1.1119793e-001 -1.9586410e-002 +v -5.4088120e-002 3.9781210e-002 -1.0544680e-002 +v -3.4190490e-002 4.7514010e-002 -2.2301500e-002 +v 1.3699090e-002 9.3914220e-002 -2.6427690e-002 +v 8.8000000e-004 9.9234930e-002 -2.4355670e-002 +v -4.6459460e-002 1.2723953e-001 -4.8843300e-003 +v -4.1735500e-002 1.2687599e-001 -4.1742000e-003 +v -2.1000480e-002 1.2313643e-001 -6.1190100e-003 +v -1.2130450e-002 1.2572568e-001 -5.2007900e-003 +v -4.3822400e-003 1.2640753e-001 -6.9495200e-003 +v 1.4085700e-003 3.4781990e-002 -2.3265200e-002 +v -1.4846200e-002 3.5070930e-002 -2.6071900e-002 +v -2.1399500e-002 3.4795120e-002 -2.7958820e-002 +v 1.2009220e-002 3.5961900e-002 -2.1735750e-002 +v 3.8249200e-003 3.6129220e-002 -2.3878090e-002 +v -5.1139560e-002 9.6617580e-002 -2.2095120e-002 +v -5.4813320e-002 9.8102480e-002 -2.1425370e-002 +v -2.7597040e-002 1.6979824e-001 -1.8170420e-002 +v 1.3359870e-002 3.9377410e-002 -2.2496330e-002 +v 4.3919300e-003 3.8674430e-002 -2.4170290e-002 +v -6.8478200e-003 3.6444540e-002 -2.5177120e-002 +v -1.3280260e-002 3.7699590e-002 -2.6391810e-002 +v -4.7672760e-002 3.6116650e-002 -1.3301210e-002 +v -4.5590120e-002 1.0853826e-001 -1.8796680e-002 +v -5.0095670e-002 1.0990925e-001 -1.8504510e-002 +v -6.5766640e-002 3.6469550e-002 -7.2073000e-003 +v -2.3455840e-002 1.6824727e-001 -1.8822880e-002 +v -4.5918000e-003 3.8404570e-002 -2.5412870e-002 +v -2.4954130e-002 3.7441060e-002 -2.9152720e-002 +v 2.9007770e-002 3.7358220e-002 -2.7474000e-004 +v -7.9468800e-003 4.1489920e-002 -2.5911270e-002 +v -1.6803800e-002 3.9753810e-002 -2.7565350e-002 +v -6.5156150e-002 1.4034537e-001 -7.6848600e-003 +v -4.7080100e-002 4.0700690e-002 -1.1869830e-002 +v -6.8470630e-002 3.7477700e-002 -4.9557400e-003 +v 3.7326850e-002 4.0209510e-002 -8.5850000e-004 +v 3.5349870e-002 4.1257050e-002 -2.8075100e-003 +v 5.1820700e-003 4.1536320e-002 -2.4065670e-002 +v 1.8660660e-002 1.0030784e-001 -2.2127290e-002 +v -6.0510780e-002 1.0748450e-001 -1.7042300e-002 +v -6.2374340e-002 4.0146090e-002 -7.4040200e-003 +v 2.5456950e-002 3.9483890e-002 -4.0251400e-003 +v -2.2828000e-004 4.3394940e-002 -2.5124420e-002 +v -8.1088400e-003 4.3439060e-002 -2.6140070e-002 +v -1.7362450e-002 4.3237420e-002 -2.7665190e-002 +v -2.6416670e-002 4.4674020e-002 -2.8209740e-002 +v 3.8064500e-003 1.0944331e-001 -2.0203790e-002 +v -5.8232370e-002 9.5690400e-002 -2.0616030e-002 +v -6.6122370e-002 4.2341260e-002 -2.7538800e-003 +v -6.0959920e-002 9.4173040e-002 -1.9015670e-002 +v 3.1352250e-002 4.2649280e-002 -4.6745000e-003 +v -3.3540900e-002 3.6342620e-002 4.9089960e-002 +v 1.7252780e-002 4.4335610e-002 -2.3067190e-002 +v 1.0637660e-002 4.4161560e-002 -2.4926170e-002 +v 4.3843100e-003 4.5806710e-002 -2.6788990e-002 +v -8.2506400e-003 4.5148720e-002 -2.8441070e-002 +v -1.5748410e-002 4.5043860e-002 -2.7877790e-002 +v 2.8990330e-002 4.4697850e-002 -6.1863000e-003 +v 8.1686400e-003 4.5053030e-002 -2.5178740e-002 +v -9.6291000e-004 4.5378230e-002 -2.7308280e-002 +v -1.7033400e-003 4.7819200e-002 -2.9928930e-002 +v -3.1535830e-002 4.4740410e-002 -2.8079410e-002 +v -3.3619650e-002 1.5691468e-001 -1.1024870e-002 +v -5.0751180e-002 4.3109620e-002 -1.0018680e-002 +v 3.6890890e-002 4.7353200e-002 -6.1057100e-003 +v 2.4975630e-002 4.2644580e-002 -7.0169900e-003 +v 2.4562420e-002 4.8369560e-002 -1.9672760e-002 +v 1.3964040e-002 4.5579170e-002 -2.4706510e-002 +v 1.3376130e-002 4.8630300e-002 -2.6551500e-002 +v 3.7308900e-003 4.8127990e-002 -2.9025970e-002 +v -8.7947000e-003 4.7056850e-002 -2.9881630e-002 +v -1.3753770e-002 5.1865060e-002 -3.2243480e-002 +v -2.1200840e-002 4.6657090e-002 -2.7951320e-002 +v 3.9693540e-002 4.5658580e-002 -4.5274100e-003 +v 3.3627400e-002 4.8717730e-002 -6.3904600e-003 +v -6.5352120e-002 9.9294570e-002 -1.6820150e-002 +v 1.2868100e-003 5.0383670e-002 -3.0357440e-002 +v -8.1797500e-003 4.9845800e-002 -3.1071390e-002 +v -1.7184350e-002 4.8210500e-002 -2.9741930e-002 +v -2.6049450e-002 4.7692500e-002 -2.6149500e-002 +v -8.4747010e-002 1.1078350e-001 3.9488380e-002 +v -5.1316870e-002 4.8270690e-002 -7.9310500e-003 +v -8.2506510e-002 1.2765487e-001 -4.6796400e-003 +v 3.8663690e-002 5.1696670e-002 -6.6910200e-003 +v -7.5643160e-002 9.9440450e-002 -1.1927610e-002 +v 2.0284470e-002 5.1349190e-002 -2.4895380e-002 +v 5.9436000e-003 5.0976660e-002 -2.9119360e-002 +v -2.5528290e-002 5.1472710e-002 -2.6884680e-002 +v -3.5562670e-002 4.9399890e-002 -1.2865040e-002 +v -4.2818980e-002 1.6220182e-001 -1.0337510e-002 +v -6.5593600e-002 1.7665711e-001 -6.0504730e-002 +v -3.4151080e-002 1.7442797e-001 -1.3312550e-002 +v 4.3673180e-002 5.0162230e-002 -5.9843500e-003 +v -5.0342410e-002 1.5546197e-001 -5.1927700e-003 +v 2.5464180e-002 5.4029700e-002 -2.1691010e-002 +v 1.0149790e-002 4.9258540e-002 -2.7750590e-002 +v -2.2043190e-002 5.3612020e-002 -3.0135610e-002 +v -3.2875520e-002 5.1677630e-002 -1.0888650e-002 +v -3.7613820e-002 4.9534770e-002 -1.1626140e-002 +v -4.0750630e-002 4.9285110e-002 -1.1286200e-002 +v -4.6385170e-002 4.7490850e-002 -1.0085980e-002 +v 4.4473170e-002 5.3293010e-002 -6.3327900e-003 +v 3.3205620e-002 5.1020650e-002 -7.2382500e-003 +v 1.5678350e-002 5.1169270e-002 -2.6397810e-002 +v 6.8341700e-003 5.5010170e-002 -3.0561130e-002 +v 2.1424700e-003 5.5502800e-002 -3.1334400e-002 +v 5.9285000e-004 5.2867950e-002 -3.0513830e-002 +v -3.6481400e-003 5.1869000e-002 -3.1457940e-002 +v -9.4245600e-003 5.5399220e-002 -3.3653980e-002 +v -1.9302150e-002 5.8224770e-002 -3.3919700e-002 +v -6.1084270e-002 1.3386190e-001 -7.2248900e-003 +v -4.3309760e-002 5.5656840e-002 -1.1402110e-002 +v -6.1080540e-002 1.6833773e-001 -5.9192060e-002 +v 4.7574690e-002 5.2943630e-002 -5.1300300e-003 +v -3.7403030e-002 1.1150775e-001 -1.8243310e-002 +v 1.9972490e-002 5.4409710e-002 -2.7108230e-002 +v 5.3974800e-003 5.8382570e-002 -3.0903760e-002 +v -1.0603590e-002 5.3602910e-002 -3.3403350e-002 +v -3.4998290e-002 5.2331560e-002 -1.0347380e-002 +v -4.6471230e-002 5.1304340e-002 -9.8299800e-003 +v -6.7945360e-002 1.1493603e-001 -9.5107300e-003 +v -7.1048210e-002 1.5161088e-001 -4.4679270e-002 +v -5.8903800e-003 3.4790620e-002 -2.4224470e-002 +v 1.6842140e-002 5.5555670e-002 -2.8284560e-002 +v 1.0711040e-002 5.4687610e-002 -2.9767520e-002 +v -1.1826800e-003 5.9492420e-002 -3.3360920e-002 +v -5.2325900e-003 5.5688960e-002 -3.2840220e-002 +v -5.1705830e-002 5.2470760e-002 -7.4047200e-003 +v -5.2626360e-002 6.0043760e-002 -8.9566900e-003 +v -7.2598590e-002 9.7762720e-002 -1.4434510e-002 +v 4.4331260e-002 5.5818010e-002 -6.0362700e-003 +v 3.8463400e-002 5.4934820e-002 -6.1822500e-003 +v 3.8838620e-002 5.7808260e-002 -5.2584800e-003 +v -9.2015400e-003 5.9510130e-002 -3.4437110e-002 +v -3.5262560e-002 5.5284900e-002 -1.0545060e-002 +v -3.8336450e-002 5.4503540e-002 -1.0905320e-002 +v -1.7727540e-002 3.6289540e-002 5.2222250e-002 +v 5.0006490e-002 5.8095800e-002 -4.6211800e-003 +v 4.6133970e-002 5.9278810e-002 -4.7769600e-003 +v 1.5110300e-002 5.9819840e-002 -2.8645750e-002 +v 1.0312380e-002 5.7586530e-002 -2.9995250e-002 +v -6.1353400e-003 6.0256790e-002 -3.4695830e-002 +v -1.2318220e-002 5.9396390e-002 -3.5268510e-002 +v -1.4466910e-002 6.3136020e-002 -3.6865870e-002 +v -4.6650260e-002 5.9840950e-002 -1.2135840e-002 +v -5.6572080e-002 1.2480275e-001 -7.1885700e-003 +v -7.9237500e-002 1.2055419e-001 -5.6744800e-003 +v -7.9334790e-002 1.2560650e-001 -6.1175900e-003 +v 2.2340000e-002 5.8492230e-002 -2.6014120e-002 +v 7.6270400e-003 6.2098330e-002 -3.1135840e-002 +v 3.3101700e-003 6.0456840e-002 -3.2481070e-002 +v -1.6811880e-002 6.1275230e-002 -3.5929330e-002 +v -3.2491910e-002 5.7196350e-002 -1.2104730e-002 +v -3.4108240e-002 6.1466560e-002 -1.3053130e-002 +v -3.3896980e-002 5.7025330e-002 -1.1047570e-002 +v -3.8623580e-002 5.8303290e-002 -1.1505750e-002 +v -4.5008400e-002 6.2723940e-002 -1.3390450e-002 +v -5.6896010e-002 1.3398739e-001 -5.6270700e-003 +v -4.4853890e-002 1.5746031e-001 -8.6731600e-003 +v -7.8609550e-002 6.9656870e-002 1.1810740e-002 +v -2.3730020e-002 1.0186156e-001 -2.3836400e-002 +v -2.8122930e-002 9.9322390e-002 -2.3580130e-002 +v -5.0076720e-002 1.4997652e-001 -3.6419700e-003 +v -3.3048420e-002 9.5958590e-002 -2.3426460e-002 +v 1.9520390e-002 6.2064770e-002 -2.7292470e-002 +v -3.8864710e-002 1.0333987e-001 -2.0641400e-002 +v -4.8952940e-002 5.6281090e-002 -1.0220880e-002 +v -5.3993040e-002 1.4498656e-001 -1.1093400e-003 +v -4.5530560e-002 9.8510850e-002 -2.1729510e-002 +v -5.0910960e-002 1.0074570e-001 -2.1619430e-002 +v 2.3245830e-002 6.2792530e-002 -2.5047990e-002 +v 9.7412800e-003 6.3181400e-002 -3.1141370e-002 +v -8.6614000e-004 6.4559630e-002 -3.4490930e-002 +v -8.5264000e-003 6.4001730e-002 -3.5850480e-002 +v -4.8451500e-002 6.4794120e-002 -1.3029910e-002 +v -5.2325160e-002 1.0614813e-001 -1.9271240e-002 +v -5.5265350e-002 1.0216682e-001 -1.9897100e-002 +v -5.9042010e-002 9.9032210e-002 -1.9222950e-002 +v -5.7846760e-002 1.0433496e-001 -1.8525740e-002 +v -2.7113460e-002 1.7332156e-001 -1.8538890e-002 +v 2.2832000e-002 6.7082570e-002 -2.6297510e-002 +v 1.4519060e-002 6.4595540e-002 -2.9855690e-002 +v 1.1471330e-002 6.7581440e-002 -3.0901170e-002 +v -1.7739360e-002 6.6260830e-002 -3.7657310e-002 +v -6.5059750e-002 1.3452104e-001 -8.0899900e-003 +v -7.5829320e-002 1.4244605e-001 -5.8090000e-003 +v -4.1362350e-002 6.1637330e-002 -1.2813770e-002 +v -5.6147890e-002 6.1921550e-002 -5.7541100e-003 +v -6.2126110e-002 6.2845360e-002 -4.5202600e-003 +v -3.7292480e-002 1.6449057e-001 -1.3627050e-002 +v -1.9818920e-002 1.6509494e-001 -1.7608980e-002 +v 6.2881100e-003 6.5416350e-002 -3.2563040e-002 +v -5.9250500e-003 6.9515630e-002 -3.5933480e-002 +v -1.0538630e-002 6.7999180e-002 -3.6517060e-002 +v -3.5385700e-002 6.6817430e-002 -1.5434860e-002 +v -5.3994500e-002 6.4638700e-002 -9.3254900e-003 +v -6.3852310e-002 6.5572310e-002 -6.9393300e-003 +v -6.3920880e-002 1.2774242e-001 -8.5494600e-003 +v -2.6940700e-002 3.6184050e-002 5.3351850e-002 +v 1.9618650e-002 6.7007390e-002 -2.8356120e-002 +v 1.2275180e-002 6.9933940e-002 -3.1553160e-002 +v 5.4265100e-003 6.8247960e-002 -3.2730520e-002 +v -4.4084200e-003 6.6619200e-002 -3.4870250e-002 +v -2.1911350e-002 6.7144790e-002 -3.6535750e-002 +v -4.5643150e-002 1.5466949e-001 -7.2969400e-003 +v -5.1673460e-002 6.6850660e-002 -1.2120350e-002 +v -5.8105180e-002 6.6465950e-002 -1.0044340e-002 +v -5.6992260e-002 1.4311862e-001 -2.2403000e-003 +v -8.0651110e-002 1.3119854e-001 -4.4397800e-003 +v -5.6544310e-002 1.2850938e-001 -6.2014700e-003 +v 1.7758080e-002 7.0138540e-002 -2.9404680e-002 +v 6.4980500e-003 7.0791870e-002 -3.3525310e-002 +v 7.5831000e-004 7.0434460e-002 -3.4462560e-002 +v -1.3235950e-002 6.9292820e-002 -3.7917490e-002 +v -6.7390780e-002 1.1889688e-001 -8.7301400e-003 +v -3.8119520e-002 6.4162310e-002 -1.3829140e-002 +v 1.8527400e-003 1.1303356e-001 -1.9794270e-002 +v -7.5950810e-002 6.8170610e-002 1.8117970e-002 +v -1.0001990e-002 7.2671480e-002 -3.7661370e-002 +v -1.7976070e-002 7.0613770e-002 -3.8443880e-002 +v -2.3035990e-002 7.2778460e-002 -3.8072640e-002 +v -2.6120100e-002 7.1177480e-002 -3.5451530e-002 +v -6.8535420e-002 1.3929375e-001 -7.8046600e-003 +v -3.5263040e-002 7.1067650e-002 -1.8011860e-002 +v -4.1558180e-002 6.9774010e-002 -1.6774100e-002 +v -5.2831730e-002 7.0298920e-002 -1.4864960e-002 +v -6.6978850e-002 6.7638980e-002 -6.8094400e-003 +v -1.0244470e-002 1.7895826e-001 -2.9538870e-002 +v -7.5272650e-002 1.2680098e-001 -8.0241700e-003 +v -8.7359900e-002 1.1248315e-001 4.2049490e-002 +v 8.7503000e-003 7.4301560e-002 -3.3398210e-002 +v -6.4249520e-002 1.6045024e-001 -5.7041470e-002 +v -4.4354010e-002 7.3372220e-002 -1.7874430e-002 +v -4.5762580e-002 6.9445320e-002 -1.5928780e-002 +v -4.7957440e-002 7.2542990e-002 -1.6106990e-002 +v -5.7822630e-002 6.9538010e-002 -1.4416470e-002 +v -7.2071600e-002 7.1538150e-002 -7.4714400e-003 +v 2.5472930e-002 7.4094500e-002 -2.4938540e-002 +v 1.5719730e-002 7.3756350e-002 -2.9747770e-002 +v 4.8214000e-003 7.3763980e-002 -3.4552450e-002 +v -2.2528600e-003 7.3921320e-002 -3.5887190e-002 +v -7.3834900e-003 7.4799620e-002 -3.7223830e-002 +v -2.0225340e-002 7.7095190e-002 -3.9044290e-002 +v -3.4016180e-002 7.2101270e-002 -2.0823150e-002 +v -3.8493370e-002 7.2839870e-002 -1.7502230e-002 +v -6.4392550e-002 7.3116330e-002 -1.5335340e-002 +v -6.4480660e-002 7.0187350e-002 -1.2261750e-002 +v -2.3854330e-002 1.6164528e-001 -1.4504190e-002 +v 2.2104450e-002 7.2692600e-002 -2.6900140e-002 +v 1.5532370e-002 7.6586960e-002 -2.9606940e-002 +v 1.1574050e-002 7.4860570e-002 -3.1383860e-002 +v -1.4731560e-002 7.7640750e-002 -3.8490670e-002 +v -1.6018820e-002 7.4288800e-002 -3.8864420e-002 +v -5.1103620e-002 7.3071950e-002 -1.6243060e-002 +v -5.7989540e-002 7.4017880e-002 -1.7522320e-002 +v -6.9608380e-002 7.2322890e-002 -1.0934430e-002 +v -7.5996110e-002 1.1714132e-001 -6.5577200e-003 +v -3.7987660e-002 1.0751453e-001 -1.9975760e-002 +v 1.0696210e-002 7.9889200e-002 -3.2009580e-002 +v -5.3433400e-003 7.8264580e-002 -3.7476940e-002 +v -2.6081990e-002 7.6191290e-002 -3.6780200e-002 +v -3.9161040e-002 1.5718885e-001 -1.0580510e-002 +v -6.5609880e-002 7.5860010e-002 -1.6750060e-002 +v -7.0177600e-002 7.5663330e-002 -1.3839210e-002 +v -7.4291360e-002 7.4808360e-002 -9.3537900e-003 +v -6.3428890e-002 1.7185387e-001 -6.1412170e-002 +v 3.0684890e-002 7.5726870e-002 -2.0778090e-002 +v 1.9305010e-002 7.9017870e-002 -2.7743990e-002 +v -8.5992100e-003 7.9338730e-002 -3.7905180e-002 +v -2.3200110e-002 7.6568500e-002 -3.8386500e-002 +v -3.8117820e-002 7.6390120e-002 -1.8644360e-002 +v -4.4231130e-002 7.7664130e-002 -1.9026580e-002 +v -5.1025500e-002 7.5705070e-002 -1.8186900e-002 +v -7.0595130e-002 1.2994832e-001 -8.7629200e-003 +v 2.8147660e-002 7.8785370e-002 -2.2432450e-002 +v 7.6016000e-003 7.9435920e-002 -3.3714560e-002 +v 4.9502400e-003 7.8027250e-002 -3.4409750e-002 +v -1.5858350e-002 8.1165550e-002 -3.9185590e-002 +v -1.8502080e-002 8.3343870e-002 -3.9010720e-002 +v -7.9739350e-002 1.3606854e-001 -4.1482100e-003 +v -3.0980180e-002 1.6634656e-001 -1.6241160e-002 +v -3.5749800e-002 7.7248350e-002 -1.9374020e-002 +v -4.8944740e-002 7.9086360e-002 -1.9575700e-002 +v -5.5065860e-002 7.8089190e-002 -1.9755480e-002 +v 2.3706000e-002 8.0240410e-002 -2.5450120e-002 +v 1.2254110e-002 8.3456700e-002 -3.0771580e-002 +v 1.8549900e-003 8.4692790e-002 -3.4838500e-002 +v -2.0857000e-004 7.8941410e-002 -3.5782080e-002 +v -4.2710000e-004 8.2947370e-002 -3.6380660e-002 +v -4.4101600e-003 8.2794510e-002 -3.7467250e-002 +v -3.3202320e-002 1.0578320e-001 -2.0647590e-002 +v -3.9206970e-002 8.1536380e-002 -2.0571000e-002 +v -6.0355410e-002 7.9766610e-002 -1.9375540e-002 +v -4.1771830e-002 1.0396706e-001 -2.0832940e-002 +v -1.1204010e-002 8.2713320e-002 -3.8489610e-002 +v -2.3181500e-002 8.1686990e-002 -3.8329160e-002 +v -2.7233190e-002 8.0570950e-002 -3.6620670e-002 +v -3.5470180e-002 8.0196070e-002 -2.2325910e-002 +v -4.4864210e-002 8.1997900e-002 -2.0473520e-002 +v -5.0647890e-002 8.2309430e-002 -2.1365890e-002 +v -5.5522610e-002 8.1927600e-002 -2.1353790e-002 +v -8.8089610e-002 1.1135484e-001 1.8516150e-002 +v -7.2036080e-002 1.1107918e-001 4.5361400e-002 +v -3.3359780e-002 1.6986395e-001 -1.5448990e-002 +v -6.6839030e-002 6.2170510e-002 2.1576840e-002 +v 3.0730560e-002 8.1968990e-002 -2.0040460e-002 +v 1.6224320e-002 8.6480380e-002 -2.8952010e-002 +v -6.9855630e-002 1.0027892e-001 -1.4847830e-002 +v -6.3836170e-002 8.1704600e-002 -1.8908860e-002 +v -6.7914820e-002 8.0136290e-002 -1.7128200e-002 +v -4.5752080e-002 1.6340754e-001 -8.1780500e-003 +v 1.1727540e-002 8.8010780e-002 -3.0860110e-002 +v 7.3334800e-003 8.5270000e-002 -3.2829380e-002 +v -3.4356500e-003 8.7017890e-002 -3.6461000e-002 +v -2.6964110e-002 8.4512810e-002 -3.6361740e-002 +v -3.6553370e-002 8.5316190e-002 -2.2576200e-002 +v -3.8791090e-002 8.5232710e-002 -2.1917600e-002 +v -5.7676940e-002 8.6258340e-002 -2.1098320e-002 +v -6.2581810e-002 8.6394530e-002 -1.9169290e-002 +v -7.1395340e-002 1.2468846e-001 -8.5944200e-003 +v 1.4801570e-002 9.9040900e-002 -2.2842920e-002 +v -2.1162860e-002 1.7491852e-001 -2.1977110e-002 +v -1.4824250e-002 8.7288840e-002 -3.8317070e-002 +v -2.3285750e-002 8.9468030e-002 -3.6027250e-002 +v -5.1595650e-002 8.4422070e-002 -2.1600960e-002 +v -6.9481040e-002 8.5656460e-002 -1.7198420e-002 +v -7.0917210e-002 1.0754846e-001 -1.1496630e-002 +v 3.0145320e-002 8.6284000e-002 -2.0408140e-002 +v -5.5578110e-002 1.1567692e-001 -1.4645990e-002 +v -8.0981100e-003 8.9070080e-002 -3.6552200e-002 +v -8.1206310e-002 1.1205088e-001 -8.8299000e-004 +v -1.8772170e-002 8.9838040e-002 -3.6991710e-002 +v -2.1100420e-002 8.6587670e-002 -3.7849050e-002 +v -2.5809910e-002 8.8889590e-002 -3.5082250e-002 +v -4.8984800e-002 9.0731760e-002 -2.1817170e-002 +v -3.5874870e-002 3.4776000e-002 -3.0845200e-002 +v -3.3164390e-002 3.3606540e-002 -2.9721880e-002 +v -2.5964020e-002 3.3487000e-002 -2.6321120e-002 +v -1.6717530e-002 3.3611640e-002 -2.4625420e-002 +v -5.3486300e-003 3.3829010e-002 -2.2600430e-002 +v 6.4843500e-003 3.4293000e-002 -2.0854930e-002 +v 1.3950350e-002 3.4880000e-002 -1.8612870e-002 +v -4.2465980e-002 3.4189100e-002 -2.7260650e-002 +v -3.3241100e-002 3.3578760e-002 -2.6719450e-002 +v 6.2813500e-003 3.4165800e-002 -1.8764230e-002 +v -4.4265790e-002 3.3663660e-002 -2.1914420e-002 +v -2.3671460e-002 3.3630970e-002 -2.3217760e-002 +v -1.1558580e-002 3.3895430e-002 -2.1054260e-002 +v -2.0406400e-003 3.4053940e-002 -1.9331070e-002 +v 1.7323900e-003 3.4459660e-002 -1.6607870e-002 +v -2.7316070e-002 3.3910070e-002 -2.1353750e-002 +v -1.3371080e-002 3.4361580e-002 -1.9023720e-002 +v 9.5887300e-003 3.4207220e-002 -1.5424050e-002 +v -1.4981540e-002 3.5878180e-002 -1.7992380e-002 +v -2.3474300e-003 3.5903130e-002 -1.5929740e-002 +v 2.2544300e-003 3.6411540e-002 -1.4783970e-002 +v -3.5199130e-002 3.3835210e-002 -2.0508290e-002 +v -2.6075450e-002 3.5918600e-002 -1.9405170e-002 +v 8.2740600e-003 3.5645200e-002 -1.2648700e-002 +v 1.0473640e-002 3.4742600e-002 -1.1262870e-002 +v 1.4055380e-002 3.4483430e-002 -1.4495730e-002 +v -3.6970520e-002 3.5680360e-002 -1.5007790e-002 +v -2.4719500e-003 3.8408770e-002 -1.4159030e-002 +v -3.9481890e-002 3.3618220e-002 -2.3612470e-002 +v -4.1091510e-002 3.4006000e-002 -1.1997540e-002 +v -3.1589810e-002 3.5592330e-002 -1.9204150e-002 +v -2.0086310e-002 3.8064450e-002 -1.7220790e-002 +v -1.1113250e-002 3.8290290e-002 -1.5646360e-002 +v 4.4522600e-003 3.7705190e-002 -1.2957650e-002 +v 1.5870480e-002 3.4416230e-002 -2.9666500e-003 +v -4.7872000e-002 3.4136300e-002 -1.5418250e-002 +v -4.7521640e-002 3.3622720e-002 -1.2804590e-002 +v -3.3407340e-002 3.7577040e-002 -1.6158190e-002 +v -2.7851470e-002 3.8404330e-002 -1.7210420e-002 +v -8.5065300e-003 3.9028950e-002 -1.3000800e-002 +v 6.4552500e-003 3.8165190e-002 -1.0164860e-002 +v 7.4147100e-003 3.4659190e-002 -3.0116800e-003 +v 1.1966200e-002 3.4335400e-002 -5.9571300e-003 +v 2.0414820e-002 3.5567580e-002 -3.7806900e-003 +v -1.9288780e-002 3.8762570e-002 -1.4202620e-002 +v -1.1390100e-003 3.9176760e-002 -1.0381370e-002 +v 3.8149200e-003 3.9024470e-002 -8.0827300e-003 +v 7.5208200e-003 3.6733400e-002 -6.7614300e-003 +v 1.9968120e-002 3.4843990e-002 -1.8984900e-003 +v -4.5058400e-002 3.3600490e-002 -1.2527510e-002 +v -3.0754850e-002 3.8639810e-002 -1.4050770e-002 +v -5.1499810e-002 3.3729110e-002 -1.2082510e-002 +v -2.3756860e-002 3.8585750e-002 -1.1093270e-002 +v 3.9734700e-003 3.8208550e-002 -3.7963500e-003 +v 9.5485400e-003 3.4232620e-002 1.7162000e-003 +v 2.9086550e-002 3.5799990e-002 3.5630900e-003 +v -5.5965200e-002 3.3529910e-002 -9.1246200e-003 +v -1.9523510e-002 3.8505210e-002 -4.5434500e-003 +v 1.6363470e-002 3.4394790e-002 2.2948600e-003 +v 2.1324740e-002 3.4624040e-002 5.6444000e-003 +v -3.9670300e-002 3.6174000e-002 -7.3397700e-003 +v -1.4251730e-002 3.8648030e-002 -4.3030400e-003 +v 2.3262300e-003 3.5348200e-002 2.3246000e-003 +v 1.4014300e-002 3.5703800e-002 3.8878900e-003 +v 1.5322800e-002 3.6239700e-002 3.6628500e-003 +v 2.3753130e-002 3.4670710e-002 3.9885300e-003 +v 3.2369180e-002 3.5816010e-002 7.0246300e-003 +v -6.3715900e-002 3.3776930e-002 -8.0065600e-003 +v -6.4266880e-002 3.3562500e-002 -5.1253200e-003 +v -3.8066600e-002 3.8518600e-002 -7.3079600e-003 +v -9.4308800e-003 3.8887690e-002 -7.4848700e-003 +v 3.9677800e-003 3.4200210e-002 4.9754500e-003 +v 9.4292600e-003 3.6030400e-002 4.5275100e-003 +v 2.9859020e-002 3.4980130e-002 9.8349300e-003 +v -5.2730060e-002 3.3497900e-002 -1.8117500e-003 +v -4.1271000e-002 3.3855400e-002 -1.8800800e-003 +v -3.1105000e-003 3.8946190e-002 -2.7793900e-003 +v 6.2194100e-003 3.5134100e-002 6.5492800e-003 +v 2.0897900e-002 3.5937100e-002 8.7849000e-003 +v 3.5606010e-002 3.6526640e-002 9.8155300e-003 +v -6.7078340e-002 3.3840100e-002 -6.1688300e-003 +v -8.1140000e-004 3.7424170e-002 4.7721500e-003 +v 3.1492300e-003 3.4125310e-002 1.1762220e-002 +v 4.9172000e-003 3.3997100e-002 9.1666100e-003 +v 2.5130800e-002 3.4546910e-002 1.1012580e-002 +v 2.8248620e-002 3.5046370e-002 1.6016700e-002 +v -6.7032970e-002 6.5145960e-002 2.7292860e-002 +v -4.6380170e-002 3.3605230e-002 -8.9435000e-004 +v -3.3163400e-002 3.8195400e-002 -5.2520000e-004 +v -3.2074200e-002 3.8323400e-002 -4.2109000e-004 +v -2.1692690e-002 3.8266010e-002 4.5100800e-003 +v 2.3930750e-002 3.4816710e-002 1.7739160e-002 +v 4.2719120e-002 3.9977070e-002 8.9321600e-003 +v -5.8604080e-002 3.3462230e-002 -2.1667000e-004 +v -3.7314400e-002 3.3633000e-002 4.5724700e-003 +v -1.0423990e-002 3.8488570e-002 6.2292700e-003 +v -1.3896900e-003 3.8651360e-002 2.3966500e-003 +v -3.0845000e-004 3.5462480e-002 8.2607200e-003 +v -1.4089000e-003 3.6193080e-002 1.2944550e-002 +v 2.2252900e-002 3.6583300e-002 1.3979700e-002 +v -7.0961830e-002 3.4345730e-002 -7.8374000e-004 +v -6.9066180e-002 3.3717630e-002 -1.9761000e-004 +v -6.4825640e-002 3.3505860e-002 2.8222500e-003 +v -4.7059660e-002 3.3501860e-002 3.5646400e-003 +v -3.6953800e-003 3.8172780e-002 1.3046800e-002 +v 3.3475850e-002 3.6447340e-002 1.6266960e-002 +v 3.7249610e-002 3.7509920e-002 1.4815820e-002 +v -4.5675940e-002 3.3703640e-002 6.4300300e-003 +v -3.8639270e-002 3.3937310e-002 8.5506500e-003 +v -9.5064100e-003 3.8352640e-002 1.5570660e-002 +v 2.1499800e-002 3.5807100e-002 1.8169400e-002 +v 4.4876460e-002 4.1230990e-002 1.6008250e-002 +v -7.2474010e-002 3.6255930e-002 1.5532600e-003 +v -7.1498130e-002 3.4452970e-002 4.2026500e-003 +v -2.7790900e-002 3.8062900e-002 7.9376100e-003 +v -1.6556410e-002 3.8286470e-002 1.0215790e-002 +v 8.1043500e-003 3.4842900e-002 1.8134600e-002 +v 2.3589460e-002 3.5890600e-002 2.5337690e-002 +v 4.1261350e-002 4.0585070e-002 2.0751930e-002 +v -5.1350870e-002 3.3645700e-002 8.0329400e-003 +v -4.7104300e-002 3.5549500e-002 8.0803900e-003 +v -1.4103500e-003 3.6999940e-002 1.6982030e-002 +v 9.1714000e-004 3.4803380e-002 1.5634690e-002 +v 2.8887900e-003 3.4636250e-002 1.8849770e-002 +v 1.3279200e-002 3.4379500e-002 2.1423700e-002 +v 1.4322700e-002 3.4425500e-002 2.1593200e-002 +v 1.7490100e-002 3.4646300e-002 2.2040900e-002 +v 2.9868460e-002 3.6248820e-002 1.9872200e-002 +v -3.9222000e-002 3.6326200e-002 1.0789900e-002 +v -3.0307100e-002 3.3995400e-002 1.4706400e-002 +v 2.0081230e-002 3.5172700e-002 2.8018770e-002 +v 2.4989010e-002 3.8104580e-002 2.9429570e-002 +v 3.3584130e-002 3.8303930e-002 2.2928670e-002 +v 4.9015720e-002 4.4573630e-002 2.0659450e-002 +v -5.8225970e-002 6.6607310e-002 3.5050280e-002 +v -6.7330830e-002 3.3846440e-002 8.7266300e-003 +v -3.4692330e-002 3.3828710e-002 1.2438580e-002 +v -2.9803200e-002 3.4287000e-002 1.6353100e-002 +v 1.7023800e-003 3.6310890e-002 2.1179600e-002 +v 4.5137020e-002 4.4625440e-002 2.5516510e-002 +v -6.8876490e-002 1.1022176e-001 3.9004630e-002 +v -5.7680560e-002 3.3622690e-002 1.4040310e-002 +v -5.3210500e-002 3.3585300e-002 1.3987000e-002 +v -3.5711600e-002 3.5891600e-002 1.5502900e-002 +v -2.8861500e-002 3.5396700e-002 1.7350000e-002 +v -2.6580500e-002 3.7742600e-002 1.5705300e-002 +v -1.0974400e-003 3.8147840e-002 2.0427010e-002 +v 3.5047710e-002 4.0973940e-002 2.6970390e-002 +v -6.9685460e-002 3.4478780e-002 9.7984300e-003 +v -5.4019000e-002 3.3309900e-002 1.5848000e-002 +v 4.4816800e-003 3.7117830e-002 2.4755300e-002 +v 6.6605500e-003 3.5204730e-002 2.4315930e-002 +v 8.3833000e-003 3.4748700e-002 2.4057310e-002 +v 3.8883100e-002 4.1032980e-002 2.4976570e-002 +v -2.6441900e-003 3.8727070e-002 2.5131260e-002 +v 3.2222300e-003 3.8708440e-002 2.5898750e-002 +v 9.0016500e-003 3.6890930e-002 2.8482190e-002 +v 1.3196980e-002 3.4835790e-002 3.1630980e-002 +v 2.2291600e-002 3.7053310e-002 3.3101020e-002 +v 2.8948390e-002 3.9160020e-002 2.7234810e-002 +v -8.7773470e-002 1.1181412e-001 3.7144310e-002 +v -1.7870490e-002 3.8203890e-002 2.0243220e-002 +v 1.0087420e-002 3.7047690e-002 3.0822500e-002 +v 4.2296550e-002 4.5435770e-002 2.9040920e-002 +v -8.4341340e-002 1.1388013e-001 4.6513480e-002 +v -7.3795710e-002 1.0895629e-001 3.9217250e-002 +v -5.1243340e-002 6.4239200e-002 3.4258040e-002 +v -6.1777390e-002 3.4017860e-002 1.6900580e-002 +v -3.6665100e-002 3.5304200e-002 2.3032000e-002 +v -1.4930180e-002 3.8643510e-002 2.9378330e-002 +v -8.0894520e-002 1.0967225e-001 3.7910230e-002 +v -8.9822620e-002 1.1387199e-001 3.2845310e-002 +v -6.9655510e-002 6.8728370e-002 3.1127880e-002 +v -7.8449800e-002 1.0988832e-001 4.2517920e-002 +v -7.5824140e-002 1.0794900e-001 3.7128750e-002 +v -5.5740630e-002 3.4128050e-002 2.6674360e-002 +v -3.8279600e-002 3.5429000e-002 2.4380600e-002 +v -3.5283340e-002 3.4179780e-002 2.2744860e-002 +v -2.5798070e-002 3.7865000e-002 1.9981460e-002 +v 6.9064300e-003 3.9004270e-002 2.9548510e-002 +v 1.5448990e-002 3.4852440e-002 3.6984890e-002 +v 1.9128230e-002 3.5640640e-002 3.6642280e-002 +v -6.3664970e-002 6.6047840e-002 3.1828080e-002 +v 3.9604800e-002 4.4939530e-002 2.9992360e-002 +v -8.0294310e-002 7.1702430e-002 1.5995300e-002 +v -5.4185430e-002 6.7322700e-002 3.6935610e-002 +v -7.3110210e-002 1.4847168e-001 -2.8748470e-002 +v -5.8999980e-002 7.3751550e-002 4.1197080e-002 +v -5.9520730e-002 6.1040260e-002 -2.3753800e-003 +v -6.2791800e-002 3.4596760e-002 2.3505640e-002 +v -4.1895500e-002 3.3668300e-002 2.6940000e-002 +v 8.9808200e-003 3.7639400e-002 3.3900800e-002 +v 8.5287800e-003 3.4888000e-002 3.6265100e-002 +v -8.9803890e-002 1.1498106e-001 4.2771650e-002 +v -6.5545420e-002 7.4430370e-002 3.9168070e-002 +v -6.4644190e-002 6.1723230e-002 2.2552000e-004 +v 5.2496900e-003 3.9507100e-002 3.3271200e-002 +v 2.0250320e-002 3.7033170e-002 3.9327190e-002 +v -6.7006400e-002 6.3292870e-002 -1.7493900e-003 +v -6.4479770e-002 6.0651470e-002 4.2343200e-003 +v -5.7219630e-002 5.7000470e-002 4.9175800e-003 +v -7.4362810e-002 7.2437050e-002 3.1430040e-002 +v -6.2019000e-002 3.4343180e-002 3.1883280e-002 +v -4.6870820e-002 3.4444130e-002 3.0513130e-002 +v -2.0814280e-002 3.8400960e-002 2.7868430e-002 +v 1.6439350e-002 3.5635110e-002 4.1281040e-002 +v -6.9087160e-002 1.1205014e-001 4.5320060e-002 +v -7.1811570e-002 1.4861318e-001 -3.4639490e-002 +v -6.9538770e-002 6.3074750e-002 3.5758200e-003 +v -8.4863890e-002 7.8392100e-002 1.6462010e-002 +v -9.1188780e-002 1.1588893e-001 2.4705540e-002 +v -8.8827760e-002 1.1359169e-001 2.3873640e-002 +v -7.1302830e-002 1.1325363e-001 4.9444530e-002 +v -5.4876950e-002 7.0282330e-002 3.8828200e-002 +v -7.7208880e-002 1.0715887e-001 3.4738290e-002 +v -6.1241780e-002 5.9007440e-002 8.0916600e-003 +v -6.5885650e-002 3.5025080e-002 2.9416520e-002 +v -5.7889430e-002 3.4419570e-002 3.6265760e-002 +v -5.1847710e-002 3.4470270e-002 3.4635180e-002 +v -3.4834600e-002 3.4721400e-002 3.4578200e-002 +v -3.0984700e-002 3.8191900e-002 3.2390100e-002 +v -4.9613100e-003 3.9364900e-002 3.6702200e-002 +v 1.2224170e-002 3.5177480e-002 4.2620580e-002 +v -7.4898220e-002 1.1458863e-001 5.0776480e-002 +v -8.0469100e-002 1.1357963e-001 4.6643440e-002 +v -7.4107560e-002 6.9586030e-002 2.7264400e-002 +v -7.9002620e-002 7.6339320e-002 2.9248090e-002 +v -6.5297080e-002 3.4778970e-002 3.3744340e-002 +v -3.3656400e-002 3.4344100e-002 3.6914100e-002 +v 4.9318500e-003 3.4814800e-002 4.3462110e-002 +v 1.1347440e-002 3.6213020e-002 4.4652280e-002 +v -6.0569260e-002 7.1154540e-002 3.8653760e-002 +v -8.8979470e-002 1.1450869e-001 2.8446030e-002 +v -6.8543520e-002 6.1090480e-002 1.0557760e-002 +v -8.2710960e-002 1.1648975e-001 4.8518530e-002 +v -4.1913210e-002 3.4467720e-002 3.3200040e-002 +v -1.1289800e-002 3.9529200e-002 3.8844100e-002 +v -2.8261900e-003 3.4885340e-002 4.5611410e-002 +v -6.4561210e-002 5.9484140e-002 1.3061680e-002 +v -5.8581440e-002 5.7801460e-002 1.3429540e-002 +v -2.3320000e-002 3.9169500e-002 3.8473300e-002 +v -1.8159900e-002 3.9322300e-002 3.9402900e-002 +v -1.6471400e-002 3.4812800e-002 4.3684700e-002 +v 3.2906600e-003 3.5833470e-002 4.6024610e-002 +v -8.5229630e-002 1.1200712e-001 3.0416940e-002 +v -8.5644730e-002 1.1131719e-001 3.4234780e-002 +v -7.4530360e-002 6.6680690e-002 4.6953300e-003 +v -7.1112970e-002 6.2751470e-002 8.7995500e-003 +v -6.1149380e-002 5.8834410e-002 1.6539440e-002 +v -4.6912270e-002 3.4627180e-002 3.9739710e-002 +v -4.0760350e-002 3.4668230e-002 4.0492530e-002 +v -2.6323100e-002 3.4658000e-002 4.3473500e-002 +v -3.1836600e-003 3.6229910e-002 4.7873100e-002 +v -7.9940490e-002 1.0916678e-001 3.4119800e-002 +v -5.9712170e-002 6.3165280e-002 2.8789180e-002 +v -5.1176600e-002 6.8061880e-002 3.7398330e-002 +v -5.0126580e-002 7.0933150e-002 3.9481010e-002 +v -7.2790130e-002 6.4399880e-002 1.5205950e-002 +v -6.8511230e-002 6.1214650e-002 1.5354080e-002 +v -3.9343210e-002 3.5440180e-002 4.2492560e-002 +v -8.1305900e-003 3.5008350e-002 4.7502400e-002 +v -6.6080670e-002 7.0202740e-002 3.5552860e-002 +v -6.8602600e-002 1.4992277e-001 -4.0051350e-002 +v -7.1722100e-002 6.7023040e-002 2.4959750e-002 +v -7.5115010e-002 6.6557040e-002 1.0244090e-002 +v -6.5146650e-002 3.5945650e-002 3.9775080e-002 +v -3.6898600e-002 3.5924640e-002 4.4794170e-002 +v -9.4780400e-003 3.5977600e-002 4.9434210e-002 +v -8.5175960e-002 1.1706809e-001 4.8139420e-002 +v -6.3366400e-002 6.2790260e-002 2.5647610e-002 +v -6.6633330e-002 6.1001700e-002 1.8101240e-002 +v -5.8167590e-002 5.9985190e-002 2.2606060e-002 +v -6.4212210e-002 3.4992560e-002 3.9401920e-002 +v -5.3425790e-002 3.4560020e-002 4.2782420e-002 +v -1.8031490e-002 3.4859970e-002 4.9264760e-002 +v -1.1440410e-002 3.7640770e-002 5.0275730e-002 +v -7.5165320e-002 1.1154286e-001 4.6707180e-002 +v -7.7168390e-002 6.9826450e-002 5.0605600e-003 +v -7.2801360e-002 6.4382590e-002 1.2089080e-002 +v -7.8022000e-002 7.0995160e-002 2.1322150e-002 +v -6.1263370e-002 3.4690410e-002 4.1994900e-002 +v -5.4403750e-002 3.5007310e-002 4.4874590e-002 +v -4.5754280e-002 3.5206980e-002 4.3518120e-002 +v -3.3832440e-002 3.5168820e-002 4.6957890e-002 +v -2.8657630e-002 3.5083380e-002 5.0549440e-002 +v -1.5306440e-002 3.5246410e-002 5.0133810e-002 +v -6.5283650e-002 1.5592447e-001 -4.9865930e-002 +v -6.6467860e-002 1.4871539e-001 -3.1579300e-002 +v -6.2095980e-002 1.6388324e-001 -5.8385930e-002 +v -6.3274890e-002 1.5245731e-001 -3.2221730e-002 +v -4.3755720e-002 1.4773408e-001 -2.1433200e-003 +v -6.5696940e-002 1.4561631e-001 -1.8974710e-002 +v -6.6713650e-002 1.5358824e-001 -4.9097100e-002 +v -1.0482810e-002 1.6668287e-001 -2.1746090e-002 +v -6.2744510e-002 1.6397531e-001 -5.9398280e-002 +v -7.0413230e-002 1.4129200e-001 -8.4590800e-003 +v -6.1530380e-002 1.4037628e-001 -6.2734700e-003 +v -1.1452460e-002 1.7220633e-001 -2.6844980e-002 +v -6.3731140e-002 1.6577037e-001 -6.0103610e-002 +v -2.8218820e-002 1.5758144e-001 -1.0999490e-002 +v -1.8471270e-002 1.5967716e-001 -1.1169510e-002 +v -6.6700710e-002 1.5236775e-001 -4.5266390e-002 +v -4.9896410e-002 1.4670859e-001 -1.8614200e-003 +v -3.1449640e-002 1.5460463e-001 -7.6802300e-003 +v -6.7447660e-002 1.5507675e-001 -5.1594250e-002 +v -1.0906650e-002 1.7649301e-001 -2.9246300e-002 +v -7.2083600e-002 1.4965550e-001 -3.9265860e-002 +v -6.4230830e-002 1.4877806e-001 -2.5899710e-002 +v -6.3056640e-002 1.4341650e-001 -7.4907700e-003 +v -5.3043350e-002 1.4092550e-001 -4.7408000e-004 +v -3.9269410e-002 1.5205232e-001 -6.6203800e-003 +v -6.4796930e-002 1.5210615e-001 -3.6185520e-002 +v -6.4400320e-002 1.5834400e-001 -5.4256370e-002 +v -6.6178120e-002 1.4218350e-001 -9.3766300e-003 +v -6.7751430e-002 1.4605207e-001 -2.3333300e-002 +v -6.4731580e-002 1.5410067e-001 -4.0464820e-002 +v -2.4265590e-002 1.5687690e-001 -7.8509300e-003 +v -1.5723180e-002 1.6312344e-001 -1.6396570e-002 +v -7.0887660e-002 1.4404618e-001 -1.4908480e-002 +v -4.4341830e-002 1.5113809e-001 -5.6859800e-003 +v -6.2896810e-002 1.4694778e-001 -1.3098620e-002 +v -6.3755400e-002 1.4428875e-001 -1.1395730e-002 +v -6.8214560e-002 1.4390932e-001 -1.4984170e-002 +v -5.0271440e-002 1.4336563e-001 1.5153000e-003 +v -2.8535590e-002 1.6208479e-001 -1.4786030e-002 +v -6.5810700e-002 1.4359119e-001 -1.2585380e-002 +v -5.6179200e-002 1.3774406e-001 -4.0674300e-003 +v -6.8866880e-002 1.4723338e-001 -2.8739870e-002 +v -6.0965420e-002 1.7002113e-001 -6.0839390e-002 +v -1.3895490e-002 1.6787168e-001 -2.1897230e-002 +v -6.9413000e-002 1.5121847e-001 -4.4538540e-002 +v -5.5039800e-002 5.7309700e-002 1.6990900e-002 +f 1069 1647 1578 +f 1058 909 939 +f 421 1176 238 +f 1055 1101 1042 +f 238 1059 1126 +f 1254 30 1261 +f 1065 1071 1 +f 1037 1130 1120 +f 1570 2381 1585 +f 2434 2502 2473 +f 1632 1654 1646 +f 1144 1166 669 +f 1202 1440 305 +f 1071 1090 1 +f 1555 1570 1584 +f 1184 1174 404 +f 65 432 12 +f 1032 1085 574 +f 1789 2207 2223 +f 1154 1118 1184 +f 1141 1086 1154 +f 99 1117 342 +f 404 1174 419 +f 489 2000 1998 +f 1118 1174 1184 +f 1196 403 136 +f 1495 717 1490 +f 1804 402 1207 +f 2272 1398 891 +f 1100 1002 804 +f 1596 1595 2381 +f 208 420 1207 +f 402 208 1207 +f 1455 1935 1925 +f 1176 1059 238 +f 1150 1040 348 +f 1957 1537 2051 +f 1124 1189 939 +f 1804 1207 1823 +f 1381 1300 1109 +f 383 384 1182 +f 1085 1086 1141 +f 1040 1046 132 +f 220 1495 1188 +f 420 261 1207 +f 261 420 1065 +f 1055 1133 1101 +f 1054 421 403 +f 182 1109 2 +f 1181 1207 320 +f 545 1570 1561 +f 35 342 432 +f 1024 574 1141 +f 432 342 12 +f 1489 1081 1547 +f 1181 320 1805 +f 1516 1683 1507 +f 357 1117 1047 +f 1561 1570 1555 +f 1090 1196 1206 +f 1047 1203 1051 +f 1165 202 1121 +f 1099 341 301 +f 1174 240 419 +f 922 921 833 +f 1121 1080 385 +f 815 21 1183 +f 35 99 342 +f 1083 398 262 +f 106 94 1317 +f 94 292 1317 +f 292 95 1317 +f 940 1039 1033 +f 1300 1306 433 +f 21 212 471 +f 1120 1131 1037 +f 833 921 688 +f 1117 357 342 +f 106 271 94 +f 386 227 1375 +f 1130 1044 1053 +f 419 240 219 +f 1255 1244 32 +f 1557 1081 1489 +f 2062 2120 2109 +f 2034 2110 430 +f 23 315 1111 +f 291 94 271 +f 291 292 94 +f 50 386 95 +f 964 734 665 +f 1616 1585 1611 +f 445 1084 402 +f 574 1085 1141 +f 1654 341 1653 +f 220 1188 1640 +f 342 69 12 +f 417 261 328 +f 292 50 95 +f 204 227 386 +f 50 204 386 +f 1276 1471 1311 +f 1206 1196 136 +f 1033 1055 1042 +f 1037 1044 1130 +f 1180 320 417 +f 1121 202 1080 +f 325 203 271 +f 291 76 292 +f 292 237 50 +f 2159 1696 1767 +f 583 929 850 +f 1584 1585 1616 +f 1495 1490 1188 +f 1557 1489 1660 +f 1078 1069 1494 +f 1972 1992 1971 +f 183 1226 2000 +f 325 429 203 +f 292 76 237 +f 1152 227 1143 +f 1488 1412 1489 +f 1638 1646 1653 +f 1947 1869 2468 +f 203 306 291 +f 306 76 291 +f 237 248 50 +f 204 1143 227 +f 2395 14 429 +f 1502 881 2500 +f 1 1090 202 +f 1652 1653 1099 +f 2117 1863 2496 +f 50 248 204 +f 160 792 994 +f 884 888 857 +f 544 2117 2496 +f 1090 1206 202 +f 2463 879 2492 +f 429 306 203 +f 498 188 418 +f 865 884 857 +f 994 998 1014 +f 884 897 888 +f 1795 948 1802 +f 208 1035 1071 +f 1065 1 1066 +f 377 435 1377 +f 304 429 14 +f 304 306 429 +f 73 60 74 +f 248 592 204 +f 846 2264 829 +f 897 912 906 +f 1004 991 992 +f 1422 1421 1233 +f 980 10 303 +f 1058 922 909 +f 2436 2449 2418 +f 394 435 377 +f 435 475 446 +f 475 474 446 +f 336 337 361 +f 338 235 372 +f 624 148 129 +f 812 306 596 +f 1726 992 1019 +f 945 1514 1511 +f 1069 1627 1628 +f 1812 1823 1181 +f 1165 1121 169 +f 447 475 435 +f 2487 2458 901 +f 42 59 46 +f 401 7 187 +f 1010 970 797 +f 1513 220 1640 +f 2474 2491 2462 +f 594 307 1014 +f 398 1513 1640 +f 307 594 1026 +f 545 2381 1570 +f 403 421 238 +f 445 402 127 +f 1611 1631 1616 +f 1805 1180 1148 +f 394 447 435 +f 2341 2413 2376 +f 75 74 60 +f 541 47 42 +f 47 59 42 +f 541 42 28 +f 917 931 1103 +f 897 906 883 +f 2484 2068 779 +f 888 883 857 +f 261 1065 328 +f 363 1307 349 +f 377 363 394 +f 444 747 464 +f 323 338 362 +f 92 116 74 +f 592 634 97 +f 982 1027 1004 +f 1020 982 1004 +f 1084 1054 1035 +f 208 402 1084 +f 421 1119 1176 +f 1207 1181 1823 +f 1179 1187 1160 +f 263 296 1343 +f 1298 296 1307 +f 1307 296 349 +f 405 363 349 +f 405 394 363 +f 405 447 394 +f 362 372 384 +f 338 372 362 +f 983 1004 987 +f 122 134 139 +f 415 440 414 +f 75 92 74 +f 226 186 246 +f 796 787 700 +f 1119 1059 1176 +f 122 114 91 +f 624 129 116 +f 641 558 631 +f 1311 1318 1487 +f 100 1162 1170 +f 1653 341 1099 +f 1316 1983 273 +f 263 277 296 +f 296 358 349 +f 436 447 405 +f 109 554 570 +f 504 1385 2501 +f 115 122 91 +f 2068 2460 779 +f 43 777 163 +f 378 405 349 +f 358 378 349 +f 448 447 436 +f 448 476 447 +f 78 77 108 +f 75 60 47 +f 1764 2481 1795 +f 717 714 1512 +f 1490 717 1501 +f 238 1126 168 +f 1878 1866 826 +f 2025 2360 2367 +f 251 278 263 +f 278 277 263 +f 277 318 296 +f 296 318 358 +f 318 350 358 +f 378 436 405 +f 384 372 1182 +f 454 440 415 +f 987 1004 992 +f 493 476 448 +f 323 788 338 +f 403 238 136 +f 1565 1503 1474 +f 297 277 278 +f 297 318 277 +f 358 350 378 +f 378 388 436 +f 476 493 500 +f 73 105 60 +f 323 337 312 +f 953 1573 2358 +f 142 161 119 +f 454 443 440 +f 1862 1871 1405 +f 297 319 318 +f 560 47 541 +f 170 1323 111 +f 357 1047 1050 +f 1119 98 1059 +f 1838 1877 1900 +f 2359 230 251 +f 350 364 378 +f 449 448 436 +f 449 493 448 +f 185 186 226 +f 443 469 479 +f 874 165 2480 +f 463 444 464 +f 64 105 91 +f 1182 440 1129 +f 1958 1651 2502 +f 1238 2034 191 +f 251 279 278 +f 278 279 297 +f 364 388 378 +f 483 493 449 +f 134 148 139 +f 244 268 259 +f 910 942 930 +f 105 115 91 +f 24 30 18 +f 1132 487 1059 +f 1869 1947 2021 +f 2497 2494 2463 +f 2359 2385 230 +f 230 280 251 +f 251 280 279 +f 279 308 297 +f 297 308 319 +f 319 364 318 +f 364 350 318 +f 388 395 436 +f 436 395 449 +f 493 472 500 +f 122 129 134 +f 125 142 124 +f 373 400 393 +f 24 557 30 +f 2264 2278 2251 +f 1261 30 1269 +f 1730 1862 1877 +f 252 280 230 +f 343 364 319 +f 364 343 388 +f 63 64 91 +f 399 393 416 +f 416 444 463 +f 162 189 142 +f 768 373 326 +f 189 661 177 +f 189 199 661 +f 847 887 864 +f 533 747 444 +f 1744 1022 1418 +f 1170 524 729 +f 121 1342 128 +f 1236 1244 26 +f 280 281 279 +f 281 308 279 +f 343 319 308 +f 343 365 388 +f 388 365 395 +f 365 406 395 +f 406 449 395 +f 483 477 493 +f 477 491 472 +f 493 477 472 +f 78 109 77 +f 166 174 196 +f 481 150 814 +f 63 59 64 +f 326 373 393 +f 643 260 43 +f 230 253 252 +f 449 441 483 +f 441 477 483 +f 415 416 463 +f 226 246 245 +f 464 470 454 +f 323 362 337 +f 52 37 1283 +f 253 281 252 +f 281 280 252 +f 309 308 281 +f 330 343 308 +f 366 365 343 +f 441 449 406 +f 464 814 15 +f 883 906 887 +f 337 362 371 +f 479 498 290 +f 247 746 1003 +f 25 37 557 +f 640 930 669 +f 2486 2499 2459 +f 309 330 308 +f 343 330 366 +f 441 437 477 +f 290 498 418 +f 124 119 108 +f 77 124 108 +f 589 125 109 +f 570 589 109 +f 125 162 142 +f 1045 433 1034 +f 1207 261 320 +f 2004 2474 2495 +f 1215 1228 2285 +f 365 396 406 +f 396 422 406 +f 422 437 441 +f 406 422 441 +f 59 47 60 +f 51 78 66 +f 361 371 383 +f 196 215 214 +f 463 454 415 +f 27 41 535 +f 53 1283 37 +f 84 1299 1283 +f 1805 320 1180 +f 254 253 222 +f 254 281 253 +f 309 366 330 +f 396 365 366 +f 456 477 437 +f 484 491 477 +f 2480 2485 2493 +f 418 188 187 +f 53 85 1283 +f 85 84 1283 +f 420 1071 1065 +f 264 281 254 +f 298 309 281 +f 368 366 367 +f 368 396 366 +f 1639 1564 1139 +f 560 48 47 +f 82 471 212 +f 25 38 37 +f 202 1206 1080 +f 264 298 281 +f 298 331 309 +f 309 331 366 +f 331 367 366 +f 396 368 422 +f 422 456 437 +f 491 1192 313 +f 1699 2064 1710 +f 462 443 479 +f 371 362 384 +f 2502 2476 2464 +f 371 384 383 +f 21 732 212 +f 1571 1629 1627 +f 38 39 53 +f 37 38 53 +f 39 85 53 +f 1173 1184 404 +f 1006 2142 1674 +f 201 255 254 +f 255 264 254 +f 368 407 422 +f 450 456 422 +f 450 484 456 +f 456 484 477 +f 314 1192 491 +f 2027 2501 2489 +f 2475 2471 2488 +f 551 492 732 +f 464 481 814 +f 1081 1494 1547 +f 201 231 255 +f 407 450 422 +f 484 494 491 +f 494 327 491 +f 327 314 491 +f 876 797 995 +f 847 856 829 +f 125 143 162 +f 134 129 148 +f 1564 1571 1627 +f 417 320 261 +f 328 1065 1066 +f 170 156 201 +f 156 231 201 +f 231 282 255 +f 282 264 255 +f 450 485 484 +f 484 485 494 +f 2463 2486 2479 +f 159 185 167 +f 492 68 212 +f 732 492 212 +f 68 82 212 +f 1311 1471 1296 +f 101 156 111 +f 332 264 282 +f 332 298 264 +f 332 331 298 +f 331 332 367 +f 407 423 450 +f 450 423 485 +f 804 1002 1443 +f 2484 779 946 +f 689 443 462 +f 440 689 1129 +f 166 167 174 +f 38 31 39 +f 112 145 101 +f 101 145 156 +f 156 256 231 +f 332 423 368 +f 367 332 368 +f 368 423 407 +f 946 779 920 +f 1432 1261 1449 +f 461 478 453 +f 464 15 470 +f 31 54 39 +f 39 54 85 +f 86 101 85 +f 145 210 156 +f 282 283 332 +f 283 369 332 +f 369 423 332 +f 423 408 485 +f 854 876 965 +f 78 108 66 +f 440 443 689 +f 374 2465 961 +f 929 519 979 +f 54 86 85 +f 156 241 256 +f 256 282 231 +f 256 283 282 +f 389 423 369 +f 389 408 423 +f 408 457 485 +f 457 49 485 +f 485 49 494 +f 494 135 327 +f 175 83 314 +f 1167 1140 1483 +f 196 174 215 +f 697 16 68 +f 1038 82 16 +f 140 117 141 +f 1654 1653 1646 +f 1234 54 31 +f 86 112 101 +f 210 241 156 +f 923 917 911 +f 697 34 16 +f 145 193 210 +f 256 265 283 +f 265 310 283 +f 283 310 369 +f 310 344 369 +f 344 370 369 +f 370 389 369 +f 409 408 389 +f 409 466 408 +f 466 457 408 +f 466 49 457 +f 49 135 494 +f 174 225 215 +f 1014 766 602 +f 826 2220 2215 +f 1078 1494 1081 +f 1273 70 86 +f 120 112 86 +f 146 145 112 +f 146 193 145 +f 265 256 241 +f 223 265 241 +f 486 49 466 +f 175 327 135 +f 105 122 115 +f 480 15 681 +f 225 234 215 +f 731 34 697 +f 86 54 1273 +f 70 120 86 +f 193 241 210 +f 299 310 265 +f 310 333 344 +f 344 351 370 +f 424 466 409 +f 135 49 175 +f 214 215 234 +f 48 75 47 +f 34 9 1038 +f 16 34 1038 +f 203 291 271 +f 9 558 754 +f 1195 397 1120 +f 120 146 112 +f 146 194 193 +f 266 265 223 +f 266 299 265 +f 299 333 310 +f 333 351 344 +f 382 383 392 +f 399 416 415 +f 266 333 299 +f 351 352 370 +f 424 486 466 +f 487 175 49 +f 7 117 187 +f 1182 414 440 +f 41 42 46 +f 290 289 497 +f 2502 2464 2473 +f 372 399 414 +f 1570 1585 1584 +f 1066 1 1165 +f 1 202 1165 +f 120 70 102 +f 157 146 120 +f 194 223 193 +f 223 241 193 +f 352 379 370 +f 370 379 389 +f 410 409 389 +f 2478 1409 1958 +f 806 945 1002 +f 157 194 146 +f 267 266 223 +f 267 333 266 +f 379 410 389 +f 410 438 409 +f 438 424 409 +f 190 205 143 +f 337 371 361 +f 2215 830 826 +f 1631 1646 1638 +f 102 157 120 +f 157 195 194 +f 195 223 194 +f 195 211 223 +f 223 211 267 +f 267 300 333 +f 300 334 351 +f 333 300 351 +f 351 334 352 +f 410 411 438 +f 438 486 424 +f 487 49 486 +f 875 594 989 +f 108 581 66 +f 225 245 244 +f 312 336 335 +f 151 754 107 +f 274 1386 300 +f 352 334 379 +f 923 1729 1096 +f 244 245 268 +f 463 464 454 +f 414 399 415 +f 15 480 470 +f 1647 1069 1078 +f 909 922 833 +f 387 417 328 +f 133 157 102 +f 1314 133 102 +f 133 195 157 +f 1148 1179 1160 +f 1046 1167 182 +f 379 411 410 +f 792 339 229 +f 391 7 668 +f 185 226 174 +f 461 290 497 +f 2027 504 2501 +f 1196 1054 403 +f 728 1019 752 +f 2459 2483 2461 +f 1291 1264 55 +f 133 1356 195 +f 195 1356 211 +f 412 438 411 +f 4 486 438 +f 458 4 438 +f 4 487 486 +f 1720 1572 1771 +f 245 275 268 +f 1869 2021 2059 +f 235 399 372 +f 64 60 105 +f 836 2492 879 +f 1315 133 1314 +f 1331 1382 1356 +f 1310 926 1128 +f 7 1121 117 +f 119 161 611 +f 380 379 334 +f 379 380 411 +f 467 4 458 +f 495 487 4 +f 495 1126 487 +f 416 400 533 +f 479 469 498 +f 74 116 73 +f 478 461 497 +f 393 400 416 +f 61 1291 55 +f 505 1999 2474 +f 1999 2491 2474 +f 199 189 36 +f 1164 1165 169 +f 1179 387 249 +f 390 411 380 +f 411 390 412 +f 458 438 412 +f 495 168 1126 +f 480 469 470 +f 116 122 105 +f 418 187 140 +f 185 174 167 +f 166 148 167 +f 470 469 443 +f 40 55 32 +f 61 71 1291 +f 71 103 1291 +f 1184 1173 1154 +f 634 514 97 +f 425 458 412 +f 917 923 931 +f 2472 2489 853 +f 754 641 567 +f 44 567 1163 +f 454 470 443 +f 40 32 1249 +f 33 40 1249 +f 56 55 40 +f 56 61 55 +f 451 1265 439 +f 1180 417 1179 +f 1099 301 1077 +f 1189 1058 939 +f 1059 221 1132 +f 598 1074 1075 +f 412 426 425 +f 650 186 185 +f 234 244 259 +f 226 245 225 +f 1033 1042 1030 +f 2492 836 247 +f 7 169 1121 +f 1462 1322 1482 +f 425 467 458 +f 496 4 467 +f 1751 2468 2480 +f 290 418 140 +f 326 789 762 +f 142 177 161 +f 165 1751 2480 +f 87 103 71 +f 103 87 104 +f 1180 1179 1148 +f 417 387 1179 +f 2081 2060 2031 +f 1154 1173 1141 +f 181 131 197 +f 442 425 426 +f 614 144 143 +f 876 1010 797 +f 40 45 56 +f 56 45 61 +f 87 71 61 +f 1563 1437 1590 +f 1121 385 117 +f 1148 1160 1137 +f 1449 1459 1439 +f 1028 2462 929 +f 442 459 425 +f 459 467 425 +f 168 495 4 +f 496 168 4 +f 1763 1403 1444 +f 140 187 117 +f 244 234 225 +f 246 740 269 +f 372 414 1182 +f 40 547 45 +f 45 62 61 +f 62 87 61 +f 87 88 104 +f 1084 517 1054 +f 387 328 1064 +f 2467 2497 2485 +f 286 1363 302 +f 205 189 162 +f 290 140 289 +f 214 234 224 +f 393 399 809 +f 315 1131 397 +f 302 321 353 +f 1164 169 391 +f 427 459 442 +f 217 496 467 +f 217 168 496 +f 978 969 2074 +f 361 383 382 +f 269 276 245 +f 1440 11 305 +f 62 88 87 +f 328 1066 1064 +f 1066 1165 1164 +f 242 287 302 +f 1363 242 302 +f 287 321 302 +f 1179 249 1187 +f 983 1020 1004 +f 464 747 481 +f 788 323 276 +f 269 245 246 +f 88 89 1325 +f 171 172 242 +f 360 353 321 +f 360 1354 353 +f 1057 1064 1164 +f 2184 2188 2183 +f 460 459 451 +f 460 467 459 +f 149 168 217 +f 149 136 168 +f 116 129 122 +f 109 124 77 +f 159 167 148 +f 28 42 41 +f 57 88 62 +f 45 57 62 +f 1336 1325 89 +f 89 72 1336 +f 147 172 171 +f 172 258 242 +f 258 257 242 +f 257 287 242 +f 257 321 287 +f 345 360 321 +f 360 381 1354 +f 1069 938 1655 +f 387 473 249 +f 270 217 467 +f 130 136 149 +f 851 847 829 +f 983 987 975 +f 189 177 142 +f 88 72 89 +f 184 258 172 +f 257 288 321 +f 1265 451 459 +f 270 149 217 +f 226 225 174 +f 27 28 41 +f 109 125 124 +f 547 57 45 +f 57 58 88 +f 88 58 72 +f 2476 2484 2458 +f 147 184 172 +f 184 213 258 +f 258 243 257 +f 243 288 257 +f 345 321 288 +f 391 169 7 +f 468 460 451 +f 468 488 460 +f 270 467 460 +f 488 270 460 +f 1206 136 130 +f 481 793 150 +f 143 205 162 +f 142 119 124 +f 58 90 72 +f 90 128 72 +f 147 173 184 +f 173 213 184 +f 213 233 258 +f 258 233 243 +f 354 360 345 +f 354 381 360 +f 1026 991 307 +f 268 312 259 +f 1206 130 1080 +f 116 105 73 +f 139 148 166 +f 275 312 268 +f 188 401 187 +f 2479 2459 2461 +f 58 63 90 +f 1064 1066 1164 +f 1064 473 387 +f 288 311 345 +f 311 354 345 +f 996 994 307 +f 452 468 439 +f 452 478 468 +f 478 488 468 +f 141 130 149 +f 1564 1639 1563 +f 547 41 57 +f 2081 2107 2060 +f 382 381 354 +f 497 270 488 +f 289 149 270 +f 289 141 149 +f 114 122 139 +f 59 60 64 +f 275 323 312 +f 401 668 7 +f 41 46 57 +f 57 46 58 +f 1459 1345 1269 +f 1342 121 158 +f 166 173 158 +f 213 224 233 +f 233 259 243 +f 243 322 288 +f 322 311 288 +f 453 478 452 +f 497 289 270 +f 912 911 906 +f 276 323 275 +f 276 275 245 +f 46 63 58 +f 90 121 128 +f 173 214 213 +f 213 214 224 +f 259 322 243 +f 336 311 322 +f 336 354 311 +f 361 382 354 +f 1043 439 1290 +f 497 488 478 +f 385 130 141 +f 385 1080 130 +f 144 190 143 +f 535 41 547 +f 121 166 158 +f 335 336 322 +f 354 336 361 +f 2004 2481 1764 +f 698 439 1043 +f 289 140 141 +f 923 1096 931 +f 650 185 159 +f 46 59 63 +f 63 91 90 +f 90 114 121 +f 121 139 166 +f 173 196 214 +f 259 335 322 +f 2478 2502 2434 +f 312 337 336 +f 90 91 114 +f 114 139 121 +f 166 196 173 +f 224 234 233 +f 234 259 233 +f 259 312 335 +f 1124 916 1189 +f 542 541 530 +f 462 479 290 +f 269 783 276 +f 813 567 641 +f 276 783 788 +f 82 1038 1333 +f 816 701 703 +f 672 137 603 +f 625 635 624 +f 2457 2439 1973 +f 767 533 529 +f 2468 1869 2480 +f 662 190 639 +f 711 720 719 +f 630 639 614 +f 161 654 638 +f 781 991 982 +f 1227 31 516 +f 648 639 630 +f 630 614 590 +f 2098 544 1899 +f 578 579 586 +f 697 492 551 +f 529 533 400 +f 869 859 870 +f 1732 924 914 +f 1004 1027 991 +f 801 591 603 +f 636 676 651 +f 876 949 965 +f 2207 1789 1859 +f 76 739 237 +f 188 681 15 +f 578 604 599 +f 797 616 995 +f 510 2035 1365 +f 76 812 617 +f 617 739 76 +f 1468 93 1765 +f 596 546 812 +f 1457 1305 1477 +f 760 197 150 +f 671 773 765 +f 586 609 604 +f 591 700 632 +f 476 2312 474 +f 2084 2027 2489 +f 582 590 571 +f 1555 2449 1996 +f 674 546 596 +f 812 655 617 +f 161 177 661 +f 599 604 636 +f 700 787 576 +f 776 675 572 +f 776 674 675 +f 617 634 739 +f 591 632 649 +f 612 546 674 +f 617 655 634 +f 728 752 706 +f 571 2311 2305 +f 775 674 776 +f 775 612 674 +f 612 628 546 +f 546 628 812 +f 812 628 655 +f 620 630 615 +f 620 648 630 +f 667 653 646 +f 810 782 785 +f 150 197 814 +f 534 1517 2000 +f 702 572 2378 +f 748 776 572 +f 655 613 634 +f 911 917 905 +f 648 679 662 +f 727 771 713 +f 750 807 799 +f 639 190 144 +f 662 679 200 +f 702 748 572 +f 775 776 748 +f 628 718 655 +f 626 658 645 +f 791 778 790 +f 612 811 628 +f 613 514 634 +f 1380 1756 1673 +f 570 590 614 +f 720 741 719 +f 1074 795 835 +f 614 639 144 +f 612 775 811 +f 718 735 655 +f 655 735 613 +f 798 338 788 +f 636 652 676 +f 571 590 555 +f 528 730 687 +f 690 702 2312 +f 476 690 2312 +f 811 718 628 +f 721 778 727 +f 748 702 690 +f 735 686 613 +f 1517 2002 2127 +f 654 685 667 +f 569 588 606 +f 513 531 538 +f 538 549 548 +f 549 553 548 +f 550 588 549 +f 1903 869 870 +f 691 775 748 +f 691 600 775 +f 600 811 775 +f 811 563 718 +f 563 736 718 +f 718 736 735 +f 736 647 735 +f 735 647 686 +f 686 745 613 +f 745 514 613 +f 569 606 605 +f 654 667 638 +f 851 857 847 +f 588 569 549 +f 690 691 748 +f 680 514 745 +f 2127 2002 2094 +f 747 701 481 +f 400 373 529 +f 600 536 811 +f 536 563 811 +f 1306 227 1152 +f 522 24 18 +f 523 24 522 +f 865 857 851 +f 2031 2060 1540 +f 767 701 747 +f 618 652 609 +f 652 636 609 +f 573 22 710 +f 642 699 730 +f 1522 1518 2476 +f 500 629 691 +f 690 500 691 +f 691 629 600 +f 780 644 641 +f 579 578 561 +f 131 668 197 +f 197 668 814 +f 789 809 798 +f 622 760 150 +f 621 563 536 +f 673 745 686 +f 673 818 745 +f 818 680 745 +f 680 96 514 +f 2495 2462 1028 +f 1028 583 575 +f 663 794 664 +f 629 761 600 +f 761 757 600 +f 600 757 536 +f 621 696 563 +f 755 736 563 +f 696 755 563 +f 633 736 755 +f 633 647 736 +f 623 686 647 +f 633 623 647 +f 686 623 673 +f 819 680 818 +f 680 819 96 +f 1729 1677 1096 +f 2482 1899 2471 +f 537 536 757 +f 536 537 621 +f 673 819 818 +f 2428 222 230 +f 25 24 523 +f 25 557 24 +f 38 25 19 +f 710 22 272 +f 663 759 794 +f 1120 878 1195 +f 537 696 621 +f 696 633 755 +f 822 2215 2220 +f 97 96 1053 +f 750 784 743 +f 887 905 864 +f 768 784 373 +f 512 513 548 +f 573 664 22 +f 696 715 633 +f 673 521 819 +f 2454 2453 2445 +f 883 887 847 +f 306 812 76 +f 642 528 759 +f 798 809 235 +f 994 792 998 +f 587 626 586 +f 1900 1918 1937 +f 645 652 618 +f 537 786 696 +f 521 593 819 +f 515 19 523 +f 741 749 719 +f 789 326 809 +f 539 581 550 +f 657 777 723 +f 684 713 660 +f 692 712 720 +f 652 666 692 +f 507 761 629 +f 472 507 629 +f 507 757 761 +f 623 633 673 +f 724 521 673 +f 515 516 19 +f 304 675 674 +f 178 778 721 +f 947 1447 2358 +f 626 645 618 +f 586 626 618 +f 784 768 742 +f 753 537 757 +f 537 753 786 +f 724 981 521 +f 521 981 593 +f 979 559 850 +f 637 660 677 +f 787 631 576 +f 141 117 385 +f 809 399 235 +f 641 754 558 +f 542 553 561 +f 742 768 762 +f 444 416 533 +f 528 687 796 +f 813 598 566 +f 1490 1501 1557 +f 753 757 507 +f 786 715 696 +f 633 724 673 +f 2090 2062 2109 +f 646 653 660 +f 660 694 683 +f 677 660 683 +f 1872 839 838 +f 1224 18 30 +f 326 393 809 +f 799 529 373 +f 313 507 472 +f 715 774 633 +f 974 699 841 +f 703 820 816 +f 692 711 676 +f 1014 355 766 +f 875 752 1019 +f 627 646 660 +f 711 692 720 +f 652 692 676 +f 799 373 784 +f 813 566 567 +f 2462 2482 2475 +f 764 644 780 +f 1479 1924 1916 +f 753 738 786 +f 738 607 786 +f 786 607 715 +f 715 524 774 +f 633 774 724 +f 559 979 672 +f 758 798 783 +f 683 694 705 +f 820 703 562 +f 764 687 644 +f 744 743 725 +f 313 753 507 +f 607 524 715 +f 664 801 22 +f 646 627 610 +f 800 820 562 +f 750 769 807 +f 767 747 533 +f 578 586 604 +f 862 593 981 +f 688 2382 1083 +f 306 304 674 +f 738 584 607 +f 168 136 238 +f 773 552 765 +f 2473 2464 2458 +f 773 793 552 +f 626 619 658 +f 1007 1139 1013 +f 562 529 799 +f 744 750 743 +f 659 683 693 +f 677 683 659 +f 313 737 753 +f 753 737 738 +f 607 729 524 +f 27 518 28 +f 553 569 580 +f 657 163 777 +f 580 569 605 +f 789 798 758 +f 769 562 807 +f 820 671 816 +f 638 646 611 +f 1074 598 644 +f 750 799 784 +f 1931 907 898 +f 2483 2487 2461 +f 737 584 738 +f 1439 1438 1431 +f 2098 1213 544 +f 48 578 75 +f 796 631 787 +f 815 732 21 +f 581 588 550 +f 625 636 651 +f 778 1011 810 +f 693 705 725 +f 693 683 705 +f 236 1921 1966 +f 584 729 607 +f 2237 1866 2227 +f 530 541 28 +f 237 739 248 +f 512 530 28 +f 727 778 771 +f 684 727 713 +f 2237 2220 826 +f 542 561 560 +f 528 796 700 +f 808 785 671 +f 739 592 248 +f 895 905 896 +f 740 246 186 +f 272 137 979 +f 770 769 744 +f 712 742 720 +f 1213 2026 544 +f 1888 1235 2438 +f 555 554 2311 +f 737 313 1192 +f 1585 1612 1611 +f 695 721 685 +f 518 17 28 +f 769 770 562 +f 719 749 740 +f 648 669 679 +f 773 657 723 +f 606 637 619 +f 2072 2062 2042 +f 606 619 626 +f 549 569 553 +f 161 638 611 +f 910 917 942 +f 917 1103 942 +f 991 1026 992 +f 979 137 672 +f 785 163 657 +f 710 2488 2472 +f 611 581 119 +f 808 671 820 +f 1820 1900 1870 +f 759 700 591 +f 637 677 619 +f 2494 2490 2463 +f 671 765 816 +f 687 764 780 +f 1019 992 1026 +f 1726 1719 987 +f 713 771 694 +f 51 2355 78 +f 510 526 525 +f 525 526 1249 +f 526 33 1249 +f 2311 554 2335 +f 827 848 840 +f 603 591 649 +f 758 269 740 +f 1595 1612 1586 +f 1694 1048 1699 +f 682 740 186 +f 22 801 603 +f 555 570 554 +f 1053 110 97 +f 615 582 601 +f 814 668 188 +f 725 705 744 +f 528 700 759 +f 640 648 620 +f 703 701 562 +f 886 892 582 +f 631 731 576 +f 1087 1835 1747 +f 882 864 895 +f 956 950 1103 +f 1502 2500 2470 +f 205 190 200 +f 815 878 616 +f 616 878 995 +f 1183 878 815 +f 1601 1827 881 +f 527 535 526 +f 2184 2183 2175 +f 1142 1125 1133 +f 235 338 798 +f 160 339 792 +f 599 92 75 +f 598 1116 566 +f 631 558 731 +f 771 770 744 +f 730 528 642 +f 841 699 642 +f 668 401 188 +f 510 527 526 +f 749 758 740 +f 706 721 695 +f 694 726 705 +f 694 744 726 +f 906 911 905 +f 661 695 161 +f 708 815 616 +f 535 547 33 +f 794 759 591 +f 778 808 790 +f 269 758 783 +f 771 744 694 +f 800 808 820 +f 571 886 582 +f 854 948 1010 +f 906 905 887 +f 625 651 635 +f 2000 1226 534 +f 2140 1504 2016 +f 601 620 615 +f 620 601 640 +f 648 640 669 +f 698 452 439 +f 671 785 657 +f 1561 2356 545 +f 685 653 667 +f 685 727 684 +f 568 616 797 +f 708 732 815 +f 93 229 339 +f 865 851 839 +f 942 1103 950 +f 589 614 125 +f 606 610 627 +f 951 834 873 +f 92 599 625 +f 1878 830 1902 +f 2482 2098 1899 +f 568 708 616 +f 708 551 732 +f 2434 2487 2483 +f 160 964 665 +f 2316 2391 2309 +f 762 758 749 +f 570 614 589 +f 888 897 883 +f 2000 1517 1388 +f 685 721 727 +f 588 610 606 +f 653 685 684 +f 651 650 635 +f 760 1151 6 +f 793 622 150 +f 651 676 650 +f 744 769 750 +f 541 542 560 +f 476 500 690 +f 473 1064 1057 +f 561 578 560 +f 636 625 599 +f 876 995 949 +f 829 856 846 +f 682 704 740 +f 791 790 770 +f 2466 2500 2460 +f 579 587 586 +f 1352 1208 1095 +f 1684 1479 1916 +f 604 609 636 +f 751 721 706 +f 810 608 782 +f 672 603 649 +f 475 447 476 +f 794 591 801 +f 682 186 650 +f 808 800 790 +f 644 598 813 +f 704 719 740 +f 1011 608 810 +f 1192 584 737 +f 687 780 796 +f 2337 474 2312 +f 638 667 646 +f 706 1186 728 +f 733 575 568 +f 595 551 708 +f 595 540 551 +f 1308 501 1852 +f 665 339 160 +f 527 2447 535 +f 558 9 731 +f 723 793 773 +f 660 713 694 +f 693 725 666 +f 562 767 529 +f 550 538 531 +f 2267 2287 2233 +f 996 964 160 +f 2068 2470 2466 +f 704 711 719 +f 741 762 749 +f 605 606 626 +f 548 542 530 +f 995 878 709 +f 1898 1684 1916 +f 778 791 771 +f 782 163 785 +f 789 758 762 +f 857 883 847 +f 733 970 1028 +f 838 829 825 +f 2447 511 535 +f 22 603 137 +f 705 726 744 +f 605 587 580 +f 512 548 530 +f 743 784 742 +f 790 800 770 +f 778 810 808 +f 1014 998 355 +f 708 568 595 +f 656 697 551 +f 540 656 551 +f 143 125 614 +f 1000 1020 983 +f 778 178 1011 +f 676 704 682 +f 637 627 660 +f 606 627 637 +f 701 552 481 +f 808 810 785 +f 590 570 555 +f 716 595 568 +f 2355 2335 554 +f 912 1729 911 +f 1076 1456 1546 +f 697 68 492 +f 676 711 704 +f 839 851 838 +f 1028 575 733 +f 1020 844 982 +f 716 568 575 +f 844 781 982 +f 1238 2156 2034 +f 553 580 561 +f 580 579 561 +f 452 461 453 +f 560 578 48 +f 564 540 595 +f 632 656 540 +f 564 632 540 +f 75 578 599 +f 518 27 535 +f 511 518 535 +f 783 798 788 +f 642 759 663 +f 720 742 741 +f 605 626 587 +f 580 587 579 +f 725 712 666 +f 562 701 767 +f 1729 923 911 +f 712 743 742 +f 619 677 658 +f 161 695 654 +f 770 800 562 +f 2084 2489 2472 +f 575 559 716 +f 716 564 595 +f 654 695 685 +f 843 855 2064 +f 34 731 9 +f 527 510 1973 +f 723 622 793 +f 992 1726 987 +f 693 666 652 +f 2472 853 573 +f 624 159 148 +f 671 657 773 +f 681 188 498 +f 797 970 733 +f 565 656 632 +f 565 697 656 +f 565 731 697 +f 1949 951 920 +f 85 111 84 +f 662 200 190 +f 44 324 754 +f 33 547 40 +f 658 693 652 +f 658 652 645 +f 664 794 801 +f 666 712 692 +f 639 648 662 +f 611 646 610 +f 850 559 575 +f 1447 2490 1106 +f 1972 1955 1935 +f 582 615 590 +f 66 581 539 +f 780 641 631 +f 796 780 631 +f 1049 1192 83 +f 1348 13 1519 +f 799 807 562 +f 581 611 588 +f 687 795 644 +f 663 8 642 +f 1936 1972 1935 +f 650 676 682 +f 615 630 590 +f 730 795 687 +f 742 762 741 +f 548 553 542 +f 1048 1692 1074 +f 658 659 693 +f 37 52 30 +f 611 610 588 +f 649 632 564 +f 565 576 731 +f 2138 922 1058 +f 1204 854 965 +f 725 743 712 +f 644 813 641 +f 660 653 684 +f 771 791 770 +f 644 795 1074 +f 469 480 681 +f 559 672 564 +f 716 559 564 +f 672 649 564 +f 2161 1378 2171 +f 474 475 476 +f 816 765 701 +f 765 552 701 +f 513 538 548 +f 754 324 107 +f 609 586 618 +f 25 523 19 +f 677 659 658 +f 689 452 698 +f 1334 1115 1353 +f 700 565 632 +f 700 576 565 +f 481 552 793 +f 763 901 2458 +f 550 549 538 +f 781 964 996 +f 1596 1634 1595 +f 198 916 1124 +f 198 1124 341 +f 842 973 1025 +f 842 1025 836 +f 1009 1024 934 +f 573 710 2472 +f 1100 971 1002 +f 1501 1081 1557 +f 1225 1219 955 +f 413 2138 284 +f 955 1630 522 +f 341 1124 301 +f 2333 2376 2350 +f 1107 218 284 +f 398 925 1513 +f 1513 1442 1495 +f 1935 1455 1744 +f 1723 1935 1744 +f 825 1872 838 +f 1495 1442 1496 +f 963 1024 1009 +f 1511 1514 966 +f 1775 1729 912 +f 688 262 1067 +f 714 1007 1512 +f 919 1732 914 +f 2319 2331 2304 +f 2400 2407 2391 +f 1674 2164 1780 +f 843 927 899 +f 1660 988 1188 +f 1067 262 1640 +f 1381 1109 1483 +f 1437 1381 1483 +f 2495 1010 948 +f 1514 1289 1313 +f 899 374 961 +f 1438 1430 1422 +f 1634 1095 1632 +f 2487 973 2461 +f 1003 499 874 +f 849 848 827 +f 1430 1462 1453 +f 2496 2084 2471 +f 909 10 980 +f 730 927 835 +f 2031 1540 1536 +f 831 849 2178 +f 881 834 951 +f 1841 1722 1803 +f 1005 670 1020 +f 1021 670 1005 +f 1869 2059 2467 +f 903 902 1939 +f 2476 2502 1651 +f 853 8 573 +f 1850 831 2178 +f 934 746 247 +f 934 65 746 +f 301 285 1077 +f 968 944 977 +f 970 2495 1028 +f 974 2465 374 +f 899 927 374 +f 1882 1898 1916 +f 1613 1634 1596 +f 909 833 1396 +f 2492 247 1003 +f 919 914 1931 +f 1459 1299 1458 +f 1634 1632 1633 +f 844 670 228 +f 2494 2497 2467 +f 901 973 2487 +f 228 1772 734 +f 1701 1709 1666 +f 963 574 1024 +f 847 864 856 +f 1730 1736 2239 +f 870 859 848 +f 2074 2111 2103 +f 1140 1590 1483 +f 927 730 974 +f 2103 978 2074 +f 756 1745 1718 +f 848 859 840 +f 1296 1482 1320 +f 2331 51 66 +f 1067 988 962 +f 1396 833 1445 +f 1001 1005 1000 +f 901 1009 973 +f 1099 1077 817 +f 933 944 936 +f 952 958 1828 +f 988 1660 986 +f 833 1067 1445 +f 1067 1640 988 +f 218 413 284 +f 1843 180 347 +f 1846 1708 1798 +f 2469 2477 855 +f 1006 1021 1005 +f 381 382 250 +f 2369 828 531 +f 968 977 1001 +f 2460 1949 779 +f 1194 1441 1115 +f 1001 1000 968 +f 756 678 1745 +f 963 1009 901 +f 2471 2084 2472 +f 841 642 8 +f 982 991 1027 +f 670 844 1020 +f 1289 1514 945 +f 869 904 890 +f 1161 1115 1639 +f 823 2178 849 +f 746 12 499 +f 263 428 2366 +f 1685 1075 1692 +f 1002 926 806 +f 1799 1755 216 +f 944 968 993 +f 943 944 993 +f 31 38 19 +f 531 828 550 +f 1501 1078 1081 +f 1921 1149 431 +f 936 943 932 +f 1660 1489 1412 +f 301 980 285 +f 903 918 902 +f 869 890 868 +f 890 903 867 +f 1003 746 499 +f 951 1949 2500 +f 990 841 853 +f 1595 1634 1611 +f 374 927 974 +f 836 1025 247 +f 1653 1652 1638 +f 1303 1545 1142 +f 1616 1631 1638 +f 1629 1546 1628 +f 936 932 913 +f 513 506 531 +f 868 890 867 +f 2330 2369 2353 +f 924 918 914 +f 907 914 904 +f 1258 1421 1267 +f 301 939 980 +f 1472 1482 1296 +f 868 867 859 +f 472 491 313 +f 272 519 2488 +f 1471 1472 1296 +f 1025 934 247 +f 1634 1633 1611 +f 2176 1847 2177 +f 1310 1289 806 +f 924 933 918 +f 1969 1968 902 +f 2107 2128 2118 +f 1428 1436 1287 +f 1139 1564 1617 +f 2378 572 2384 +f 853 841 8 +f 2501 961 2465 +f 1221 1240 1408 +f 1069 1578 1627 +f 1006 1005 1001 +f 1617 1564 1578 +f 828 539 550 +f 1791 2168 2160 +f 1829 1718 1739 +f 1968 1939 902 +f 756 1718 665 +f 1998 2000 1388 +f 2451 545 2356 +f 178 997 1011 +f 1275 325 1270 +f 1709 872 1666 +f 2176 1959 1847 +f 944 943 936 +f 2424 518 511 +f 1445 1067 962 +f 2007 952 1828 +f 2052 2061 2081 +f 828 2303 539 +f 835 1699 1048 +f 1709 1706 872 +f 885 574 963 +f 1318 1296 1320 +f 859 867 1902 +f 1452 1448 1421 +f 943 993 976 +f 993 1000 983 +f 854 1010 876 +f 988 986 962 +f 2031 2052 2081 +f 924 1732 1828 +f 965 949 1060 +f 781 228 734 +f 1718 1765 665 +f 943 976 932 +f 1680 1794 1783 +f 1448 1471 1276 +f 1276 1267 1421 +f 1931 914 907 +f 991 781 996 +f 1276 1421 1448 +f 10 909 1396 +f 831 860 849 +f 1523 1762 1774 +f 924 1828 937 +f 307 994 1014 +f 946 963 901 +f 978 2103 977 +f 977 1006 1001 +f 1007 1161 1639 +f 1639 1294 1437 +f 885 1032 574 +f 1294 1381 1437 +f 733 568 797 +f 792 229 1112 +f 119 581 108 +f 843 835 927 +f 1889 860 831 +f 2211 2216 2204 +f 2400 2431 2422 +f 2103 1006 977 +f 840 1902 830 +f 827 840 830 +f 827 830 822 +f 1003 874 2492 +f 1432 1439 1431 +f 781 734 964 +f 1937 1936 1723 +f 918 913 902 +f 958 977 944 +f 1850 2178 2177 +f 1005 1020 1000 +f 991 996 307 +f 1396 1445 340 +f 2179 1763 889 +f 939 909 980 +f 1828 958 937 +f 978 977 958 +f 1590 1571 1563 +f 779 1949 920 +f 1551 1362 1573 +f 2103 2142 1006 +f 920 885 963 +f 946 920 963 +f 1584 1616 1583 +f 1453 1472 1452 +f 1647 1617 1578 +f 1578 1564 1627 +f 1628 938 1069 +f 869 868 859 +f 993 983 976 +f 912 1762 1775 +f 752 751 706 +f 1628 1546 938 +f 844 228 781 +f 840 859 1902 +f 898 907 904 +f 1025 973 1009 +f 663 664 573 +f 763 946 901 +f 898 904 869 +f 2172 889 1763 +f 1128 926 971 +f 860 848 849 +f 904 903 890 +f 2486 2459 2479 +f 577 782 608 +f 933 936 918 +f 2177 1847 1851 +f 665 1765 339 +f 937 958 944 +f 894 981 724 +f 968 1000 993 +f 2192 2195 2205 +f 1652 1099 817 +f 997 608 1011 +f 997 577 608 +f 577 163 782 +f 1112 998 792 +f 2177 1851 1850 +f 1257 1421 1258 +f 951 873 920 +f 822 830 2215 +f 1899 2496 2471 +f 1773 1668 1558 +f 904 914 903 +f 932 1671 913 +f 873 885 920 +f 1013 1617 1647 +f 873 1032 885 +f 894 862 981 +f 2469 855 961 +f 913 1671 1969 +f 2477 2064 855 +f 918 936 913 +f 860 870 848 +f 937 944 933 +f 1501 1013 1647 +f 824 178 751 +f 824 997 178 +f 824 577 997 +f 643 163 577 +f 863 856 882 +f 2128 2153 2134 +f 722 774 880 +f 722 894 774 +f 864 905 895 +f 850 575 583 +f 914 918 903 +f 924 937 933 +f 1501 717 1013 +f 1587 1324 928 +f 717 1512 1013 +f 602 577 824 +f 766 643 577 +f 894 709 862 +f 709 878 862 +f 976 975 932 +f 1324 1596 928 +f 880 524 1060 +f 2434 2459 2499 +f 1324 1613 1596 +f 752 824 751 +f 602 766 577 +f 1014 602 594 +f 1387 1226 2152 +f 2153 1387 2152 +f 669 930 950 +f 1710 1694 1699 +f 768 326 762 +f 582 892 601 +f 974 990 2465 +f 624 116 625 +f 835 795 730 +f 2458 2484 763 +f 989 602 824 +f 2064 2477 1710 +f 976 983 975 +f 949 722 880 +f 996 160 994 +f 2305 863 556 +f 556 863 886 +f 601 910 640 +f 2264 825 829 +f 989 824 752 +f 856 864 882 +f 1595 1586 2381 +f 1627 1629 1628 +f 2174 2180 2173 +f 2128 2134 2118 +f 137 272 22 +f 949 880 1060 +f 995 894 722 +f 894 995 709 +f 894 724 774 +f 886 895 892 +f 640 910 930 +f 871 870 860 +f 846 856 863 +f 1026 875 1019 +f 838 851 829 +f 1024 1171 934 +f 36 189 205 +f 863 882 886 +f 886 882 895 +f 875 1026 594 +f 52 1459 1269 +f 896 917 910 +f 1025 1009 934 +f 949 995 722 +f 2152 1226 1636 +f 895 896 892 +f 892 910 601 +f 942 950 930 +f 875 989 752 +f 594 602 989 +f 766 355 643 +f 355 260 643 +f 905 917 896 +f 965 1060 1162 +f 892 896 910 +f 1101 1052 1042 +f 1029 1031 834 +f 1101 1133 1118 +f 342 357 376 +f 516 515 2454 +f 1656 2494 2467 +f 1056 1303 1133 +f 1120 1130 862 +f 69 342 376 +f 1055 1056 1133 +f 499 69 165 +f 85 101 111 +f 1031 1032 834 +f 200 679 1166 +f 1031 1042 1032 +f 1171 65 934 +f 1822 1204 1177 +f 1096 956 1103 +f 514 96 97 +f 956 1145 1144 +f 1185 1166 1144 +f 1145 1185 1144 +f 1185 200 1166 +f 375 132 1041 +f 1153 1202 305 +f 32 1244 1249 +f 1096 1087 956 +f 554 78 2355 +f 1191 138 110 +f 65 35 432 +f 1087 1110 956 +f 1110 1146 956 +f 956 1146 1145 +f 1146 1156 1145 +f 1145 1156 1185 +f 950 956 1144 +f 2481 2495 948 +f 1156 1193 1185 +f 1050 1047 1051 +f 239 151 107 +f 1185 1193 36 +f 1747 1110 1087 +f 1134 1146 1110 +f 1146 1157 1156 +f 1156 1157 1193 +f 1041 1045 1034 +f 1397 1134 1110 +f 1157 1146 1134 +f 1157 1175 1193 +f 1193 199 36 +f 1090 1035 1196 +f 1456 1150 1051 +f 1175 199 1193 +f 1186 695 199 +f 1186 199 1175 +f 1175 1157 1134 +f 728 1186 1175 +f 197 760 6 +f 1130 593 862 +f 1167 1109 182 +f 1194 1115 1161 +f 2140 1928 1504 +f 921 922 2138 +f 1147 1134 1397 +f 1719 1147 1397 +f 1147 1175 1134 +f 1175 1147 728 +f 341 1654 1208 +f 754 151 9 +f 284 2138 1058 +f 1188 1557 1660 +f 1191 110 1053 +f 916 284 1189 +f 284 1058 1189 +f 2094 1465 2127 +f 1726 1019 1147 +f 1147 1019 728 +f 593 1130 96 +f 239 305 1038 +f 1036 1131 315 +f 397 1131 1120 +f 1053 96 1130 +f 2467 2485 1869 +f 517 1089 421 +f 834 1827 1029 +f 419 1047 1117 +f 1034 433 1306 +f 2239 1862 1730 +f 1453 1462 1472 +f 1408 1422 1399 +f 471 23 1111 +f 1205 1150 1456 +f 1205 1040 1150 +f 1131 1036 293 +f 293 1068 1044 +f 375 1041 138 +f 1205 1140 1046 +f 1040 1205 1046 +f 1140 1167 1046 +f 1104 1049 83 +f 1052 1085 1032 +f 1044 1068 1191 +f 1167 1483 1109 +f 208 1084 1035 +f 1040 132 375 +f 1834 20 3 +f 1050 1051 1070 +f 1133 1125 1174 +f 11 1440 1401 +f 420 208 1071 +f 1135 1079 1094 +f 1086 1101 1118 +f 1029 1030 1031 +f 1200 1061 294 +f 1191 1068 138 +f 1171 1141 65 +f 1141 1172 65 +f 1172 35 65 +f 1172 404 35 +f 404 99 35 +f 221 1104 1063 +f 802 398 1083 +f 20 1089 3 +f 2064 1699 835 +f 1042 1052 1032 +f 1433 1261 1432 +f 1323 2338 155 +f 1076 1205 1456 +f 1088 1402 1056 +f 1150 348 1070 +f 1200 1089 20 +f 1097 1162 100 +f 1032 873 834 +f 21 471 1111 +f 294 1097 1104 +f 1072 100 584 +f 1151 760 622 +f 132 1045 1041 +f 1050 1070 1135 +f 1088 1039 940 +f 650 159 635 +f 100 1170 729 +f 729 584 100 +f 1103 931 1096 +f 925 1443 1513 +f 138 1102 110 +f 1034 1306 1152 +f 1071 1035 1090 +f 100 1072 1097 +f 23 1158 315 +f 1068 375 138 +f 1586 1612 1585 +f 1819 1030 1029 +f 1041 1034 1102 +f 232 375 1068 +f 348 1079 1070 +f 1061 1097 294 +f 1513 1443 1442 +f 1200 294 1119 +f 376 1050 1062 +f 1094 1036 315 +f 1200 1119 1089 +f 1111 1183 21 +f 1044 1191 1053 +f 698 295 689 +f 1079 232 1036 +f 404 1117 99 +f 1495 1496 717 +f 1119 294 98 +f 3 1089 517 +f 1132 1063 83 +f 1132 83 175 +f 132 1046 182 +f 1111 1195 1183 +f 1131 1044 1037 +f 127 402 1804 +f 219 1272 1047 +f 1697 1135 1094 +f 2140 1854 2117 +f 1111 397 1195 +f 1177 1162 1097 +f 1061 1177 1097 +f 717 1509 714 +f 2 1300 433 +f 462 290 461 +f 98 294 221 +f 294 1104 221 +f 714 1161 1007 +f 1073 1152 1143 +f 1697 1094 1360 +f 1223 1423 1218 +f 836 2479 842 +f 1097 1072 1049 +f 348 1040 375 +f 3 517 316 +f 180 1061 1201 +f 348 375 232 +f 1432 1431 1415 +f 220 1513 1495 +f 1104 1097 1049 +f 306 674 596 +f 777 455 723 +f 2170 2151 1641 +f 1047 419 219 +f 1102 1034 1073 +f 1073 1034 1152 +f 1035 1054 1196 +f 1177 1204 1162 +f 746 65 12 +f 751 178 721 +f 1054 517 421 +f 1051 1150 1070 +f 1102 1073 110 +f 998 1136 355 +f 567 566 1163 +f 1111 315 397 +f 1048 1074 835 +f 1158 1094 315 +f 1374 1107 1252 +f 1112 1136 998 +f 472 629 500 +f 355 1136 260 +f 260 118 43 +f 1104 83 1063 +f 376 357 1050 +f 1463 1142 1545 +f 1036 232 293 +f 1030 1042 1031 +f 1079 348 232 +f 221 1063 1132 +f 1094 1079 1036 +f 1076 1629 1205 +f 1136 1197 260 +f 260 1197 118 +f 1204 965 1162 +f 293 232 1068 +f 1590 1205 1629 +f 1205 1590 1140 +f 250 382 392 +f 1296 1318 1311 +f 347 1201 20 +f 1201 1200 20 +f 132 182 1045 +f 1101 1086 1052 +f 1033 1039 1055 +f 138 1041 1102 +f 970 1010 2495 +f 455 777 43 +f 1992 1948 2023 +f 20 1834 347 +f 1072 584 1049 +f 584 1192 1049 +f 182 2 1045 +f 1163 324 44 +f 1360 1094 1158 +f 1450 1360 1158 +f 1091 1112 229 +f 509 723 455 +f 207 509 455 +f 1251 1257 1266 +f 1488 1489 1547 +f 2157 1541 1875 +f 305 107 324 +f 1045 2 433 +f 1070 1079 1135 +f 1136 1168 1197 +f 1197 359 118 +f 118 359 43 +f 359 356 43 +f 356 455 43 +f 356 207 455 +f 1240 1422 1408 +f 1163 1153 324 +f 1201 1061 1200 +f 1052 1086 1085 +f 1024 1141 1171 +f 1112 1105 1136 +f 1050 1135 1062 +f 1105 1168 1136 +f 1168 1178 1197 +f 1197 1178 359 +f 1173 404 1172 +f 465 356 359 +f 1174 1125 240 +f 1240 1431 1422 +f 1098 1113 1105 +f 1112 1098 1105 +f 1105 1178 1168 +f 1178 465 359 +f 1091 1098 1112 +f 1133 1174 1118 +f 98 221 1059 +f 487 1132 175 +f 980 1017 285 +f 465 207 356 +f 180 1201 347 +f 1060 524 1170 +f 445 127 316 +f 1431 1438 1422 +f 498 469 681 +f 940 1807 1759 +f 381 250 1290 +f 1113 1122 1105 +f 1105 1122 1178 +f 1151 509 207 +f 1236 2035 525 +f 1131 293 1044 +f 346 207 465 +f 346 1151 207 +f 1822 1796 1204 +f 1143 204 97 +f 123 1128 971 +f 2153 2152 2134 +f 126 1151 346 +f 517 445 316 +f 1450 1158 23 +f 1458 1462 1430 +f 1129 152 1182 +f 1122 1159 1178 +f 1178 1198 465 +f 79 346 465 +f 126 1155 1151 +f 1151 1155 6 +f 295 1129 689 +f 1073 1143 97 +f 1098 1123 1113 +f 1113 1123 1122 +f 1123 1169 1122 +f 1178 1159 1198 +f 1198 79 465 +f 392 383 152 +f 1822 1061 180 +f 116 92 625 +f 421 1089 1119 +f 1129 295 152 +f 110 1073 97 +f 1173 1172 1141 +f 1122 1169 1159 +f 79 126 346 +f 1155 181 6 +f 971 926 1002 +f 295 1043 152 +f 1039 1088 1056 +f 1428 1266 1436 +f 404 419 1117 +f 836 879 2479 +f 2464 2476 2458 +f 1198 317 79 +f 1124 939 301 +f 44 754 567 +f 1039 1056 1055 +f 1439 1459 1458 +f 1660 1412 986 +f 1169 1160 1159 +f 179 1155 126 +f 1155 131 181 +f 1061 1822 1177 +f 1153 305 324 +f 175 314 327 +f 1160 1187 1159 +f 1159 1187 1198 +f 1198 1187 317 +f 79 179 126 +f 1043 250 392 +f 152 1043 392 +f 96 819 593 +f 1123 1127 1169 +f 317 179 79 +f 1057 1155 179 +f 1155 391 131 +f 131 391 668 +f 2381 1586 1585 +f 12 69 499 +f 262 398 1640 +f 2107 2118 2060 +f 2130 2094 2002 +f 1187 249 317 +f 1155 1057 391 +f 1290 439 1265 +f 305 239 107 +f 1127 1160 1169 +f 317 473 179 +f 473 1057 179 +f 83 1192 314 +f 1043 1290 250 +f 1807 940 1030 +f 517 1084 445 +f 1057 1164 391 +f 2492 2480 2493 +f 163 643 43 +f 1056 1545 1303 +f 1069 1655 1023 +f 249 473 317 +f 1162 1060 1170 +f 1086 1118 1154 +f 82 68 16 +f 1989 1990 1536 +f 1633 1632 1611 +f 1487 2372 1305 +f 1494 1069 1023 +f 1137 1160 1127 +f 669 1166 679 +f 390 1285 426 +f 1955 1972 1971 +f 1219 1223 2437 +f 1254 1261 1223 +f 1319 1545 1056 +f 1320 1328 2443 +f 1261 1433 1223 +f 1219 1254 1223 +f 254 222 2428 +f 1237 1290 1265 +f 1284 1273 1263 +f 1277 1291 1301 +f 1314 102 1301 +f 1280 363 377 +f 1313 1353 1514 +f 468 451 439 +f 1918 1964 1956 +f 2026 29 2140 +f 1354 381 1279 +f 1224 30 1254 +f 147 158 173 +f 1247 1253 274 +f 1271 380 334 +f 2043 2072 2042 +f 274 300 267 +f 1356 1392 211 +f 13 240 1142 +f 1382 1330 1392 +f 1312 1323 155 +f 240 1125 1142 +f 2358 1573 1362 +f 1236 1249 1244 +f 1272 219 1348 +f 1271 1274 380 +f 191 2034 1982 +f 1992 2052 1990 +f 462 452 689 +f 2262 2286 2261 +f 183 489 1642 +f 2485 2480 1869 +f 84 111 1323 +f 1190 353 1354 +f 446 434 435 +f 1336 171 1341 +f 2021 430 2059 +f 862 878 1120 +f 1263 1273 1248 +f 1966 1921 2144 +f 1312 84 1323 +f 240 13 1348 +f 1359 1274 1271 +f 1392 1330 1247 +f 1520 1333 11 +f 1368 1253 1247 +f 1279 1285 1190 +f 2465 990 2489 +f 1272 1519 805 +f 1369 1272 805 +f 1317 95 1344 +f 1242 1248 1234 +f 1368 242 1363 +f 274 1262 1386 +f 532 597 1886 +f 2117 2026 2140 +f 1392 1247 274 +f 2162 508 985 +f 1964 1469 1965 +f 1315 104 1331 +f 1392 1356 1382 +f 128 1342 1336 +f 1285 427 426 +f 1219 1224 1254 +f 1320 1322 1321 +f 1320 1321 1328 +f 153 2443 1328 +f 1321 153 1328 +f 1235 1244 1243 +f 1225 1224 1219 +f 1359 353 1190 +f 1312 1473 1458 +f 1336 1342 147 +f 305 1333 1038 +f 1336 147 171 +f 516 31 19 +f 2479 2461 842 +f 1237 1265 427 +f 1263 1278 1284 +f 881 1827 834 +f 1237 427 1285 +f 1299 1312 1458 +f 1190 1285 1274 +f 1363 286 1253 +f 2330 2303 828 +f 427 442 426 +f 2493 2463 2492 +f 1285 380 1274 +f 522 18 1225 +f 2471 2472 2488 +f 2338 154 1321 +f 1423 1415 1218 +f 1225 18 1224 +f 1253 286 1262 +f 286 353 1359 +f 171 1368 1383 +f 1273 54 1234 +f 1973 2447 527 +f 1322 155 1321 +f 1203 1369 1413 +f 1307 363 1298 +f 1364 1375 1329 +f 1329 227 1306 +f 296 1298 1343 +f 947 2499 1447 +f 1203 1047 1272 +f 1098 1748 1123 +f 1519 1272 1348 +f 1277 70 1273 +f 1282 1337 1361 +f 286 302 353 +f 103 104 1315 +f 1377 435 434 +f 1449 1261 1345 +f 926 1310 806 +f 1263 1248 1242 +f 985 508 597 +f 1415 1222 1218 +f 88 1325 104 +f 170 111 156 +f 1384 1282 1361 +f 274 1253 1262 +f 1371 1317 1344 +f 1371 1366 1337 +f 1345 1459 1449 +f 171 1383 1341 +f 2438 1235 1227 +f 2134 1582 2118 +f 428 1260 1379 +f 1336 1341 1325 +f 1235 1242 1227 +f 1228 1687 2284 +f 1854 2140 2016 +f 1866 1887 1873 +f 1343 1298 1370 +f 1384 1361 2440 +f 171 242 1368 +f 1344 1309 1366 +f 1371 1344 1366 +f 1280 1377 1293 +f 200 1185 205 +f 1330 1383 1368 +f 1255 1264 1263 +f 543 1367 1876 +f 1343 1370 1260 +f 1293 1326 1370 +f 2440 1361 1302 +f 1282 1384 2406 +f 271 1337 1282 +f 170 2338 1323 +f 1528 1503 2470 +f 515 1347 2453 +f 1997 1705 1998 +f 2285 1228 2284 +f 1229 1250 1228 +f 1330 1368 1247 +f 1919 1619 2045 +f 1344 1364 1335 +f 1222 1240 1221 +f 1212 858 1741 +f 2388 1222 1221 +f 1528 2470 2068 +f 501 1308 2171 +f 1295 1311 1487 +f 2116 1619 1655 +f 1220 1229 1228 +f 8 663 573 +f 1343 1260 428 +f 1337 1366 1361 +f 1298 1280 1293 +f 1269 1345 1261 +f 1279 381 1290 +f 1230 1229 1220 +f 1230 1245 1229 +f 1245 1250 1229 +f 1227 1234 31 +f 1302 1361 1350 +f 1245 1266 1428 +f 1992 2023 2052 +f 2482 2471 2475 +f 452 462 461 +f 271 1282 1275 +f 1991 1989 1934 +f 1366 1309 1350 +f 1344 1335 1309 +f 730 699 974 +f 1374 1252 1208 +f 597 508 1912 +f 1363 1253 1368 +f 1386 1271 300 +f 1211 1218 1222 +f 1376 1377 434 +f 2399 2437 1211 +f 1284 1291 1277 +f 1230 1251 1245 +f 1251 1266 1245 +f 1317 1371 1337 +f 1288 1286 1095 +f 1095 1286 1352 +f 1241 1208 1352 +f 1241 1374 1208 +f 1284 1278 1291 +f 211 1392 267 +f 1344 1375 1364 +f 929 583 1028 +f 1361 1366 1350 +f 1115 1294 1639 +f 1291 103 1301 +f 1220 1231 1230 +f 1231 1251 1230 +f 1234 1248 1273 +f 1255 55 1264 +f 1360 1450 1702 +f 363 1280 1298 +f 1369 1203 1272 +f 1415 1240 1222 +f 1216 1231 1220 +f 1243 1263 1235 +f 1375 227 1329 +f 1264 1278 1263 +f 855 899 961 +f 1286 1241 1352 +f 2081 2128 2107 +f 1223 1433 1423 +f 1473 1312 155 +f 154 153 1321 +f 1377 1376 1293 +f 1392 274 267 +f 334 300 1271 +f 1955 1991 1934 +f 1613 1327 1288 +f 1327 1286 1288 +f 1349 1374 1241 +f 2370 2025 2367 +f 1315 1331 133 +f 434 446 1256 +f 1232 1251 1231 +f 1243 1244 1255 +f 1286 1304 1241 +f 1349 1107 1374 +f 1359 1271 1386 +f 1227 516 2431 +f 219 240 1348 +f 1270 271 1275 +f 1255 1263 1243 +f 2026 1926 29 +f 1683 2157 1212 +f 1326 1293 1376 +f 1255 32 55 +f 104 1325 1341 +f 519 2462 2475 +f 2154 2161 2137 +f 1376 434 1246 +f 1246 434 1256 +f 1257 1251 1232 +f 1262 1359 1386 +f 2195 2192 2186 +f 1308 534 1226 +f 2026 2117 544 +f 1327 1613 1324 +f 1327 1326 1286 +f 1286 1326 1304 +f 104 1341 1331 +f 774 524 880 +f 837 1517 534 +f 1127 1123 1567 +f 1279 1237 1285 +f 1297 1381 1294 +f 1217 1232 1216 +f 1142 1519 13 +f 1436 1267 1287 +f 1324 1372 1327 +f 1304 1246 1241 +f 1246 1349 1241 +f 1246 1373 1349 +f 286 1359 1262 +f 1382 1383 1330 +f 1284 1277 1273 +f 489 1998 1799 +f 1675 1116 1075 +f 106 1317 1337 +f 1311 1295 1281 +f 1292 1364 1329 +f 1335 1364 1292 +f 1334 1294 1115 +f 1334 1297 1294 +f 1300 1381 1297 +f 973 842 2461 +f 1217 1239 1232 +f 1232 1239 1257 +f 1258 1267 1436 +f 1359 1190 1274 +f 1862 1405 1877 +f 1372 1339 1327 +f 1339 1326 1327 +f 1373 1351 1349 +f 1276 1311 1281 +f 1256 2386 1351 +f 2 1109 1300 +f 482 1731 520 +f 803 1604 2022 +f 1223 1218 1211 +f 1341 1383 1382 +f 1298 1293 1370 +f 1190 1354 1279 +f 1324 2398 1372 +f 1714 1700 2173 +f 183 2000 489 +f 1701 1666 192 +f 1227 1242 1234 +f 1332 1289 1310 +f 1517 2005 2130 +f 1331 1341 1382 +f 525 1249 1236 +f 23 1268 1450 +f 1264 1291 1278 +f 1281 1287 1267 +f 1295 1305 1287 +f 1281 1295 1287 +f 1487 1305 1295 +f 1605 2097 2058 +f 1326 1376 1304 +f 1304 1376 1246 +f 1316 1919 1984 +f 2500 1949 2460 +f 1332 1313 1289 +f 2189 2181 2177 +f 1335 1334 1353 +f 1292 1297 1334 +f 1428 1250 1245 +f 969 958 952 +f 1217 1233 1239 +f 1233 1257 1239 +f 1876 1367 1338 +f 1379 1260 1372 +f 1372 1260 1339 +f 1128 1302 1310 +f 1310 1302 1332 +f 1335 1353 1313 +f 1292 1334 1335 +f 1297 1329 1300 +f 1279 1290 1237 +f 1301 103 1314 +f 70 1301 102 +f 23 1333 1268 +f 380 1285 390 +f 772 325 1275 +f 1314 103 1315 +f 2473 2458 2487 +f 1276 1281 1267 +f 1344 95 1375 +f 2053 1771 1572 +f 1246 1256 1373 +f 1373 1256 1351 +f 1340 1302 1128 +f 1350 1313 1332 +f 1329 1297 1292 +f 2434 2473 2487 +f 106 1337 271 +f 23 471 1333 +f 622 723 509 +f 1388 1517 2127 +f 1991 1990 1989 +f 183 1636 1226 +f 2133 1605 2151 +f 1260 1370 1339 +f 1339 1370 1326 +f 867 1894 1902 +f 390 426 412 +f 1235 1263 1242 +f 1399 1422 1233 +f 305 11 1333 +f 1300 1329 1306 +f 1302 1350 1332 +f 1350 1309 1313 +f 1309 1335 1313 +f 2470 2102 1502 +f 1787 1531 1599 +f 1724 1725 1691 +f 1827 1601 1927 +f 1678 1358 1476 +f 1823 1812 1846 +f 1805 1824 1708 +f 1746 1676 1797 +f 325 2395 429 +f 1835 1677 1826 +f 1507 1790 1722 +f 1526 1672 858 +f 158 147 1342 +f 1462 1473 1322 +f 1474 1414 1565 +f 1761 1900 1877 +f 940 1759 1008 +f 1565 1015 1008 +f 1924 1533 1933 +f 1878 826 830 +f 1565 1414 1015 +f 1402 1088 1008 +f 1538 1532 1651 +f 1015 1552 1008 +f 1538 1591 1474 +f 1532 1538 1474 +f 1474 1591 1414 +f 1484 1402 1008 +f 1552 1484 1008 +f 1414 1460 1015 +f 1015 1460 1552 +f 806 1289 945 +f 1597 1538 1659 +f 1484 1319 1402 +f 1056 1402 1319 +f 1538 1597 1591 +f 1591 960 1414 +f 1414 960 1460 +f 1925 1466 1455 +f 1552 1400 1484 +f 1484 1400 1319 +f 1400 113 1319 +f 1597 1580 1591 +f 1460 1400 1552 +f 1514 1441 966 +f 1597 1659 1409 +f 1657 113 1400 +f 1460 1657 1400 +f 1288 1095 1634 +f 1551 1597 1409 +f 1580 1598 1591 +f 1591 1598 960 +f 1536 1990 2031 +f 960 1657 1460 +f 1809 1746 1797 +f 1423 1433 1432 +f 2478 1362 1409 +f 1463 1545 113 +f 1657 1463 113 +f 1457 1287 1305 +f 1682 1716 1746 +f 1434 1761 1885 +f 1013 1139 1617 +f 2379 1362 2478 +f 1420 1597 1551 +f 1420 1580 1597 +f 1664 1808 1712 +f 2256 2250 2231 +f 1362 1551 1409 +f 2196 2214 2213 +f 1691 1725 1777 +f 1626 192 1666 +f 1534 1574 2058 +f 1574 1600 1605 +f 1600 1606 1605 +f 1606 1641 1605 +f 1573 1420 1551 +f 1657 1485 1463 +f 678 1806 1742 +f 1534 1553 1574 +f 1574 1575 1600 +f 1810 2170 585 +f 1623 1641 1606 +f 1407 1657 960 +f 1598 1407 960 +f 1485 1142 1463 +f 1716 1581 1676 +f 1738 1743 1733 +f 843 2064 835 +f 1539 1575 1574 +f 1553 1539 1574 +f 1575 1592 1600 +f 1592 1624 1606 +f 1600 1592 1606 +f 1642 585 1641 +f 1623 1642 1641 +f 1485 164 1142 +f 1738 1516 1743 +f 1809 1720 1798 +f 1533 1535 1534 +f 1592 1607 1624 +f 1624 1623 1606 +f 1163 566 1116 +f 1407 1485 1657 +f 1432 1449 1439 +f 1100 802 2382 +f 1743 1516 1722 +f 1746 1716 1676 +f 1535 1539 1534 +f 1534 1539 1553 +f 1642 1623 1624 +f 1095 1208 1654 +f 967 1407 1598 +f 1580 967 1598 +f 1809 1797 1720 +f 1924 1524 1535 +f 1533 1924 1535 +f 1539 1576 1575 +f 1642 216 585 +f 1407 1529 1485 +f 1485 1529 164 +f 1472 1462 1482 +f 1415 1431 1240 +f 966 1194 714 +f 383 1182 152 +f 474 2337 446 +f 1743 1841 1757 +f 1486 1524 1924 +f 1535 1525 1539 +f 1575 1576 1592 +f 1420 967 1580 +f 1288 1634 1613 +f 459 427 1265 +f 1404 2179 1393 +f 1404 1403 1800 +f 1404 1410 1403 +f 1410 1749 1403 +f 1349 1351 218 +f 1486 1498 1524 +f 1535 1524 1525 +f 1607 1636 1624 +f 183 1642 1624 +f 1636 183 1624 +f 1107 1349 218 +f 1351 845 218 +f 164 1519 1142 +f 845 413 218 +f 1525 1576 1539 +f 1576 1582 1592 +f 1592 2134 1607 +f 2134 1636 1607 +f 2147 1491 1401 +f 1407 1589 1529 +f 1529 1519 164 +f 1693 1763 1444 +f 1924 1479 1486 +f 1592 1582 2134 +f 499 165 874 +f 2176 1857 1959 +f 2327 2368 2326 +f 2358 821 953 +f 953 821 1573 +f 1824 1704 1464 +f 1731 1358 1678 +f 1394 1410 1404 +f 1394 1418 1410 +f 1466 1479 1839 +f 1486 1479 1498 +f 1498 1525 1524 +f 1576 2080 1582 +f 1785 1684 1898 +f 804 398 802 +f 804 925 398 +f 1447 1562 2358 +f 2358 1562 821 +f 821 1620 1573 +f 1620 1420 1573 +f 1420 1556 967 +f 1393 1394 1404 +f 1525 2080 1576 +f 1621 1420 1620 +f 1621 1556 1420 +f 967 1589 1407 +f 1505 5 1357 +f 1266 1258 1436 +f 1393 1395 1394 +f 2176 2175 1848 +f 1455 1466 1839 +f 1525 1540 2080 +f 1582 2080 2118 +f 1100 804 802 +f 1556 1589 967 +f 1589 1082 1529 +f 1093 1685 1357 +f 1504 1093 1357 +f 1425 1418 1394 +f 1475 1479 1466 +f 1479 1506 1498 +f 1789 1784 1730 +f 2501 2465 2489 +f 1438 1458 1430 +f 1462 1458 1473 +f 1454 805 1529 +f 1082 1454 1529 +f 1529 805 1519 +f 1425 1394 1395 +f 1425 1744 1418 +f 1479 1475 1506 +f 1540 2060 2080 +f 1556 1082 1589 +f 1443 945 1511 +f 1506 1536 1498 +f 1498 1536 1525 +f 1525 1536 1540 +f 1670 852 1672 +f 1998 1388 1389 +f 1511 966 1509 +f 1509 966 714 +f 1442 1443 1496 +f 1562 1635 821 +f 155 1322 1473 +f 1439 1458 1438 +f 1426 1425 1395 +f 1475 1499 1506 +f 1735 1588 1776 +f 2422 2454 2421 +f 1423 1432 1415 +f 1559 2101 2073 +f 845 866 413 +f 1429 1620 821 +f 1620 1429 1621 +f 1228 1250 1687 +f 1002 945 1443 +f 2382 802 1083 +f 1859 1411 1395 +f 1411 1426 1395 +f 1426 1744 1425 +f 1590 1437 1483 +f 1480 1475 1466 +f 1480 1499 1475 +f 1510 1733 1743 +f 1663 1696 1658 +f 1430 1453 1452 +f 1452 1472 1471 +f 1452 1471 1448 +f 1430 1452 1421 +f 1430 1421 1422 +f 1429 1082 1556 +f 1621 1429 1556 +f 1351 2386 845 +f 1126 1059 487 +f 1639 1437 1563 +f 1504 1928 1093 +f 1499 1536 1506 +f 1588 1770 1727 +f 1110 1747 1397 +f 1776 1588 1531 +f 1322 1320 1482 +f 1590 1629 1571 +f 1730 1877 1838 +f 1429 935 1082 +f 1082 935 1454 +f 804 1443 925 +f 1139 1007 1639 +f 1925 1480 1466 +f 1934 1989 1480 +f 1499 1989 1536 +f 1727 1526 1531 +f 1593 1614 502 +f 2455 2431 2400 +f 1755 1680 908 +f 1563 1571 1564 +f 1647 1078 1501 +f 2490 1635 1106 +f 1496 1511 717 +f 2454 2431 516 +f 1478 1153 1093 +f 1870 1426 1411 +f 1426 1723 1744 +f 962 986 1412 +f 717 1511 1509 +f 1825 1704 1824 +f 2225 2234 2253 +f 1490 1557 1188 +f 1635 80 821 +f 805 1454 935 +f 1186 706 695 +f 1194 1161 714 +f 1512 1007 1013 +f 592 97 204 +f 1258 1266 1257 +f 82 1333 471 +f 1694 1710 1505 +f 1643 490 1661 +f 1661 490 1114 +f 1518 2068 2484 +f 1750 1808 1664 +f 1656 1635 2490 +f 935 1521 805 +f 1546 1629 1076 +f 1301 70 1277 +f 966 1441 1194 +f 1148 1825 1824 +f 1614 1609 1643 +f 1114 1092 1921 +f 1770 1739 1670 +f 1631 1632 1646 +f 821 1016 1429 +f 1429 1016 935 +f 1632 1095 1654 +f 1083 262 688 +f 1724 1686 1725 +f 1644 490 1643 +f 1092 1149 1921 +f 3 893 1832 +f 988 1640 1188 +f 916 1107 284 +f 1656 80 1635 +f 1016 821 80 +f 1016 1521 935 +f 1478 1202 1153 +f 1401 1928 29 +f 1440 1478 1928 +f 1849 1700 1865 +f 1595 1611 1612 +f 1208 198 341 +f 1464 1704 1746 +f 2143 984 1721 +f 1848 1849 1868 +f 1662 1114 490 +f 1669 1787 1682 +f 1656 1618 80 +f 198 1208 916 +f 1440 1928 1401 +f 1521 1369 805 +f 1252 1107 916 +f 1745 678 1672 +f 1703 1779 1721 +f 1750 1465 1808 +f 1609 1644 1643 +f 1092 1114 1662 +f 1826 1523 1793 +f 2262 2261 2224 +f 1696 2166 1767 +f 1016 1648 1521 +f 1208 1252 916 +f 833 688 1067 +f 1794 1803 1558 +f 28 17 512 +f 1750 861 1566 +f 1594 1644 1609 +f 1644 1645 490 +f 490 1645 1662 +f 2229 2262 2224 +f 1602 861 1760 +f 1530 1777 1760 +f 872 1706 1673 +f 1696 1668 2166 +f 1708 1809 1798 +f 1581 1716 1814 +f 1709 1794 1680 +f 1233 1421 1257 +f 1724 1476 1686 +f 1469 1481 1965 +f 1965 1481 1492 +f 2073 1549 1559 +f 1594 1615 1644 +f 1799 1706 1755 +f 1725 1686 1837 +f 1720 1797 1572 +f 1618 2467 2022 +f 1618 1579 80 +f 1648 1016 80 +f 2134 2152 1636 +f 1611 1632 1631 +f 1761 1434 1470 +f 1559 1577 1594 +f 1603 1615 1594 +f 1615 1645 1644 +f 1637 1662 1645 +f 1662 1199 1092 +f 1199 1149 1092 +f 1451 1108 1149 +f 665 734 756 +f 1865 1700 1714 +f 1709 1841 1794 +f 1618 2022 1579 +f 1648 1413 1369 +f 1521 1648 1369 +f 1520 11 1401 +f 1446 1470 1434 +f 1798 1691 1754 +f 2063 1544 2073 +f 2073 1544 1549 +f 1594 1577 1603 +f 1615 1637 1645 +f 1637 1199 1662 +f 1427 1149 1199 +f 2167 1108 1451 +f 1997 1673 1705 +f 1706 1799 1705 +f 1841 1709 1757 +f 1604 1579 2022 +f 1579 707 80 +f 80 707 1648 +f 1520 1401 1491 +f 1649 1520 1491 +f 1435 1434 1885 +f 1470 1469 1461 +f 1481 1508 2024 +f 2370 1544 2063 +f 1549 1568 1559 +f 1559 1568 1577 +f 1603 1610 1615 +f 1615 1610 1637 +f 999 1199 1637 +f 1451 1149 1427 +f 1137 1825 1148 +f 1706 1705 1673 +f 1138 1604 2116 +f 1138 1579 1604 +f 1413 1648 707 +f 2360 2024 1508 +f 598 1075 1116 +f 229 93 1468 +f 1839 1479 1684 +f 2216 2229 2224 +f 1610 1625 1637 +f 329 999 1637 +f 1199 1017 1427 +f 1017 303 1427 +f 303 1451 1427 +f 1792 1754 1777 +f 2309 2391 2301 +f 1655 1138 2116 +f 1138 707 1579 +f 1649 1491 206 +f 1406 1885 1398 +f 1406 1419 1885 +f 1419 1435 1885 +f 1434 1435 1446 +f 1470 1481 1469 +f 1577 1583 1603 +f 999 1017 1199 +f 81 67 941 +f 67 1650 941 +f 1259 1815 2164 +f 1619 2116 2045 +f 1424 707 1138 +f 1702 1649 206 +f 1687 1406 1398 +f 1477 1481 1470 +f 1568 1569 1577 +f 1577 1569 1583 +f 1603 1583 1610 +f 1625 329 1637 +f 2167 340 273 +f 81 273 340 +f 81 962 67 +f 1547 1619 1488 +f 1830 1739 1770 +f 938 1424 1138 +f 1424 1413 707 +f 1527 1649 1702 +f 1527 1520 1649 +f 1527 1268 1520 +f 1250 1406 1687 +f 1441 1353 1115 +f 1203 1413 1051 +f 1250 1419 1406 +f 1477 2372 1481 +f 1481 2372 1508 +f 2449 1560 1568 +f 1549 2449 1568 +f 1568 1560 1569 +f 1569 1584 1583 +f 1652 329 1625 +f 329 817 999 +f 285 1017 999 +f 303 10 1451 +f 10 2167 1451 +f 1412 1650 67 +f 1412 1488 1650 +f 1547 1023 1619 +f 1023 1655 1619 +f 1655 938 1138 +f 1456 1413 1424 +f 1457 1470 1446 +f 1457 1477 1470 +f 329 1652 817 +f 10 340 2167 +f 938 1546 1424 +f 1546 1456 1424 +f 1259 1548 1779 +f 2052 2031 1990 +f 1440 1202 1478 +f 1428 1419 1250 +f 1428 1435 1419 +f 1428 1446 1435 +f 1934 1935 1955 +f 1560 1584 1569 +f 1610 1638 1625 +f 1638 1652 1625 +f 817 1077 999 +f 1077 285 999 +f 980 303 1017 +f 962 1412 67 +f 1494 1023 1547 +f 325 271 1270 +f 1443 1511 1496 +f 1450 1268 1527 +f 1514 1353 1441 +f 1287 1446 1428 +f 1446 1287 1457 +f 1305 2372 1477 +f 1992 1990 1991 +f 1992 1991 1971 +f 1971 1991 1955 +f 2449 1549 2418 +f 1583 1616 1610 +f 1610 1616 1638 +f 10 1396 340 +f 340 1445 81 +f 1445 962 81 +f 1790 984 1753 +f 984 2148 1753 +f 1588 1713 1770 +f 969 978 958 +f 1741 1779 1703 +f 1758 1846 1754 +f 1827 1819 1029 +f 1818 1530 1712 +f 1750 1566 2127 +f 2459 2434 2483 +f 1798 1720 1771 +f 1794 1841 1803 +f 216 1755 1810 +f 1098 1735 1748 +f 1735 1497 1748 +f 1502 2102 1601 +f 881 1502 1601 +f 1455 1839 1744 +f 1706 1709 1680 +f 1212 1741 1703 +f 1788 1969 1671 +f 1075 1074 1692 +f 951 2500 881 +f 2490 2486 2463 +f 1748 1497 1781 +f 1721 984 1840 +f 1815 1259 1741 +f 1626 1756 1837 +f 975 987 1542 +f 2230 2236 2235 +f 1772 678 734 +f 1542 1671 975 +f 1806 1772 1780 +f 678 1772 1806 +f 2218 2225 2268 +f 1828 1732 2007 +f 1526 1688 1531 +f 1752 1526 1554 +f 1844 1818 1712 +f 1823 1846 1804 +f 1781 1669 1704 +f 1721 1779 2143 +f 1770 1670 1526 +f 1497 1669 1781 +f 1098 1713 1735 +f 1742 1815 1741 +f 1526 858 1875 +f 1599 1531 1688 +f 1803 1790 1558 +f 1703 1721 1683 +f 1832 1766 957 +f 1542 1679 1671 +f 1679 1788 1671 +f 1927 1819 1827 +f 1718 1745 1739 +f 1684 1022 1839 +f 1459 1283 1299 +f 1022 1410 1418 +f 2368 2393 2326 +f 1669 1497 1776 +f 1875 858 1212 +f 1739 1745 852 +f 1964 1918 1461 +f 1356 133 1331 +f 1765 1829 1468 +f 858 1742 1741 +f 1006 1674 1021 +f 1723 1936 1935 +f 1468 1713 1098 +f 1724 1678 1476 +f 1680 1783 908 +f 1731 1543 520 +f 1683 1721 1840 +f 1467 1679 1542 +f 1812 1708 1846 +f 1679 1975 1788 +f 1713 1830 1770 +f 1803 1722 1790 +f 2301 2391 2349 +f 1713 1588 1735 +f 1836 1530 1818 +f 1837 1756 861 +f 886 571 556 +f 1181 1805 1812 +f 1706 1680 1755 +f 1677 1729 1775 +f 1776 1787 1669 +f 1526 1670 1672 +f 1727 1770 1526 +f 987 1467 1542 +f 1567 1704 1137 +f 1693 1865 1714 +f 897 1762 912 +f 1135 1697 1062 +f 1697 376 1062 +f 1543 1731 1678 +f 1793 1679 1467 +f 1777 1602 1760 +f 1846 1798 1754 +f 1835 1096 1677 +f 1033 1030 940 +f 1450 1527 1702 +f 1717 376 1697 +f 1711 1717 1697 +f 1717 165 376 +f 1840 984 1790 +f 1669 1746 1704 +f 1669 1682 1746 +f 2301 2349 2308 +f 1882 1444 1898 +f 1820 1789 1730 +f 861 1380 1566 +f 2301 2308 2266 +f 1771 1543 1691 +f 1958 1659 1651 +f 1697 1360 1711 +f 1711 1737 1717 +f 1717 1737 165 +f 1790 1753 1558 +f 1668 1696 1663 +f 1360 1702 1711 +f 1702 1707 1711 +f 1707 1737 1711 +f 1737 1751 165 +f 1444 1782 1693 +f 1716 1787 1599 +f 1744 1839 1022 +f 1898 1444 1785 +f 206 1707 1702 +f 1764 2468 1751 +f 316 1844 893 +f 893 1844 915 +f 1845 1804 1758 +f 1380 861 1756 +f 1780 670 1021 +f 1714 2172 1763 +f 1783 1558 1663 +f 1750 2127 1465 +f 1798 1771 1691 +f 1691 1543 1724 +f 1872 1910 839 +f 1737 2044 1751 +f 1751 2044 1764 +f 1757 1701 482 +f 1725 1602 1777 +f 1836 1845 1530 +f 2102 2470 1503 +f 2496 1899 544 +f 763 2484 946 +f 987 1719 1467 +f 1845 1758 1792 +f 1725 1837 1602 +f 1872 1866 1873 +f 1712 1530 1760 +f 489 1799 216 +f 1760 861 1750 +f 2068 2466 2460 +f 1696 2159 2168 +f 377 1377 1280 +f 1797 1676 1572 +f 1581 2053 1572 +f 1676 1581 1572 +f 1764 2498 2468 +f 2468 2498 1994 +f 1861 1695 1860 +f 2481 2004 2495 +f 1826 1677 1523 +f 1670 1739 852 +f 2234 2269 2253 +f 1724 1543 1678 +f 1658 2168 1791 +f 1397 1747 1719 +f 1696 2168 1658 +f 979 519 272 +f 1774 1975 1679 +f 975 1671 932 +f 1787 1716 1682 +f 1835 1826 1747 +f 2501 2469 961 +f 1810 908 1791 +f 1982 1768 191 +f 1137 1704 1825 +f 1804 1846 1758 +f 2004 2044 1737 +f 913 1969 902 +f 2498 1795 1801 +f 915 1844 1712 +f 1689 915 1712 +f 1740 1752 1541 +f 695 661 199 +f 1865 1693 1782 +f 1824 1464 1809 +f 1829 1765 1718 +f 1816 1768 1982 +f 1816 1622 1768 +f 1622 2165 1681 +f 1768 1622 1681 +f 670 1772 228 +f 1283 1459 52 +f 1785 1444 1749 +f 1675 1075 1685 +f 1567 1781 1704 +f 1858 1857 1848 +f 1526 1752 1688 +f 1791 2160 1810 +f 908 1658 1791 +f 1813 1773 1558 +f 1845 1792 1530 +f 69 376 165 +f 3 1832 1834 +f 1722 1516 1507 +f 1801 1821 1994 +f 1833 1982 2046 +f 1821 1833 2046 +f 1833 1816 1982 +f 1022 1785 1749 +f 2160 2170 1810 +f 1147 1719 1726 +f 1683 1840 1507 +f 1467 1719 1793 +f 1795 1802 1801 +f 1802 1811 1801 +f 1801 1811 1821 +f 1690 2165 1622 +f 1934 1480 1925 +f 229 1468 1091 +f 1780 2164 1742 +f 1672 1742 858 +f 1833 1417 1816 +f 1417 1622 1816 +f 1831 2165 1690 +f 1668 1663 1558 +f 1719 1747 1826 +f 1760 1750 1664 +f 1817 1690 1622 +f 1530 1792 1777 +f 948 1796 1802 +f 1796 1811 1802 +f 1515 1817 1622 +f 1695 1861 1831 +f 1783 1663 1658 +f 1749 1410 1022 +f 854 1796 948 +f 1811 1842 1833 +f 1821 1811 1833 +f 1833 1842 1417 +f 1622 1417 1515 +f 127 1804 1845 +f 1686 1626 1837 +f 1608 1690 1817 +f 1523 1775 1762 +f 127 1845 1836 +f 1812 1805 1708 +f 1523 1677 1775 +f 1780 1772 670 +f 1758 1754 1792 +f 1204 1796 854 +f 1822 1842 1811 +f 1608 1831 1690 +f 1822 1811 1796 +f 1842 1416 1417 +f 1417 1416 1515 +f 1515 1608 1817 +f 1728 1831 1608 +f 908 1783 1658 +f 127 1836 316 +f 1805 1148 1824 +f 852 1745 1672 +f 1478 1093 1928 +f 1822 1843 1842 +f 1843 959 1842 +f 1842 959 1416 +f 1728 1695 1831 +f 1728 1860 1695 +f 2346 446 2337 +f 1602 1837 861 +f 1087 1096 1835 +f 1708 1824 1809 +f 2004 1737 505 +f 1567 1748 1781 +f 520 1543 1883 +f 1760 1664 1712 +f 128 1336 72 +f 2053 1883 1543 +f 1822 180 1843 +f 1786 1608 1515 +f 929 2462 519 +f 512 2402 506 +f 1212 1703 1683 +f 1830 1829 1739 +f 2053 1543 1771 +f 1416 1769 1515 +f 1769 1786 1515 +f 1786 1728 1608 +f 1712 1808 1689 +f 1794 1558 1783 +f 1497 1735 1776 +f 1127 1567 1137 +f 1123 1748 1567 +f 36 205 1185 +f 959 1734 1416 +f 1738 1733 1541 +f 1774 1762 1974 +f 1752 1554 1541 +f 1752 1740 1688 +f 1526 1875 1554 +f 1468 1829 1830 +f 1755 908 1810 +f 1716 1599 1814 +f 1806 1780 1742 +f 2308 2349 2340 +f 1832 915 1689 +f 1713 1468 1830 +f 1814 1599 1346 +f 1832 1689 1766 +f 1022 1684 1785 +f 1093 1153 1116 +f 1672 678 1742 +f 1675 1685 1093 +f 1841 1743 1722 +f 1814 2053 1581 +f 1464 1746 1809 +f 2485 2497 2493 +f 1416 1734 1769 +f 1665 1728 1786 +f 1665 1951 1728 +f 1951 1860 1728 +f 1951 2094 1860 +f 1844 1836 1818 +f 316 1836 1844 +f 1776 1531 1787 +f 1719 1826 1793 +f 2147 1401 29 +f 2111 2121 1548 +f 1741 1259 1779 +f 1843 347 1834 +f 1843 1734 959 +f 1766 1769 1734 +f 957 1766 1734 +f 1766 1786 1769 +f 1766 1689 1786 +f 1689 1665 1786 +f 1754 1691 1777 +f 1507 1840 1790 +f 1761 1470 1461 +f 1523 1679 1793 +f 1091 1468 1098 +f 1820 1730 1838 +f 1843 1834 1734 +f 1808 1951 1665 +f 1588 1727 1531 +f 893 915 1832 +f 1523 1774 1679 +f 272 2488 710 +f 1093 1116 1675 +f 2340 2349 2348 +f 1832 1734 1834 +f 1832 957 1734 +f 1951 1808 2094 +f 1685 1692 1505 +f 1043 295 698 +f 2143 1779 2121 +f 1689 1808 1665 +f 1693 1714 1763 +f 1738 2157 1516 +f 1114 1921 236 +f 1268 1333 1520 +f 1149 1108 431 +f 508 2144 1912 +f 1957 1108 1537 +f 431 1108 1957 +f 1018 1108 2167 +f 1338 1957 1681 +f 2163 1957 1338 +f 1983 1390 2093 +f 30 557 37 +f 1714 2173 2172 +f 1983 1984 1390 +f 1984 2065 1390 +f 884 1762 897 +f 2065 1984 1214 +f 1950 1974 1762 +f 884 1950 1762 +f 2012 1698 1861 +f 1214 2116 803 +f 1950 1938 1974 +f 1938 1967 1974 +f 1900 1761 1461 +f 865 1929 884 +f 884 1929 1950 +f 2062 2071 2042 +f 919 1985 1732 +f 1593 502 2146 +f 1995 1213 2098 +f 1522 2476 1651 +f 2174 1849 2175 +f 1480 1989 1499 +f 1929 1938 1950 +f 1605 2058 1574 +f 2097 1605 2133 +f 1912 2014 1886 +f 2092 2082 2083 +f 206 1930 505 +f 2101 2100 2092 +f 2073 2101 2092 +f 839 1910 865 +f 1910 1901 1929 +f 865 1910 1929 +f 1967 1788 1975 +f 2073 2092 2063 +f 2101 1593 2100 +f 2015 1876 1698 +f 1853 1884 2014 +f 1831 1698 2165 +f 1316 273 81 +f 1901 1920 1929 +f 1929 1920 1938 +f 1920 1968 1967 +f 1938 1920 1967 +f 1849 2174 1700 +f 2173 1700 2174 +f 2062 2072 2091 +f 803 2467 2059 +f 2239 1736 2240 +f 1505 1357 1685 +f 1358 1686 1476 +f 1967 1968 1788 +f 1968 1969 1788 +f 2065 2110 2156 +f 2065 1214 2110 +f 2110 1214 503 +f 273 2093 1018 +f 273 1983 2093 +f 532 1886 2155 +f 2034 2021 1947 +f 216 1810 585 +f 1912 543 2014 +f 1390 2051 1537 +f 1872 1873 1910 +f 1984 2045 1214 +f 597 1912 1886 +f 1593 2146 2100 +f 2071 2062 2090 +f 2034 2046 1982 +f 2034 1947 2046 +f 1214 2045 2116 +f 1873 1887 1910 +f 1887 1901 1910 +f 1562 1447 1106 +f 2163 431 1957 +f 1948 1972 1936 +f 1972 1948 1992 +f 2014 2015 2013 +f 1853 2014 2013 +f 1550 1884 1853 +f 1947 2468 1994 +f 1355 1550 2154 +f 1355 1884 1550 +f 2081 2108 2128 +f 2024 1965 1492 +f 2024 2032 1965 +f 2116 1604 803 +f 1901 1911 1920 +f 1939 1968 1920 +f 1911 1939 1920 +f 872 1626 1666 +f 2062 2091 2120 +f 1819 1927 1759 +f 1021 1674 1780 +f 872 1673 1756 +f 1550 501 2171 +f 1378 1550 2171 +f 2146 2162 2145 +f 1358 482 192 +f 2109 2120 2119 +f 1866 1872 2227 +f 1391 2012 1860 +f 2136 2137 2161 +f 2162 1661 236 +f 1887 1894 1901 +f 1901 1894 1911 +f 505 1707 206 +f 2120 2137 2136 +f 2142 2164 1674 +f 1860 2012 1861 +f 1894 1939 1911 +f 2080 2060 2118 +f 2162 236 508 +f 2164 1815 1742 +f 1018 2093 1537 +f 2154 1378 2161 +f 2041 2098 2491 +f 2043 2042 2032 +f 1108 1018 1537 +f 1465 2094 1808 +f 502 1643 1661 +f 2467 1618 1656 +f 2119 2136 2135 +f 2119 2108 2071 +f 878 1183 1195 +f 2101 1594 1593 +f 2033 2370 2063 +f 2482 2491 2098 +f 1282 2406 1275 +f 2003 1948 1956 +f 2043 2032 2024 +f 2025 2043 2024 +f 2154 1550 1378 +f 1795 2498 1764 +f 2142 1548 2164 +f 2431 2454 2422 +f 1981 2011 1993 +f 2349 2391 2362 +f 502 2162 2146 +f 2025 2024 2360 +f 2129 2120 2091 +f 1732 1985 2007 +f 2171 1308 209 +f 1930 1995 2041 +f 1390 1238 2051 +f 1866 1878 1887 +f 1878 1894 1887 +f 1965 2032 2011 +f 874 2480 2492 +f 2071 2108 2069 +f 1358 1731 482 +f 430 2021 2034 +f 1965 2003 1964 +f 1855 1889 831 +f 1668 1773 2150 +f 1390 2156 1238 +f 898 869 1903 +f 2391 2407 2362 +f 2121 2111 2074 +f 1548 1259 2164 +f 2099 2129 2091 +f 1550 1853 501 +f 1853 1852 501 +f 952 2017 969 +f 2085 2121 2074 +f 2130 2006 1391 +f 2144 1367 543 +f 2100 2146 2099 +f 1545 1319 113 +f 1903 1922 898 +f 1922 1931 898 +f 585 2170 1641 +f 2007 2017 952 +f 2017 2074 969 +f 1558 1753 1813 +f 837 2005 1517 +f 2005 2006 2130 +f 1532 1474 1528 +f 2003 1981 1948 +f 2070 2071 2069 +f 1922 919 1931 +f 2017 2085 2074 +f 2085 2104 2121 +f 2100 2099 2082 +f 2156 2110 2034 +f 505 2474 2004 +f 1903 871 1922 +f 1922 1952 919 +f 919 1952 1985 +f 1985 2001 2007 +f 2001 2036 2017 +f 2007 2001 2017 +f 2017 2036 2085 +f 2036 2047 2085 +f 2047 2075 2085 +f 2075 2104 2085 +f 1948 1993 2023 +f 2400 2422 2407 +f 2011 2070 1993 +f 2033 2043 2025 +f 2012 2015 1698 +f 1876 1338 2165 +f 871 1940 1922 +f 1985 1976 2001 +f 2121 2104 2143 +f 1051 1413 1456 +f 2358 1362 2379 +f 1859 1789 1870 +f 2090 2109 2071 +f 1405 1398 1885 +f 1886 1884 1355 +f 1922 1960 1952 +f 1952 1960 1985 +f 1960 1976 1985 +f 1956 1948 1936 +f 2135 209 2128 +f 2157 1875 1212 +f 2160 2168 2169 +f 1900 1461 1918 +f 2001 2018 2036 +f 2075 2086 2104 +f 2111 2142 2103 +f 1937 1956 1936 +f 2023 2070 2061 +f 2135 2128 2108 +f 2042 2071 2011 +f 2138 413 2383 +f 2033 2072 2043 +f 1922 1940 1960 +f 2070 2069 2061 +f 2069 2108 2061 +f 2108 2119 2135 +f 1855 1904 1889 +f 1889 1904 871 +f 871 1904 1940 +f 1976 2018 2001 +f 2036 2018 2047 +f 2122 2143 2104 +f 216 1642 489 +f 2148 984 2143 +f 1975 1974 1967 +f 2157 1683 1516 +f 1614 1593 1594 +f 2269 2270 2276 +f 1926 2147 29 +f 2082 2091 2072 +f 430 503 2059 +f 1904 1905 1940 +f 1940 1961 1960 +f 1961 1976 1960 +f 2087 2086 2075 +f 2065 2156 1390 +f 1820 1838 1900 +f 534 1308 837 +f 2167 273 1018 +f 831 1850 1855 +f 2019 2037 2018 +f 2018 2037 2047 +f 2037 2075 2047 +f 2086 2095 2104 +f 2095 2122 2104 +f 2122 2148 2143 +f 1926 1213 1995 +f 1405 1885 1761 +f 2006 2013 2012 +f 2211 2233 2216 +f 1855 1890 1904 +f 1904 1895 1905 +f 1905 1932 1940 +f 1961 1977 1976 +f 1976 1986 2018 +f 2484 2476 1518 +f 1870 1411 1859 +f 1548 2142 2111 +f 1904 1890 1895 +f 1895 1932 1905 +f 1940 1932 1961 +f 1976 1977 1986 +f 1986 2008 2018 +f 2018 2008 2019 +f 2087 2075 2037 +f 2087 2095 2086 +f 2094 1391 1860 +f 1852 1853 2006 +f 1853 2013 2006 +f 929 979 850 +f 1855 1874 1890 +f 2008 2028 2019 +f 1993 2070 2023 +f 1705 1799 1998 +f 1491 2147 206 +f 1851 1856 1855 +f 1895 1890 1874 +f 2038 2019 2028 +f 2038 2048 2037 +f 2019 2038 2037 +f 2048 2067 2087 +f 2037 2048 2087 +f 2087 2067 2095 +f 2095 2149 2122 +f 2149 2148 2122 +f 1308 2005 837 +f 209 1308 1387 +f 1601 2102 1927 +f 254 170 201 +f 1800 1403 1763 +f 1510 1346 1740 +f 870 871 1903 +f 1919 1650 1619 +f 2148 1667 1753 +f 1932 1923 1961 +f 1977 1953 1986 +f 2067 2112 2095 +f 2112 2149 2095 +f 2148 2149 1667 +f 2422 2421 2407 +f 1926 2026 1213 +f 1912 2144 543 +f 2128 1387 2153 +f 1733 1510 1740 +f 990 853 2489 +f 503 1214 803 +f 1921 431 2163 +f 2146 2145 2129 +f 2144 1921 2163 +f 1855 1856 1874 +f 1895 1923 1932 +f 1923 1941 1961 +f 1961 1941 1977 +f 2048 2076 2067 +f 2076 2113 2067 +f 2067 2113 2112 +f 1723 1900 1937 +f 1870 1900 1723 +f 1367 2163 1338 +f 520 1346 1510 +f 1698 1831 1861 +f 1984 1919 2045 +f 1895 1891 1923 +f 2008 1986 2028 +f 1948 1981 1993 +f 1883 1346 520 +f 1883 1814 1346 +f 1930 206 2147 +f 2499 2486 1447 +f 1891 1906 1923 +f 1923 1953 1941 +f 1953 1977 1941 +f 1953 1987 1986 +f 2113 2123 2112 +f 2123 2149 2112 +f 1387 1308 1226 +f 1599 1688 1346 +f 2093 1390 1537 +f 2003 2011 1981 +f 1987 2028 1986 +f 2038 2049 2048 +f 2048 2049 2076 +f 1813 1667 2149 +f 2123 1813 2149 +f 1461 1469 1964 +f 1757 1510 1743 +f 505 1930 1999 +f 2223 1784 1789 +f 1532 1522 1651 +f 1906 1913 1923 +f 1913 1943 1923 +f 1943 1942 1923 +f 1923 1942 1953 +f 1942 1987 1953 +f 1308 1852 2005 +f 2053 1814 1883 +f 1733 1740 1541 +f 2154 1886 1355 +f 1503 1528 1474 +f 1874 1879 1895 +f 1895 1879 1891 +f 2076 2124 2113 +f 2113 2124 2123 +f 1896 1891 1879 +f 1891 1896 1906 +f 1942 1962 1987 +f 1962 2009 2028 +f 1987 1962 2028 +f 2009 2038 2028 +f 2109 2119 2071 +f 1918 1956 1937 +f 1851 1864 1856 +f 1896 1897 1906 +f 1906 1897 1913 +f 1943 1962 1942 +f 2049 2077 2076 +f 2124 2125 2123 +f 1930 2147 1926 +f 1902 1894 1878 +f 482 1510 1757 +f 2129 2137 2120 +f 503 803 2059 +f 1847 1857 1851 +f 1851 1857 1864 +f 2039 2038 2009 +f 2038 2039 2049 +f 2076 2077 2124 +f 2150 1813 2123 +f 482 520 1510 +f 1994 1821 2046 +f 2044 2004 1764 +f 1864 1867 1856 +f 1867 1874 1856 +f 1897 1944 1913 +f 1943 1944 1962 +f 2124 2126 2125 +f 2150 2123 2125 +f 2099 2146 2129 +f 2041 1995 2098 +f 1605 1641 2151 +f 1847 1959 1857 +f 1874 1867 1879 +f 1913 1944 1943 +f 1944 1963 1962 +f 2077 2096 2124 +f 2096 2126 2124 +f 2126 2150 2125 +f 941 1650 1919 +f 2135 2136 209 +f 1884 1886 2014 +f 2049 2029 2077 +f 1388 2127 1389 +f 1389 2127 1566 +f 1930 1926 1995 +f 941 1919 1316 +f 2110 503 430 +f 1867 1880 1879 +f 1879 1880 1896 +f 1897 1907 1944 +f 1963 1978 1962 +f 1962 1978 2009 +f 2039 2029 2049 +f 2077 2078 2096 +f 822 823 827 +f 2166 1668 2150 +f 81 941 1316 +f 2204 2216 2203 +f 2011 2071 2070 +f 1880 1892 1896 +f 1892 1907 1897 +f 1896 1892 1897 +f 1907 1914 1944 +f 1978 2010 2009 +f 2010 2039 2009 +f 1688 1740 1346 +f 1789 1820 1870 +f 2130 1391 2094 +f 1944 1945 1963 +f 2029 2078 2077 +f 1767 2150 2126 +f 1767 2166 2150 +f 803 2022 2467 +f 1503 1927 2102 +f 1914 1954 1944 +f 1944 1954 1945 +f 1963 1970 1978 +f 2078 2105 2096 +f 2105 2126 2096 +f 1965 2011 2003 +f 192 1626 1358 +f 2101 1559 1594 +f 1930 2041 1999 +f 1698 1876 2165 +f 1398 1871 891 +f 2165 1338 1681 +f 1970 2010 1978 +f 2010 2030 2029 +f 2039 2010 2029 +f 2030 2055 2078 +f 2029 2030 2078 +f 1849 1848 2175 +f 1871 1862 891 +f 543 2015 2014 +f 1857 1858 1864 +f 1864 1858 1867 +f 1963 1945 1970 +f 2055 2088 2078 +f 2078 2088 2105 +f 2105 2131 2126 +f 2126 2131 1767 +f 2063 2083 2033 +f 2161 2171 209 +f 2032 2042 2011 +f 1813 2150 1773 +f 1914 1908 1954 +f 1970 1979 2010 +f 2088 2131 2105 +f 2015 543 1876 +f 1694 1692 1048 +f 1395 2207 1859 +f 1395 1393 2207 +f 1730 1784 1736 +f 2500 2466 2470 +f 1709 1701 1757 +f 1945 1979 1970 +f 2030 2050 2055 +f 2350 2317 2286 +f 2154 2155 1886 +f 871 860 1889 +f 2161 209 2136 +f 2497 2463 2493 +f 2190 2204 2203 +f 1800 2179 1404 +f 2477 2469 1385 +f 1385 1715 2477 +f 2128 209 1387 +f 1858 1868 1867 +f 1867 1881 1880 +f 1893 1892 1880 +f 1881 1893 1880 +f 1893 1907 1892 +f 1907 1908 1914 +f 1954 1979 1945 +f 1979 1980 2010 +f 2131 2159 1767 +f 1765 93 339 +f 1761 1877 1405 +f 523 1347 515 +f 1541 2157 1738 +f 2144 2163 1367 +f 1380 1389 1566 +f 2317 2392 2316 +f 1994 2498 1801 +f 1867 1868 1881 +f 1980 2050 2030 +f 2010 1980 2030 +f 2050 2089 2055 +f 2055 2089 2088 +f 2088 2114 2131 +f 1538 1651 1659 +f 2145 2155 2129 +f 2140 29 1928 +f 2370 2033 2025 +f 2252 2239 2240 +f 2239 2252 1862 +f 2392 2391 2316 +f 2469 2501 1385 +f 2477 1715 1710 +f 502 1614 1643 +f 2438 1227 2431 +f 1915 1907 1893 +f 1915 1908 1907 +f 1954 1908 1979 +f 1908 1988 1979 +f 1979 1988 1980 +f 2114 2159 2131 +f 2155 2154 2129 +f 508 1966 2144 +f 872 1756 1626 +f 1710 1715 1505 +f 236 1966 508 +f 2272 2284 1398 +f 2325 2355 2319 +f 1548 2121 1779 +f 1532 1528 1522 +f 1980 2056 2050 +f 2050 2056 2089 +f 2013 2015 2012 +f 1964 2003 1956 +f 2006 2012 1391 +f 1565 1927 1503 +f 2244 2243 2226 +f 5 1715 1385 +f 1858 1848 1868 +f 1915 1946 1908 +f 1946 1988 1908 +f 1980 2020 2056 +f 2115 2159 2114 +f 2092 2083 2063 +f 1398 2284 1687 +f 2162 2155 2145 +f 519 2475 2488 +f 2158 5 1385 +f 5 1505 1715 +f 1692 1694 1505 +f 1988 2020 1980 +f 2115 2169 2159 +f 2169 2168 2159 +f 2083 2082 2072 +f 1316 1984 1983 +f 1488 1619 1650 +f 2083 2072 2033 +f 2361 1210 1233 +f 1933 1946 1915 +f 2056 2079 2089 +f 2088 2115 2114 +f 2099 2091 2082 +f 2162 532 2155 +f 1852 2006 2005 +f 2023 2061 2052 +f 2176 2184 2175 +f 2162 985 532 +f 1909 1893 1881 +f 1909 1915 1893 +f 1988 2040 2020 +f 2040 2056 2020 +f 2089 2079 2088 +f 2088 2079 2115 +f 1782 1444 1882 +f 1216 1215 2320 +f 867 1939 1894 +f 867 903 1939 +f 1372 2398 1379 +f 1863 504 2027 +f 2158 1385 504 +f 1868 1782 1881 +f 1909 1933 1915 +f 2040 1988 1946 +f 1481 2024 1492 +f 2120 2136 2119 +f 1522 1528 1518 +f 1871 1398 1405 +f 1221 1408 1399 +f 1357 5 2158 +f 2179 1800 1763 +f 1868 1865 1782 +f 1882 1881 1782 +f 1882 1909 1881 +f 2040 2057 2056 +f 2106 2079 2056 +f 2057 2106 2056 +f 2106 2132 2079 +f 2132 2115 2079 +f 2115 2132 2169 +f 532 985 597 +f 2092 2100 2082 +f 1210 1221 1399 +f 1399 1233 1210 +f 2130 2002 1517 +f 1849 1865 1868 +f 1933 2040 1946 +f 52 1269 30 +f 1667 1813 1753 +f 1997 1380 1673 +f 940 1008 1088 +f 1947 1994 2046 +f 1882 1916 1909 +f 1924 1933 1909 +f 1533 2040 1933 +f 1533 1534 2040 +f 2058 2040 1534 +f 2058 2057 2040 +f 1238 191 1768 +f 1997 1389 1380 +f 1875 1541 1554 +f 1854 504 1863 +f 1854 2158 504 +f 2396 1275 2406 +f 2426 2443 153 +f 1916 1924 1909 +f 1925 1935 1934 +f 1870 1723 1426 +f 2058 2097 2057 +f 2097 2106 2057 +f 2132 2151 2169 +f 2151 2160 2169 +f 1106 1635 1562 +f 1957 1768 1681 +f 1957 2051 1768 +f 526 535 33 +f 1614 1594 1609 +f 2233 2229 2216 +f 2496 2027 2084 +f 2496 1863 2027 +f 2117 1854 1863 +f 2016 2158 1854 +f 2016 1504 1357 +f 2158 2016 1357 +f 1114 236 1661 +f 2129 2154 2137 +f 2133 2106 2097 +f 2491 1999 2041 +f 2051 1238 1768 +f 2061 2108 2081 +f 2189 2195 2186 +f 2348 2349 2362 +f 1701 192 482 +f 505 1737 1707 +f 2133 2132 2106 +f 2132 2133 2151 +f 2151 2170 2160 +f 502 1661 2162 +f 1998 1389 1997 +f 2297 2352 2329 +f 2352 2364 2329 +f 2394 2414 2364 +f 2352 2394 2364 +f 2402 512 2415 +f 2255 2254 2243 +f 2446 1365 2456 +f 2271 2282 2298 +f 846 2283 2264 +f 2293 2310 2318 +f 2254 2295 2294 +f 2283 2290 2278 +f 2270 2294 2293 +f 2423 2455 2400 +f 2281 2287 2267 +f 2190 2191 2204 +f 2271 2263 2282 +f 2334 2329 2364 +f 2424 2432 2409 +f 2282 2263 2298 +f 1409 1659 1958 +f 2263 2302 2298 +f 2297 2329 2296 +f 1256 446 2346 +f 1958 2502 2478 +f 2437 2399 2444 +f 263 2366 2359 +f 849 827 823 +f 2311 2325 2290 +f 2499 2379 2434 +f 2446 2456 2423 +f 947 2358 2379 +f 2499 947 2379 +f 2205 2195 2212 +f 2245 2237 2227 +f 2245 2256 2237 +f 2256 2263 2271 +f 556 571 2305 +f 1528 2068 1518 +f 2424 2439 2432 +f 2302 2352 2297 +f 1866 2237 826 +f 2248 2242 2211 +f 2334 2364 2363 +f 2235 2244 2226 +f 2255 2295 2254 +f 2329 2324 2296 +f 2439 2447 1973 +f 2329 2334 2324 +f 2409 2432 2414 +f 2293 2318 2276 +f 866 2425 2416 +f 1487 1493 2372 +f 2237 2231 2230 +f 2415 512 17 +f 2035 1236 26 +f 921 2138 688 +f 2491 2482 2462 +f 6 181 197 +f 2481 948 1795 +f 2138 2383 2382 +f 2377 2394 2352 +f 2377 506 2394 +f 2394 506 2402 +f 2401 2402 2415 +f 2394 2402 2401 +f 2318 2326 2276 +f 2439 2457 2432 +f 2298 2302 2297 +f 2244 2249 2243 +f 2404 1100 2382 +f 2238 2245 2227 +f 2245 2257 2256 +f 2257 2263 2256 +f 2324 2334 2328 +f 2257 2289 2263 +f 2289 2302 2263 +f 2236 2231 2250 +f 2138 2382 688 +f 2383 2404 2382 +f 1100 2404 2343 +f 2353 2352 2302 +f 2353 2377 2352 +f 2237 2230 2220 +f 2335 2355 2325 +f 2308 2340 2315 +f 2253 2269 2276 +f 2311 2335 2325 +f 2439 2424 511 +f 2268 2267 2248 +f 2383 413 2404 +f 123 971 832 +f 2234 2243 2269 +f 2225 2213 2234 +f 2219 2213 2225 +f 2195 2196 2212 +f 1544 2418 1549 +f 413 866 2404 +f 2404 866 2416 +f 2416 2417 2404 +f 2404 2417 2343 +f 2415 2409 2401 +f 2196 2219 2212 +f 2268 2248 2218 +f 2206 2214 2197 +f 2417 2332 2343 +f 2343 2332 832 +f 2330 2302 2289 +f 2330 2353 2302 +f 2453 2454 515 +f 2218 2248 2217 +f 2218 2217 2205 +f 2276 2281 2268 +f 2178 2197 2177 +f 2197 2189 2177 +f 2332 2066 832 +f 832 2066 123 +f 2231 2236 2230 +f 669 950 1144 +f 2217 2211 2199 +f 1216 1209 1217 +f 2066 2365 123 +f 2230 2226 2214 +f 2290 2325 2304 +f 2325 2319 2304 +f 2217 2248 2211 +f 2191 2192 2199 +f 510 525 2035 +f 2417 1917 2332 +f 2332 1917 2066 +f 2408 2413 2341 +f 2248 2267 2242 +f 2326 2333 2281 +f 1340 2365 2066 +f 2440 1302 1340 +f 2226 2230 2235 +f 1153 1163 1116 +f 2431 2455 2438 +f 2416 2425 2417 +f 2495 2474 2462 +f 2290 2304 2277 +f 825 2227 1872 +f 151 239 1038 +f 9 151 1038 +f 545 928 2381 +f 2440 2406 1384 +f 928 1596 2381 +f 2186 2188 2185 +f 2456 26 1888 +f 2287 2333 2262 +f 2425 2342 2417 +f 2342 1917 2417 +f 1917 877 2066 +f 2336 1340 2066 +f 2336 2440 1340 +f 2328 2351 2327 +f 825 2238 2227 +f 2351 2368 2327 +f 1222 2388 1211 +f 678 756 734 +f 428 263 1343 +f 2188 2191 2190 +f 2341 2376 2333 +f 2066 877 2336 +f 2290 2277 2278 +f 739 634 592 +f 675 304 14 +f 2384 675 14 +f 2199 2211 2204 +f 2191 2199 2204 +f 2322 2318 2310 +f 2287 2262 2233 +f 2185 2188 2184 +f 2386 2425 845 +f 2384 572 675 +f 1128 123 2365 +f 832 971 2343 +f 2188 2186 2191 +f 2185 2184 2176 +f 2345 1917 2342 +f 2345 877 1917 +f 2336 2406 2440 +f 971 1100 2343 +f 2299 2289 2257 +f 2299 2303 2289 +f 2249 2255 2243 +f 506 513 512 +f 2437 955 1219 +f 1587 2398 1324 +f 877 2396 2336 +f 2336 2396 2406 +f 2463 2479 879 +f 2376 2412 2350 +f 2281 2267 2268 +f 2303 2330 2289 +f 624 635 159 +f 1996 2356 1561 +f 2449 2436 1996 +f 2356 2054 2451 +f 928 2398 1587 +f 2333 2350 2262 +f 2035 26 2456 +f 2346 2342 2425 +f 2346 2345 2342 +f 1544 2380 2418 +f 2412 2392 2350 +f 622 509 1151 +f 2436 2054 1996 +f 545 2451 928 +f 2326 2341 2333 +f 2346 2425 2386 +f 1365 2035 2456 +f 2369 2377 2353 +f 2369 506 2377 +f 2451 900 928 +f 900 2398 928 +f 1235 1888 1244 +f 2337 2345 2346 +f 877 772 2396 +f 772 1275 2396 +f 2432 2446 2414 +f 2294 2295 2310 +f 2369 2330 828 +f 2418 2419 2436 +f 2450 2429 2436 +f 2436 2429 2054 +f 2490 2494 1656 +f 1321 155 2338 +f 1256 2346 2386 +f 2448 877 2345 +f 877 2448 772 +f 2446 2423 2414 +f 2351 2334 2363 +f 2243 2254 2269 +f 2380 2419 2418 +f 2419 2450 2436 +f 2283 2278 2264 +f 822 2197 823 +f 1008 1759 1565 +f 2448 2345 2337 +f 2270 2293 2276 +f 2323 2324 2328 +f 2429 1012 2054 +f 2226 2243 2213 +f 2395 325 772 +f 2370 2367 2380 +f 2054 2435 2451 +f 2435 2397 2451 +f 2451 2397 900 +f 1774 1974 1975 +f 2305 2290 2283 +f 846 2305 2283 +f 2320 1215 2285 +f 2139 2448 2337 +f 2448 2395 772 +f 1232 1231 1216 +f 2272 2285 2284 +f 2367 2371 2380 +f 2371 2405 2380 +f 2380 2405 2419 +f 2419 2429 2450 +f 2429 176 1012 +f 2397 2373 900 +f 2373 2398 900 +f 2373 1379 2398 +f 2372 1500 1508 +f 1133 1303 1142 +f 2252 2273 2272 +f 891 2252 2272 +f 2419 2405 2429 +f 2405 2430 2429 +f 2429 2430 176 +f 2189 2186 2181 +f 2212 2219 2218 +f 2312 2139 2337 +f 2139 2384 2448 +f 2448 2384 2395 +f 899 855 843 +f 2272 2273 2285 +f 2331 2303 2299 +f 176 2435 2054 +f 1012 176 2054 +f 2177 2185 2176 +f 2218 2219 2225 +f 1216 1220 1215 +f 2378 2139 2312 +f 2384 14 2395 +f 2324 2295 2255 +f 2240 2273 2252 +f 2371 2387 2405 +f 2410 2430 2405 +f 2430 2442 176 +f 2435 2344 2397 +f 2397 2344 2373 +f 2456 1888 2455 +f 2242 2267 2233 +f 2233 2262 2229 +f 2378 2384 2139 +f 2323 2310 2295 +f 2323 2322 2310 +f 2240 2274 2273 +f 974 841 990 +f 2490 1447 2486 +f 2387 2410 2405 +f 2442 2141 176 +f 2344 1778 2373 +f 972 1379 2373 +f 1778 972 2373 +f 1379 972 428 +f 1211 2437 1223 +f 1228 1215 1220 +f 702 2378 2312 +f 17 518 2415 +f 1888 26 1244 +f 2324 2323 2295 +f 2305 2311 2290 +f 2307 2285 2273 +f 2274 2307 2273 +f 2307 2320 2285 +f 2369 531 506 +f 2435 2258 2344 +f 2296 2324 2288 +f 1233 1217 2361 +f 2360 2371 2367 +f 2410 2442 2430 +f 176 2141 2258 +f 176 2258 2435 +f 539 2331 66 +f 2350 2392 2317 +f 2268 2225 2253 +f 1508 1500 2371 +f 2360 1508 2371 +f 2371 1500 2387 +f 972 2366 428 +f 1626 1686 1358 +f 1759 1807 1819 +f 2277 2257 2245 +f 2277 2299 2257 +f 1784 2228 1736 +f 2265 2240 1736 +f 2228 2265 1736 +f 2265 2274 2240 +f 1209 2320 2307 +f 2320 1209 1216 +f 1555 1584 1560 +f 2387 1500 2372 +f 2410 2420 2442 +f 2433 972 1778 +f 2433 2366 972 +f 955 522 1225 +f 2339 2307 2274 +f 2372 1493 2387 +f 2411 2420 2410 +f 2420 954 2442 +f 2442 954 2141 +f 2344 2433 1778 +f 2205 2212 2218 +f 2328 2334 2351 +f 2394 2401 2414 +f 2250 2256 2271 +f 2339 1209 2307 +f 2328 2322 2323 +f 866 845 2425 +f 3 316 893 +f 2387 2411 2410 +f 2441 2141 954 +f 2141 2441 2258 +f 2354 2433 2344 +f 2254 2294 2270 +f 2269 2254 2270 +f 863 2305 846 +f 2441 2354 2258 +f 2258 2354 2344 +f 2319 2355 51 +f 2223 2228 1784 +f 1493 2411 2387 +f 1560 2449 1555 +f 2288 2324 2255 +f 825 2251 2238 +f 2251 2245 2238 +f 1299 84 1312 +f 2246 2265 2228 +f 2313 2274 2265 +f 2313 2339 2274 +f 2251 2277 2245 +f 2319 51 2331 +f 891 1862 2252 +f 2443 954 2420 +f 2443 2441 954 +f 511 2447 2439 +f 2242 2233 2211 +f 188 15 814 +f 2443 2426 2441 +f 2426 2354 2441 +f 2306 2403 2433 +f 2433 2403 2366 +f 539 2303 2331 +f 2246 2228 2223 +f 1030 1819 1807 +f 2354 2306 2433 +f 2413 2412 2376 +f 2438 2455 1888 +f 1848 1857 2176 +f 2207 2208 2223 +f 2208 2246 2223 +f 1209 2339 1217 +f 2339 2361 1217 +f 1221 1210 2388 +f 554 109 78 +f 386 1375 95 +f 2327 2326 2318 +f 2179 2182 1393 +f 2182 2208 1393 +f 1393 2208 2207 +f 2361 2399 2388 +f 2388 2399 1211 +f 2306 2354 2426 +f 2403 2359 2366 +f 2214 2226 2213 +f 2268 2253 2276 +f 889 2200 2179 +f 2200 2182 2179 +f 2200 2221 2182 +f 2221 2208 2182 +f 2314 2265 2246 +f 2314 2313 2265 +f 2339 2374 2361 +f 2478 2434 2379 +f 2205 2217 2199 +f 2208 2259 2246 +f 2259 2275 2246 +f 2314 2321 2313 +f 2313 2347 2339 +f 2347 2374 2339 +f 2374 2399 2361 +f 153 154 2426 +f 154 2306 2426 +f 2385 2359 2403 +f 2221 2259 2208 +f 2306 2357 2403 +f 2357 2385 2403 +f 2237 2256 2231 +f 2172 2180 889 +f 2180 2200 889 +f 2200 2201 2221 +f 2246 2291 2314 +f 2374 2444 2399 +f 571 555 2311 +f 2192 2205 2199 +f 2173 2180 2172 +f 2279 2246 2275 +f 2279 2291 2246 +f 2292 2314 2291 +f 2321 2362 2313 +f 2362 2347 2313 +f 2347 2389 2374 +f 2444 955 2437 +f 2292 2291 2279 +f 2452 2444 2374 +f 2054 2356 1996 +f 2338 2306 154 +f 2186 2192 2191 +f 2193 2201 2200 +f 2259 2221 2201 +f 2247 2259 2201 +f 2452 955 2444 +f 2278 2277 2251 +f 2338 2357 2306 +f 2181 2186 2185 +f 2276 2326 2281 +f 2432 2457 2446 +f 2198 2201 2193 +f 2198 2232 2201 +f 2232 2247 2201 +f 2389 2452 2374 +f 2452 1630 955 +f 1403 1749 1444 +f 1555 1996 1561 +f 2357 2427 2385 +f 2385 2428 230 +f 2409 2415 2424 +f 2304 2331 2299 +f 2193 2200 2180 +f 2445 2452 2389 +f 1565 1759 1927 +f 2380 1544 2370 +f 2338 2427 2357 +f 2427 2428 2385 +f 230 222 253 +f 2202 2198 2193 +f 2202 2209 2198 +f 2209 2241 2198 +f 2241 2232 2198 +f 2266 2275 2259 +f 2365 1340 1128 +f 2415 518 2424 +f 2338 170 2427 +f 170 2428 2427 +f 2181 2185 2177 +f 2196 2195 2189 +f 2183 2193 2180 +f 2453 1630 2452 +f 2197 2214 2189 +f 2401 2409 2414 +f 822 2220 2197 +f 1210 2361 2388 +f 2187 2193 2183 +f 2187 2202 2193 +f 2266 2279 2275 +f 2279 2300 2292 +f 2375 2347 2362 +f 2375 2390 2347 +f 2390 2389 2347 +f 2453 2452 2445 +f 1347 1630 2453 +f 1630 1347 522 +f 2220 2206 2197 +f 2262 2350 2286 +f 170 254 2428 +f 2457 1973 2446 +f 1973 1365 2446 +f 2174 2183 2180 +f 2194 2202 2187 +f 2222 2241 2209 +f 2222 2260 2241 +f 2266 2259 2247 +f 2390 2445 2389 +f 2264 2251 825 +f 2363 2368 2351 +f 2326 2393 2341 +f 1855 1850 1851 +f 2210 2209 2202 +f 2210 2222 2209 +f 2261 2260 2222 +f 2280 2279 2266 +f 2280 2300 2279 +f 251 263 2359 +f 2277 2304 2299 +f 2220 2230 2206 +f 2202 2194 2210 +f 2213 2243 2234 +f 2328 2327 2322 +f 2294 2310 2293 +f 2214 2196 2189 +f 2196 2213 2219 +f 2224 2222 2210 +f 2421 2390 2375 +f 2206 2230 2214 +f 2194 2203 2210 +f 2224 2261 2222 +f 2421 2445 2390 +f 2322 2327 2318 +f 2393 2408 2341 +f 1365 1973 510 +f 2216 2210 2203 +f 2216 2224 2210 +f 2266 2308 2280 +f 2280 2308 2300 +f 2407 2421 2375 +f 2175 2183 2174 +f 2194 2190 2203 +f 2454 2445 2421 +f 522 1347 523 +f 2456 2455 2423 +f 823 2197 2178 +f 2281 2333 2287 +f 2188 2187 2183 +f 2188 2190 2194 +f 2187 2188 2194 +f 2308 2315 2300 +f 2407 2375 2362 +f 2443 2420 2503 +f 2420 2411 2503 +f 2411 1493 2503 +f 1493 1487 2503 +f 1487 1318 2503 +f 1318 1320 2503 +f 1320 2443 2503 diff --git a/gradio/media_assets/models3d/Duck.glb b/gradio/media_assets/models3d/Duck.glb new file mode 100644 index 0000000..217170d Binary files /dev/null and b/gradio/media_assets/models3d/Duck.glb differ diff --git a/gradio/media_assets/models3d/Fox.gltf b/gradio/media_assets/models3d/Fox.gltf new file mode 100644 index 0000000..ff3115c --- /dev/null +++ b/gradio/media_assets/models3d/Fox.gltf @@ -0,0 +1,1777 @@ +{ + "asset": { + "copyright": "CC-BY 4.0 Model by PixelMannen https://opengameart.org/content/fox-and-shiba and @tomkranis https://sketchfab.com/3d-models/low-poly-fox-by-pixelmannen-animated-371dea88d7e04a76af5763f2a36866bc and @AsoboStudio with @scurest https://github.com/KhronosGroup/glTF-Sample-Models/pull/150#issuecomment-406300118", + "version": "2.0" + }, + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 1728, + "type": "VEC3", + "byteOffset": 0, + "min": [ + -12.592718124389648, + -0.12174476683139801, + -88.09500122070312 + ], + "max": [ + 12.592718124389648, + 78.90718841552734, + 66.62486267089844 + ] + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 1728, + "type": "VEC2", + "byteOffset": 0 + }, + { + "bufferView": 1, + "componentType": 5123, + "count": 1728, + "type": "VEC4", + "byteOffset": 13824 + }, + { + "bufferView": 2, + "byteOffset": 0, + "componentType": 5126, + "count": 1728, + "type": "VEC4" + }, + { + "bufferView": 3, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "type": "MAT4" + }, + { + "bufferView": 4, + "byteOffset": 0, + "componentType": 5126, + "count": 83, + "type": "SCALAR", + "min": [ + 0.0 + ], + "max": [ + 3.4166667461395264 + ] + }, + { + "bufferView": 5, + "byteOffset": 0, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 1328, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 2656, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 3984, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 5312, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 6640, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 7968, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 9296, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 10624, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 11952, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 13280, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 14608, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 15936, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 17264, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 18592, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 19920, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 21248, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 22576, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 23904, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 6, + "byteOffset": 0, + "componentType": 5126, + "count": 83, + "type": "VEC3" + }, + { + "bufferView": 5, + "byteOffset": 25232, + "componentType": 5126, + "count": 83, + "type": "VEC4" + }, + { + "bufferView": 4, + "byteOffset": 332, + "componentType": 5126, + "count": 18, + "type": "SCALAR", + "min": [ + 0.0 + ], + "max": [ + 0.7083333134651184 + ] + }, + { + "bufferView": 5, + "byteOffset": 26560, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 26848, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 27136, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 27424, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 27712, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 28000, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 28288, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 28576, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 28864, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 29152, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 29440, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 29728, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 30016, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 30304, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 30592, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 30880, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 31168, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 31456, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 31744, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 6, + "byteOffset": 996, + "componentType": 5126, + "count": 18, + "type": "VEC3" + }, + { + "bufferView": 5, + "byteOffset": 32032, + "componentType": 5126, + "count": 18, + "type": "VEC4" + }, + { + "bufferView": 4, + "byteOffset": 404, + "componentType": 5126, + "count": 25, + "type": "SCALAR", + "min": [ + 0.0 + ], + "max": [ + 1.1583333015441895 + ] + }, + { + "bufferView": 5, + "byteOffset": 32320, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 32720, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 33120, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 33520, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 33920, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 34320, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 34720, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 35120, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 35520, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 35920, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 36320, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 36720, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 37120, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 37520, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 37920, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 38320, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 38720, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 39120, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 5, + "byteOffset": 39520, + "componentType": 5126, + "count": 25, + "type": "VEC4" + }, + { + "bufferView": 6, + "byteOffset": 1212, + "componentType": 5126, + "count": 25, + "type": "VEC3" + }, + { + "bufferView": 5, + "byteOffset": 39920, + "componentType": 5126, + "count": 25, + "type": "VEC4" + } + ], + "animations": [ + { + "channels": [ + { + "sampler": 0, + "target": { + "node": 8, + "path": "rotation" + } + }, + { + "sampler": 1, + "target": { + "node": 7, + "path": "rotation" + } + }, + { + "sampler": 2, + "target": { + "node": 11, + "path": "rotation" + } + }, + { + "sampler": 3, + "target": { + "node": 10, + "path": "rotation" + } + }, + { + "sampler": 4, + "target": { + "node": 9, + "path": "rotation" + } + }, + { + "sampler": 5, + "target": { + "node": 14, + "path": "rotation" + } + }, + { + "sampler": 6, + "target": { + "node": 13, + "path": "rotation" + } + }, + { + "sampler": 7, + "target": { + "node": 12, + "path": "rotation" + } + }, + { + "sampler": 8, + "target": { + "node": 6, + "path": "rotation" + } + }, + { + "sampler": 9, + "target": { + "node": 5, + "path": "rotation" + } + }, + { + "sampler": 10, + "target": { + "node": 17, + "path": "rotation" + } + }, + { + "sampler": 11, + "target": { + "node": 16, + "path": "rotation" + } + }, + { + "sampler": 12, + "target": { + "node": 15, + "path": "rotation" + } + }, + { + "sampler": 13, + "target": { + "node": 20, + "path": "rotation" + } + }, + { + "sampler": 14, + "target": { + "node": 19, + "path": "rotation" + } + }, + { + "sampler": 15, + "target": { + "node": 18, + "path": "rotation" + } + }, + { + "sampler": 16, + "target": { + "node": 24, + "path": "rotation" + } + }, + { + "sampler": 17, + "target": { + "node": 23, + "path": "rotation" + } + }, + { + "sampler": 18, + "target": { + "node": 22, + "path": "rotation" + } + }, + { + "sampler": 19, + "target": { + "node": 4, + "path": "translation" + } + }, + { + "sampler": 20, + "target": { + "node": 4, + "path": "rotation" + } + } + ], + "samplers": [ + { + "input": 5, + "output": 6 + }, + { + "input": 5, + "output": 7 + }, + { + "input": 5, + "output": 8 + }, + { + "input": 5, + "output": 9 + }, + { + "input": 5, + "output": 10 + }, + { + "input": 5, + "output": 11 + }, + { + "input": 5, + "output": 12 + }, + { + "input": 5, + "output": 13 + }, + { + "input": 5, + "output": 14 + }, + { + "input": 5, + "output": 15 + }, + { + "input": 5, + "output": 16 + }, + { + "input": 5, + "output": 17 + }, + { + "input": 5, + "output": 18 + }, + { + "input": 5, + "output": 19 + }, + { + "input": 5, + "output": 20 + }, + { + "input": 5, + "output": 21 + }, + { + "input": 5, + "output": 22 + }, + { + "input": 5, + "output": 23 + }, + { + "input": 5, + "output": 24 + }, + { + "input": 5, + "output": 25 + }, + { + "input": 5, + "output": 26 + } + ], + "name": "Survey" + }, + { + "channels": [ + { + "sampler": 0, + "target": { + "node": 8, + "path": "rotation" + } + }, + { + "sampler": 1, + "target": { + "node": 7, + "path": "rotation" + } + }, + { + "sampler": 2, + "target": { + "node": 11, + "path": "rotation" + } + }, + { + "sampler": 3, + "target": { + "node": 10, + "path": "rotation" + } + }, + { + "sampler": 4, + "target": { + "node": 9, + "path": "rotation" + } + }, + { + "sampler": 5, + "target": { + "node": 14, + "path": "rotation" + } + }, + { + "sampler": 6, + "target": { + "node": 13, + "path": "rotation" + } + }, + { + "sampler": 7, + "target": { + "node": 12, + "path": "rotation" + } + }, + { + "sampler": 8, + "target": { + "node": 6, + "path": "rotation" + } + }, + { + "sampler": 9, + "target": { + "node": 5, + "path": "rotation" + } + }, + { + "sampler": 10, + "target": { + "node": 17, + "path": "rotation" + } + }, + { + "sampler": 11, + "target": { + "node": 16, + "path": "rotation" + } + }, + { + "sampler": 12, + "target": { + "node": 15, + "path": "rotation" + } + }, + { + "sampler": 13, + "target": { + "node": 20, + "path": "rotation" + } + }, + { + "sampler": 14, + "target": { + "node": 19, + "path": "rotation" + } + }, + { + "sampler": 15, + "target": { + "node": 18, + "path": "rotation" + } + }, + { + "sampler": 16, + "target": { + "node": 24, + "path": "rotation" + } + }, + { + "sampler": 17, + "target": { + "node": 23, + "path": "rotation" + } + }, + { + "sampler": 18, + "target": { + "node": 22, + "path": "rotation" + } + }, + { + "sampler": 19, + "target": { + "node": 4, + "path": "translation" + } + }, + { + "sampler": 20, + "target": { + "node": 4, + "path": "rotation" + } + } + ], + "samplers": [ + { + "input": 27, + "output": 28 + }, + { + "input": 27, + "output": 29 + }, + { + "input": 27, + "output": 30 + }, + { + "input": 27, + "output": 31 + }, + { + "input": 27, + "output": 32 + }, + { + "input": 27, + "output": 33 + }, + { + "input": 27, + "output": 34 + }, + { + "input": 27, + "output": 35 + }, + { + "input": 27, + "output": 36 + }, + { + "input": 27, + "output": 37 + }, + { + "input": 27, + "output": 38 + }, + { + "input": 27, + "output": 39 + }, + { + "input": 27, + "output": 40 + }, + { + "input": 27, + "output": 41 + }, + { + "input": 27, + "output": 42 + }, + { + "input": 27, + "output": 43 + }, + { + "input": 27, + "output": 44 + }, + { + "input": 27, + "output": 45 + }, + { + "input": 27, + "output": 46 + }, + { + "input": 27, + "output": 47 + }, + { + "input": 27, + "output": 48 + } + ], + "name": "Walk" + }, + { + "channels": [ + { + "sampler": 0, + "target": { + "node": 8, + "path": "rotation" + } + }, + { + "sampler": 1, + "target": { + "node": 7, + "path": "rotation" + } + }, + { + "sampler": 2, + "target": { + "node": 11, + "path": "rotation" + } + }, + { + "sampler": 3, + "target": { + "node": 10, + "path": "rotation" + } + }, + { + "sampler": 4, + "target": { + "node": 9, + "path": "rotation" + } + }, + { + "sampler": 5, + "target": { + "node": 14, + "path": "rotation" + } + }, + { + "sampler": 6, + "target": { + "node": 13, + "path": "rotation" + } + }, + { + "sampler": 7, + "target": { + "node": 12, + "path": "rotation" + } + }, + { + "sampler": 8, + "target": { + "node": 6, + "path": "rotation" + } + }, + { + "sampler": 9, + "target": { + "node": 5, + "path": "rotation" + } + }, + { + "sampler": 10, + "target": { + "node": 17, + "path": "rotation" + } + }, + { + "sampler": 11, + "target": { + "node": 16, + "path": "rotation" + } + }, + { + "sampler": 12, + "target": { + "node": 15, + "path": "rotation" + } + }, + { + "sampler": 13, + "target": { + "node": 20, + "path": "rotation" + } + }, + { + "sampler": 14, + "target": { + "node": 19, + "path": "rotation" + } + }, + { + "sampler": 15, + "target": { + "node": 18, + "path": "rotation" + } + }, + { + "sampler": 16, + "target": { + "node": 24, + "path": "rotation" + } + }, + { + "sampler": 17, + "target": { + "node": 23, + "path": "rotation" + } + }, + { + "sampler": 18, + "target": { + "node": 22, + "path": "rotation" + } + }, + { + "sampler": 19, + "target": { + "node": 4, + "path": "translation" + } + }, + { + "sampler": 20, + "target": { + "node": 4, + "path": "rotation" + } + } + ], + "samplers": [ + { + "input": 49, + "output": 50 + }, + { + "input": 49, + "output": 51 + }, + { + "input": 49, + "output": 52 + }, + { + "input": 49, + "output": 53 + }, + { + "input": 49, + "output": 54 + }, + { + "input": 49, + "output": 55 + }, + { + "input": 49, + "output": 56 + }, + { + "input": 49, + "output": 57 + }, + { + "input": 49, + "output": 58 + }, + { + "input": 49, + "output": 59 + }, + { + "input": 49, + "output": 60 + }, + { + "input": 49, + "output": 61 + }, + { + "input": 49, + "output": 62 + }, + { + "input": 49, + "output": 63 + }, + { + "input": 49, + "output": 64 + }, + { + "input": 49, + "output": 65 + }, + { + "input": 49, + "output": 66 + }, + { + "input": 49, + "output": 67 + }, + { + "input": 49, + "output": 68 + }, + { + "input": 49, + "output": 69 + }, + { + "input": 49, + "output": 70 + } + ], + "name": "Run" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 20736, + "byteStride": 12 + }, + { + "buffer": 0, + "byteOffset": 20736, + "byteLength": 27648, + "byteStride": 8 + }, + { + "buffer": 0, + "byteOffset": 48384, + "byteLength": 27648, + "byteStride": 16 + }, + { + "buffer": 0, + "byteOffset": 76032, + "byteLength": 1536 + }, + { + "buffer": 0, + "byteOffset": 77568, + "byteLength": 504, + "byteStride": 4 + }, + { + "buffer": 0, + "byteOffset": 78072, + "byteLength": 40320, + "byteStride": 16 + }, + { + "buffer": 0, + "byteOffset": 118392, + "byteLength": 1512, + "byteStride": 12 + } + ], + "buffers": [ + { + "uri": "data:application/octet-stream;base64,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AgACAAoABAACAAIAAgAQAAIAAgACABAAAgACAAIADQACAAIABAAFAAcACgAKAAQAAgACAAoAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABQAEAAIAAgAKAAIAAgACAAUABAACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIAAgAQAAIAAgADAAIAAgACAAIAEAARAAIABAAFAAcACgAEAAoABwACAAoABAACAAIAAgANAAIAAgACABAAAgACAAIADQACAAIAAwACAAIAAgADAAIAAgACAAIAEAACAAIAAgAQABEAAgADAAIAAgACAAIAAgACAAIACgACAAIAAgAKAAQAAgACAAoAAgACAAIADwACAAIAAgAPAA4AAgACAA8AAgACAAIADwACAAIAAgAPAAIAAgACAA8AAgACAAIADwACAAIAAgAPAAIAAgACAA8AAgACAAIADwACAAIAAgAPAAIAAgACAA8AAgACAAIADwACAAIAAgAPAAIAAgACAA8AAgACAAIAAgAUAAIAAgACAA0AAgACAAIAEAAUAAIACgACAAIAAgAFAAQAAgACAAUABAACAAIABQAEAAIAAgAEAAUABwAKAAoAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABwACAAIAAgAHAAgAAgACAAcAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIAAgAUAAIAAgACAA0AAgACAAIAFAACAAIAAwACAAIAAgADAAIAAgACAAMAAgACAAIAAwACAAIAAgACABQAAgACAAMAAgACAAIAAgAQABQAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABQAAgACAAIAEAAUAAIAAgAUAAIAAgAVABQAAgACAAIAFAACAAIABwACAAIAAgAIAAcAAgACAAcACAACAAIADwACAAIAAgAPAAIAAgACAA8AAgACAAIADwACAAIAAgAPAAIAAgACAA8AAgACAAIADwAOAAIAAgAPAAIAAgACAA8AAgACAAIADwACAAIAAgAPAAIAAgACAA8AAgACAAIAAwACAAIAAgAHAAQAAgACAAMAFAACAAIAAgAUAAIAAgACAA0AAgACAAIAFAACAAIABAAFAAcACgAHAAIAAgACAAcABAACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABQAEAAIAAgAFAAQAAgACAAcAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIAAgAUAAIAAgACABQAFQACAAMAAgACAAIABAAFAAcACgAHAAQAAgACAAQACgAHAAIAAgANAAIAAgACAA0AAgACAAIAFAACAAIAAwACAAIAAgACABQAAgACAAMAAgACAAIAAgAUABUAAgACABQAAgACAAIAAgACAAIABwACAAIAAgAHAAIAAgACAAcABAACAAIADwACAAIAAgAPAAIAAgACAA8ADgACAAIADwACAAIAAgAPAAIAAgACAA8AAgACAAIADwACAAIAAgAPAAIAAgACAA8AAgACAAIADwACAAIAAgAPAAIAAgACAA8AAgACAAIADwACAAIAAgAPAAIAAgACAA8AAgACAAIABwACAAIAAgAEAAUABwAKAAUABAACAAIABQAEAAIAAgAFAAQAAgACAAcAAgACAAIACgACAAIAAgAKAAIAAgACAAoAAgACAAIACgACAAIAAgAKAAQAAgACAAoAAgACAAIACgACAAIAAgALAAoAAgACAAoACwACAAIACgALAAIAAgAKAAIAAgACAAoAAgACAAIABwACAAIAAgAHAAQAAgACAAcAAgACAAIABwACAAIAAgAHAAIAAgACAAcAAgACAAIACgALAAIAAgAKAAIAAgACAAoABAACAAIACgAEAAIAAgAEAAoABwACAAoACwACAAIABwAIAAIAAgAEAAoABwACAAcABAACAAIABwAEAAIAAgAHAAIAAgACAAcACAACAAIABwACAAIAAgAHAAIAAgACAAcACAACAAIABwAIAAIAAgAIAAcAAgACAAcAAgACAAIAAwAUAAIAAgACABQAAgACAAIAFAACAAIAAgAUAAIAAgADAAIAAgACAAMAFAACAAIABAAKAAcAAgADAAIAAgACAAMAAgACAAIAAwACAAIAAgAEAAoABwACAAcABAACAAIABwAEAAIAAgADAAIAAgACAAMAAgACAAIAAwAQAAIAAgADAAIAAgACAAIAEAACAAIAAgAQAAIAAgACABAAAgACAAMAEAACAAIABAAKAAcAAgADAAIAAgACAAMAAgACAAIAAwACAAIAAgADAAIAAgACAAoABAACAAIACgAEAAIAAgAEAAoABwACAAMAAgACAAIAAwACAAIAAgACAAIAAgACAAIAAgACAAIAAwACAAIAAgACAAIAAgACAAIAAgACAAIAAwACAAIAAgACABQAFQACAAIAAgACAAIAAgAQAAIAAgACABAAEQACAAIAAgACAAIABQAEAAIAAgAFAAQAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAUABAACAAIABQAEAAIAAgAFAAQAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAUABAACAAIABQAEAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAFAAQAAgACAAUABAACAAIABQAEAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAFAAQAAgACAAUABAACAAIAAgANAAIAAgACAA0AAgACAA0ADgACAAIADQAOAAIAAgACAA0AAgACAA0ADgACAAIACgAEAAIAAgADABAAAgACAAMAEAACAAIAAwAQAAIAAgAKAAQAAgACAAoABAACAAIAAwACAAIAAgADAAIAAgACAAMAAgACAAIAAwACAAIAAgACAAIAAgACAAMAAgACAAIAAgANAAIAAgACABAAAgACAAIADQACAAIAAgACAAIAAgACABAAAgACAAIAEAACAAIAAgAQAAIAAgACABAAAgACAAIADQACAAIACgAEAAIAAgAKAAQAAgACAAoACwACAAIABAACAAIAAgAEAAIAAgACAAUABAACAAIABAACAAIAAgAEAAIAAgACAAUABAACAAIABgACAAIAAgAFAAQAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABQAEAAIAAgAFAAQAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABQAEAAIAAgAFAAQAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIAAgANAAIAAgACAA0AAgACAA0ADgACAAIAAgAQAAIAAgACABAAAgACABEAEAACAAIADQAOAAIAAgANAA4AAgACAA8ADgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIAAgACAAIAAgACAAIAAgACAAIADQACAAIAAgANAAIAAgACAA0AAgACAAIADQACAAIAAgAQAAIAAgACABAAAgACABAAEQACAAIAAgAQAAIAAgACABAAAgACABEAEAACAAIAAgAQAAIAAgACABAAAgACABEAEAACAAIAAgAQAAIAAgACABAAAgACABEAEAACAAIAEQASAAIAAgARABIAAgACABEAEgACAAIAEQASAAIAAgARABIAAgACABEAEgACAAIAEQAQAAIAAgACABAAEQACABEAEgACAAIAEQASAAIAAgARABIAAgACABEAEgACAAIAEQASAAIAAgARABIAAgACABEAEgACAAIAEQAQAAIAAgARABAAAgACABEAAgACAAIAEAARAAIAAgARABAAAgACABEAAgACAAIAEQASAAIAAgARABIAAgACABEAEgACAAIACgALAAIAAgAKAAsAAgACAAsAAgACAAIACwAKAAIAAgAKAAsAAgACAAsAAgACAAIABwACAAIAAgAHAAIAAgACAAcAAgACAAIABwACAAIAAgAHAAIAAgACAAcAAgACAAIABAAKAAcAAgAKAAQAAgACAAoACwACAAIACgALAAIAAgAKAAsAAgACAAsAAgACAAIADQAOAAIAAgACAA0AAgACAA0ADgACAAIADQAOAAIAAgAPAA4AAgACAA0ADgACAAIAAgANAAIAAgACAA0AAgACAA0ADgACAAIADQAOAAIAAgANAA4AAgACAAIADQACAAIADwAOAAIAAgAPAA4AAgACAA8AAgACAAIADwAOAAIAAgAPAA4AAgACAA8AAgACAAIADwAOAAIAAgAPAA4AAgACAA8AAgACAAIADwAOAAIAAgAPAA4AAgACAA8AAgACAAIADwAOAAIAAgAPAA4AAgACAA8AAgACAAIAEQAQAAIAAgACABAAAgACAAIAEAARAAIAAwACAAIAAgACAAIAAgACAAIAAgACAAIAAgANAAIAAgACAA0AAgACAAIADQACAAIAAgANAAIAAgACAA0AAgACAAIADQACAAIAAgANAAIAAgACAAIAAgACAAIAEAACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIAAgANAAIAAgACABAAAgACAAIADQACAAIABQAEAAIAAgAEAAIAAgACAAUABAACAAIABQAEAAIAAgAEAAIAAgACAAUABAACAAIACgALAAIAAgAKAAQAAgACAAoACwACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAFAAQAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIACgALAAIAAgAKAAQAAgACAAoACwACAAIACgACAAIAAgAKAAIAAgACAAoAAgACAAIAAwAQAAIAAgADAAIAAgACAAoABAACAAIADQAOAAIAAgACAA0AAgACAAIADQACAAIAEAARAAIAAgACABAAAgACABEAEAACAAIADwAOAAIAAgANAA4AAgACAA8ADgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIAAgANAAIAAgACAAIAAgACAAIADQACAAIAEQAQAAIAAgACABAAAgACABAAEQACAAIAAwACAAIAAgADAAIAAgACAAIAAgACAAIAEQAQAAIAAgACABAAAgACABEAEAACAAIAEQAQAAIAAgACABAAAgACABEAEAACAAIAEQAQAAIAAgACABAAAgACABEAEAACAAIAEQASAAIAAgACABAAEQACABEAEgACAAIACgALAAIAAgAEAAoABwACAAoACwACAAIACwACAAIAAgAKAAsAAgACAAsACgACAAIADwAOAAIAAgANAA4AAgACAA8ADgACAAIADwAOAAIAAgANAA4AAgACAA8ADgACAAIADwAOAAIAAgANAA4AAgACAA8ADgACAAIADwAOAAIAAgANAA4AAgACAA0ADgACAAIADwACAAIAAgAPAA4AAgACAA8AAgACAAIADwACAAIAAgAPAA4AAgACAA8AAgACAAIADwACAAIAAgAPAA4AAgACAA8AAgACAAIAAgANAAIAAgANAA4AAgACAAIADQACAAIADQAOAAIAAgANAA4AAgACAAIADQACAAIABwAEAAIAAgADABQAAgACAAMAFAACAAIAAwAUAAIAAgAHAAQAAgACAAcABAACAAIAAgACAAIAAgADAAIAAgACAAMAAgACAAIAAwACAAIAAgADAAIAAgACAAIAAgACAAIABAACAAIAAgAHAAIAAgACAAcAAgACAAIABwACAAIAAgAEAAIAAgACAAQAAgACAAIAAgANAAIAAgACAA0AAgACAAIAFAACAAIAAgACAAIAAgACABQAAgACAAIAFAACAAIAAgAUAAIAAgACAA0AAgACAAIAFAACAAIABwAEAAIAAgAHAAgAAgACAAcABAACAAIABAACAAIAAgAFAAQAAgACAAQAAgACAAIABAACAAIAAgAFAAQAAgACAAQAAgACAAIABgACAAIAAgAGAAIAAgACAAUABAACAAIABAACAAIAAgAFAAQAAgACAAUABAACAAIABQAEAAIAAgAKAAIAAgACAAQAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABQAEAAIAAgAGAAIAAgACAAUABAACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABQAEAAIAAgAGAAIAAgACAAUABAACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIAAgANAAIAAgANAA4AAgACAAIADQACAAIAAgAUAAIAAgAVABQAAgACAAIAFAACAAIADQAOAAIAAgAPAA4AAgACAA0ADgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIAAgACAAIAAgACAA0AAgACAAIAAgACAAIAAgANAAIAAgACAA0AAgACAAIADQACAAIAAgAUAAIAAgAUABUAAgACAAIAFAACAAIAAgAUAAIAAgAVABQAAgACAAIAFAACAAIAAgAUAAIAAgAVABQAAgACAAIAFAACAAIAAgAUAAIAAgAVABQAAgACAAIAFAACAAIAFQAWAAIAAgAVABYAAgACABUAFgACAAIAFQAUAAIAAgAVABYAAgACAAIAFAAVAAIAFQAWAAIAAgAVABYAAgACABUAFgACAAIAFQAWAAIAAgAVABYAAgACABUAFgACAAIAFQAUAAIAAgAVAAIAAgACABUAFAACAAIAFAAVAAIAAgAVAAIAAgACABUAFAACAAIAFQAWAAIAAgAVABYAAgACABUAFgACAAIABwAIAAIAAgAIAAIAAgACAAcACAACAAIACAAHAAIAAgAIAAIAAgACAAcACAACAAIABAAKAAcAAgAHAAgAAgACAAcABAACAAIABwAIAAIAAgAIAAIAAgACAAcACAACAAIADQAOAAIAAgAPAA4AAgACAA0ADgACAAIADQAOAAIAAgAPAA4AAgACAA0ADgACAAIADQAOAAIAAgAPAA4AAgACAA8ADgACAAIADwAOAAIAAgANAA4AAgACAA0ADgACAAIADwAOAAIAAgAPAAIAAgACAA8ADgACAAIADwAOAAIAAgAPAAIAAgACAA8ADgACAAIADwAOAAIAAgAPAAIAAgACAA8ADgACAAIADwAOAAIAAgAPAAIAAgACAA8ADgACAAIADwAOAAIAAgAPAAIAAgACAA8ADgACAAIAFQAUAAIAAgACABQAFQACAAIAFAACAAIAAwACAAIAAgACAAIAAgACAAIAAgACAAIAAgANAAIAAgACAA0AAgACAAIADQACAAIAAgANAAIAAgACAA0AAgACAAIADQACAAIAAgANAAIAAgACABQAAgACAAIAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIAAgANAAIAAgACAA0AAgACAAIAFAACAAIABQAEAAIAAgAFAAQAAgACAAQAAgACAAIABQAEAAIAAgAEAAIAAgACAAUABAACAAIABwAIAAIAAgAHAAgAAgACAAcABAACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAUABAACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABwAIAAIAAgAHAAgAAgACAAcABAACAAIABwACAAIAAgAHAAIAAgACAAcAAgACAAIAAwAUAAIAAgAHAAQAAgACAAMAAgACAAIADQAOAAIAAgACAA0AAgACAAIADQACAAIAFAAVAAIAAgAVABQAAgACAAIAFAACAAIADwAOAAIAAgAPAA4AAgACAA0ADgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIAAgANAAIAAgACAA0AAgACAAIAAgACAAIAFQAUAAIAAgAUABUAAgACAAIAFAACAAIAAwACAAIAAgACAAIAAgACAAMAAgACAAIAFQAUAAIAAgAVABQAAgACAAIAFAACAAIAFQAUAAIAAgAVABQAAgACAAIAFAACAAIAFQAUAAIAAgAVABQAAgACAAIAFAACAAIAFQAWAAIAAgAVABYAAgACABUAFgACAAIABwAIAAIAAgAEAAoABwACAAcACAACAAIACAACAAIAAgAIAAcAAgACAAcACAACAAIADwAOAAIAAgAPAA4AAgACAA0ADgACAAIADwAOAAIAAgAPAA4AAgACAA0ADgACAAIADwAOAAIAAgAPAA4AAgACAA0ADgACAAIADQAOAAIAAgANAA4AAgACAA8ADgACAAIADwAOAAIAAgAPAA4AAgACAA0ADgACAAIADwACAAIAAgAPAAIAAgACAA8ADgACAAIADwACAAIAAgAPAAIAAgACAA8ADgACAAIADwACAAIAAgAPAAIAAgACAA8ADgACAAIAEQASAAIAAgARABIAAgACAAIAEAARAAIAAgAQABEAAgACABAAAgACABEAEgACAAIABAACAAIAAgADAAIAAgACAAMAAgACAAIAAwACAAIAAgAEAAIAAgACAAQAAgACAAIACgAEAAIAAgADAAIAAgACAAMAAgACAAIAAwACAAIAAgAEAAIAAgACAAoABAACAAIABwAEAAIAAgAHAAgAAgACAAcAAgACAAIABAACAAIAAgADAAIAAgACAAMAAgACAAIAAwACAAIAAgAHAAQAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAMAAgACAAIAAwACAAIAAgADAAIAAgACAAQAAgACAAIACgACAAIAAgAKAAIAAgACAAoAAgACAAIACgACAAIAAgAKAAIAAgACAAoAAgACAAIABwAIAAIAAgAIAAcAAgACAAcAAgACAAIABwACAAIAAgAHAAIAAgACAAcACAACAAIABwAIAAIAAgAHAAgAAgACAAcAAgACAAIACgACAAIAAgAKAAsAAgACAAoACwACAAIACgALAAIAAgAKAAIAAgACAAoAAgACAAIACgACAAIAAgAKAAsAAgACAAsACgACAAIACwAKAAIAAgAKAAIAAgACAAoAAgACAAIABwACAAIAAgAHAAIAAgACAAgABwACAAIACAAHAAIAAgAHAAgAAgACAAcAAgACAAIACwAKAAIAAgAKAAsAAgACAAoACwACAAIACAAHAAIAAgAHAAgAAgACAAcACAACAAIACgALAAIAAgAKAAIAAgACAAsACgACAAIACgACAAIAAgAKAAsAAgACAAoAAgACAAIACgACAAIAAgAKAAIAAgACAAoAAgACAAIABAACAAIAAgAEAAIAAgACAAoAAgACAAIACgACAAIAAgAKAAIAAgACAAQAAgACAAIACgACAAIAAgAKAAQAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAoAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIABwACAAIAAgAHAAIAAgACAAcACAACAAIABwAIAAIAAgAHAAIAAgACAAcAAgACAAIACgAEAAIAAgAKAAIAAgACAAoACwACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAcABAACAAIABwAEAAIAAgAHAAIAAgACAAQAAgACAAIAEQASAAIAAgARABIAAgACABEAEAACAAIAEQAQAAIAAgARABAAAgACABEAEgACAAIAEQASAAIAAgARABIAAgACAAIAEAACAAIAAgAQAAIAAgARABAAAgACABEAEgACAAIAEQASAAIAAgARABIAAgACABEAEAACAAIAEQAQAAIAAgARABAAAgACABEAEgACAAIAFQAWAAIAAgAVABQAAgACABUAFAACAAIAFQAUAAIAAgAVABYAAgACABUAFgACAAIAFQAWAAIAAgAVABQAAgACABUAFAACAAIAFQAUAAIAAgAVABYAAgACABUAFgACAAIAFQAUAAIAAgAVABYAAgACABQAFQACAAIAEQAQAAIAAgAQABEAAgACABEAEgACAAIAFQAWAAIAAgACABQAAgACAAIAFAAVAAIAAgAUABUAAgAVABYAAgACABUAFgACAAIAFQAWAAIAAgAVABQAAgACAAIAFAACAAIAAgAUAAIAAgAVABYAAgACABUAFgACAAIAAgAUABUAAgAVABYAAgACABUAFgACAAIAAgANAAIAAgANAA4AAgACAA0ADgACAAIADQAOAAIAAgACAA0AAgACAAIADQACAAIAAgANAAIAAgANAA4AAgACAA0ADgACAAIAAgANAAIAAgANAA4AAgACAA0ADgACAAIAAgANAAIAAgANAA4AAgACAA0ADgACAAIABAACAAIAAgADAAIAAgACAAMAAgACAAIABAACAAIAAgADAAIAAgACAAMAAgACAAIAAwACAAIAAgADAAIAAgACAAQAAgACAAIABAACAAIAAgAEAAIAAgACAAMAAgACAAIAAwACAAIAAgADAAIAAgACAAIAEAACAAIAAgAQAAIAAgACAAIAAgACAAMAAgACAAIAAwACAAIAAgADABAAAgACAAIAEAACAAIAAgAQAAIAAgACABAAAgACAAMAAgACAAIAAgAQAAIAAgADAAIAAgACAAMAAgACAAIAAgAQAAIAAgADABAAAgACAAMAEAACAAIAAgAUAAIAAgADAAIAAgACAAMAAgACAAIAAwACAAIAAgAEAAIAAgACAAQAAgACAAIABAACAAIAAgADAAIAAgACAAMAAgACAAIAAwACAAIAAgACAAIAAgACAAIAFAACAAIAAgAUAAIAAgADAAIAAgACAAMAAgACAAIAAgAUAAIAAgADABQAAgACAAMAFAACAAIAAwACAAIAAgACABQAAgACAAIAFAACAAIAAgAUAAIAAgADABQAAgACAAMAAgACAAIABAACAAIAAgAHAAIAAgACAAUABAACAAIABQAEAAIAAgAFAAQAAgACAAQAAgACAAIABQAEAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAFAAQAAgACAAUABAACAAIABQAEAAIAAgAFAAQAAgACAAoAAgACAAIACgACAAIAAgAKAAIAAgACAAUABAACAAIABQAEAAIAAgAHAAIAAgACAAcAAgACAAIABwACAAIAAgAFAAQAAgACAAUABAACAAIABQAEAAIAAgAFAAQAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAUABAACAAIABgACAAIAAgAFAAQAAgACAAUABAACAAIABgACAAIAAgAFAAQAAgACAAUABAACAAIAEQASAAIAAgARAAIAAgACABIAEQACAAIAEQASAAIAAgARABIAAgACABMAEgACAAIAEQASAAIAAgASABEAAgACABIAEwACAAIAEwASAAIAAgASABMAAgACABMAAgACAAIAEQASAAIAAgARABIAAgACABMAEgACAAIAEgARAAIAAgARABIAAgACABIAEwACAAIAEQASAAIAAgARABIAAgACABIAEwACAAIAEQASAAIAAgARABIAAgACABIAEwACAAIAEwACAAIAAgATABIAAgACABMAAgACAAIAEwACAAIAAgATAAIAAgACABMAAgACAAIAEwASAAIAAgATAAIAAgACABMAAgACAAIAEwACAAIAAgASABMAAgACABMAAgACAAIAEgATAAIAAgASABMAAgACABMAAgACAAIAEgATAAIAAgASABMAAgACABMAAgACAAIACwACAAIAAgALAAIAAgACAAwACwACAAIACwACAAIAAgAMAAsAAgACAAsAAgACAAIACwACAAIAAgALAAIAAgACAAwACwACAAIACwACAAIAAgALAAIAAgACAAwACwACAAIACwACAAIAAgALAAIAAgACAAwACwACAAIACwACAAIAAgALAAoAAgACAAsAAgACAAIADAACAAIAAgAMAAsAAgACAAwAAgACAAIADAACAAIAAgAMAAIAAgACAAwAAgACAAIADAALAAIAAgAMAAsAAgACAAwAAgACAAIADAALAAIAAgAMAAsAAgACAAwAAgACAAIADAALAAIAAgAMAAsAAgACAAwAAgACAAIADAALAAIAAgAMAAsAAgACAAwAAgACAAIADAALAAIAAgAMAAsAAgACAAwAAgACAAIAEQASAAIAAgARABAAAgACABEAEgACAAIAEQACAAIAAgARABAAAgACABEAEgACAAIAEQASAAIAAgARABAAAgACABEAEgACAAIAEgARAAIAAgARAAIAAgACABEAEgACAAIAEQASAAIAAgAQABEAAgACABEAAgACAAIAEgATAAIAAgARABIAAgACABMAEgACAAIAEgATAAIAAgASABEAAgACABIAEwACAAIAEwASAAIAAgARABIAAgACABMAEgACAAIAEgATAAIAAgASABEAAgACABIAEwACAAIAEwASAAIAAgARABIAAgACABIAEwACAAIAEwACAAIAAgATABIAAgACABMAEgACAAIAEwACAAIAAgATAAIAAgACABMAAgACAAIAEwASAAIAAgATAAIAAgACABMAAgACAAIAEwACAAIAAgATAAIAAgACABMAAgACAAIAEwACAAIAAgASABMAAgACABMAEgACAAIAEwACAAIAAgASABMAAgACABMAAgACAAIACwACAAIAAgAKAAsAAgACAAsAAgACAAIADAALAAIAAgALAAIAAgACAAwACwACAAIACwACAAIAAgALAAoAAgACAAsAAgACAAIACwACAAIAAgALAAoAAgACAAsAAgACAAIACwACAAIAAgAKAAsAAgACAAsAAgACAAIADAALAAIAAgALAAIAAgACAAwACwACAAIADAALAAIAAgALAAIAAgACAAwACwACAAIADAACAAIAAgAMAAsAAgACAAwAAgACAAIADAALAAIAAgAMAAsAAgACAAwACwACAAIADAACAAIAAgAMAAIAAgACAAwAAgACAAIADAACAAIAAgAMAAIAAgACAAwAAgACAAIADAACAAIAAgAMAAsAAgACAAwAAgACAAIADAACAAIAAgAMAAsAAgACAAwAAgACAAIADAACAAIAAgAMAAsAAgACAAwAAgACAAIAFQAWAAIAAgAWABUAAgACABUAAgACAAIAFQAWAAIAAgAXABYAAgACABUAFgACAAIAFQAWAAIAAgAWABcAAgACABYAFQACAAIAFwAWAAIAAgAXAAIAAgACABYAFwACAAIAFQAWAAIAAgAXABYAAgACABUAFgACAAIAFgAVAAIAAgAWABcAAgACABUAFgACAAIAFQAWAAIAAgAWABcAAgACABUAFgACAAIAFQAWAAIAAgAWABcAAgACABUAFgACAAIAFwACAAIAAgAXAAIAAgACABcAFgACAAIAFwACAAIAAgAXAAIAAgACABcAAgACAAIAFwAWAAIAAgAXAAIAAgACABcAAgACAAIAFwACAAIAAgAXAAIAAgACABYAFwACAAIAFgAXAAIAAgAXAAIAAgACABYAFwACAAIAFgAXAAIAAgAXAAIAAgACABYAFwACAAIACAACAAIAAgAJAAgAAgACAAgAAgACAAIACAACAAIAAgAJAAgAAgACAAgAAgACAAIACAACAAIAAgAJAAgAAgACAAgAAgACAAIACAACAAIAAgAJAAgAAgACAAgAAgACAAIACAACAAIAAgAJAAgAAgACAAgAAgACAAIACAACAAIAAgAJAAgAAgACAAgAAgACAAIACQACAAIAAgAJAAIAAgACAAkACAACAAIACQACAAIAAgAJAAIAAgACAAkAAgACAAIACQAIAAIAAgAJAAIAAgACAAkACAACAAIACQAIAAIAAgAJAAIAAgACAAkACAACAAIACQAIAAIAAgAJAAIAAgACAAkACAACAAIACQAIAAIAAgAJAAIAAgACAAkACAACAAIACQAIAAIAAgAJAAIAAgACAAkACAACAAIAFQAWAAIAAgAVABYAAgACABUAFAACAAIAFQACAAIAAgAVABYAAgACABUAFAACAAIAFQAWAAIAAgAVABYAAgACABUAFAACAAIAFgAVAAIAAgAVABYAAgACABUAAgACAAIAFQAWAAIAAgAVAAIAAgACABQAFQACAAIAFgAXAAIAAgAXABYAAgACABUAFgACAAIAFgAXAAIAAgAWABcAAgACABYAFQACAAIAFwAWAAIAAgAXABYAAgACABUAFgACAAIAFgAXAAIAAgAWABcAAgACABYAFQACAAIAFwAWAAIAAgAWABcAAgACABUAFgACAAIAFwACAAIAAgAXABYAAgACABcAFgACAAIAFwACAAIAAgAXAAIAAgACABcAAgACAAIAFwAWAAIAAgAXAAIAAgACABcAAgACAAIAFwACAAIAAgAXAAIAAgACABcAAgACAAIAFwACAAIAAgAXABYAAgACABYAFwACAAIAFwACAAIAAgAXAAIAAgACABYAFwACAAIACAACAAIAAgAIAAIAAgACAAcACAACAAIACQAIAAIAAgAJAAgAAgACAAgAAgACAAIACAACAAIAAgAIAAIAAgACAAgABwACAAIACAACAAIAAgAIAAIAAgACAAgABwACAAIACAACAAIAAgAIAAIAAgACAAcACAACAAIACQAIAAIAAgAJAAgAAgACAAgAAgACAAIACQAIAAIAAgAJAAgAAgACAAgAAgACAAIACQACAAIAAgAJAAIAAgACAAkACAACAAIACQAIAAIAAgAJAAgAAgACAAkACAACAAIACQACAAIAAgAJAAIAAgACAAkAAgACAAIACQACAAIAAgAJAAIAAgACAAkAAgACAAIACQACAAIAAgAJAAIAAgACAAkACAACAAIACQACAAIAAgAJAAIAAgACAAkACAACAAIACQACAAIAAgAJAAIAAgACAAkACAACAAIACwACAAIAAgAMAAsAAgACAAwACwACAAIADAALAAIAAgALAAIAAgACAAsAAgACAAIACAACAAIAAgAIAAIAAgACAAkACAACAAIACQAIAAIAAgAJAAgAAgACAAgAAgACAAIACAACAAIAAgAIAAIAAgACAAkACAACAAIACQAIAAIAAgAJAAgAAgACAAgAAgACAAIACwACAAIAAgALAAIAAgACAAsACgACAAIACwAKAAIAAgAKAAsAAgACAAsAAgACAAIACwACAAIAAgAMAAsAAgACAAwACwACAAIADAALAAIAAgALAAIAAgACAAsAAgACAAIACwACAAIAAgAMAAsAAgACAAwACwACAAIADAALAAIAAgALAAIAAgACAAsAAgACAAIACAACAAIAAgAHAAgAAgACAAcACAACAAIABwAIAAIAAgAIAAIAAgACAAgAAgACAAIACAACAAIAAgAIAAcAAgACAAcACAACAAIABwAIAAIAAgAIAAIAAgACAAgAAgACAAIACwACAAIAAgALAAIAAgACAAoACwACAAIACgALAAIAAgAKAAsAAgACAAsAAgACAAIACwACAAIAAgALAAIAAgACAAoACwACAAIACgALAAIAAgALAAoAAgACAAsAAgACAAIACAACAAIAAgAHAAgAAgACAAgABwACAAIACAAHAAIAAgAIAAIAAgACAAgAAgACAAIACAACAAIAAgAIAAIAAgACAAkACAACAAIACQAIAAIAAgAJAAgAAgACAAgAAgACAAIADAALAAIAAgALAAIAAgACAAsAAgACAAIACwAKAAIAAgALAAIAAgACAAsAAgACAAIACAAHAAIAAgAIAAIAAgACAAgAAgACAAIACAAHAAIAAgAIAAIAAgACAAgAAgACAAIAFQAWAAIAAgAVABYAAgACABUAFgACAAIAFQAWAAIAAgAVABYAAgACABUAFgACAAIAEQASAAIAAgARABIAAgACABEAEgACAAIAEQASAAIAAgARABIAAgACABEAEgACAAIAEQASAAIAAgARABIAAgACABEAEgACAAIAEQASAAIAAgARABIAAgACABEAEgACAAIAFQAWAAIAAgAVABYAAgACABUAFgACAAIAFQAWAAIAAgAVABYAAgACABUAFgACAAIAFQAWAAIAAgAVABYAAgACABUAFAACAAIAFQAWAAIAAgAVABYAAgACABUAFgACAAIAEQASAAIAAgARABAAAgACABEAEgACAAIAEQASAAIAAgARABIAAgACABEAEgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIABgACAAIAAgAGAAIAAgACAAYAAgACAAIAmpkZP83MzD4AAAAAAAAAANo/Mz+amRk+AmcZPgAAAAAK12M/rkfhPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAADNzEw/zcxMPgAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAmpkZP83MzD4AAAAAAAAAAJqZGT/NzMw+AAAAAAAAAAAK12M/rkfhPQAAAAAAAAAAKFwPP7BH4T4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAM3MTD/MzEw+AAAAAAAAAAAoXA8/sEfhPgAAAAAAAAAAmpkZP83MzD4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAANo/Mz+amRk+AmcZPgAAAACamRk/zczMPgAAAAAAAAAAmpkZP83MzD4AAAAAAAAAAJqZGT/NzMw+AAAAAAAAAAAYhWs/QdejPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAKRwPT+4HoU+AAAAAAAAAAAUrkc/sEdhPgAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAGZmZj/NzMw9AAAAAAAAAADNzEw/zcxMPgAAAAAAAAAAmpkZP83MzD4AAAAAAAAAAJqZGT/NzMw+AAAAAAAAAAAK12M/rkfhPQAAAAAAAAAAm8IXP7gehT5SjZc9++KVPWZmZj/NzMw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAMzNzP83MTD0AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAKFwPP7BH4T4AAAAAAAAAAM3MTD/MzEw+AAAAAAAAAACBbhQ/ATSmPva7wz0AAAAAm8IXP7gehT5SjZc9++KVPcrUTD/NzMw93IzMPQAAAABmZmY/zczMPQAAAAAAAAAACtdjP65H4T0AAAAAAAAAAJqZGT/NzMw+AAAAAAAAAAAK12M/rkfhPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAoXA8/sEfhPgAAAAAAAAAAgW4UPwE0pj72u8M9AAAAAM3MTD/MzEw+AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAGZmZj/NzMw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAD8AAAA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAmpkZP83MzD4AAAAAAAAAAArXYz+uR+E9AAAAAAAAAADaPzM/mpkZPgJnGT4AAAAAAACAPwAAAAAAAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAAMzNzP83MTD0AAAAAAAAAAJvCFz+4HoU+Uo2XPfvilT0AAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAM3MTD/NzEw+AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAmpkZP83MzD4AAAAAAAAAAArXYz+uR+E9AAAAAAAAAACamRk/zczMPgAAAAAAAAAAzcxMP8zMTD4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAChcDz+wR+E+AAAAAAAAAADNzEw/zMxMPgAAAAAAAAAA2j8zP5qZGT4CZxk+AAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAJqZGT/NzMw+AAAAAAAAAADaPzM/mpkZPgJnGT4AAAAAmpkZP83MzD4AAAAAAAAAABiFaz9B16M9AAAAAAAAAACamRk/zczMPgAAAAAAAAAAAACAPwAAAAAAAAAAAAAAABSuRz+wR2E+AAAAAAAAAACkcD0/uB6FPgAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAM3MTD/NzEw+AAAAAAAAAABmZmY/zczMPQAAAAAAAAAAmpkZP83MzD4AAAAAAAAAAArXYz+uR+E9AAAAAAAAAACamRk/zczMPgAAAAAAAAAAm8IXP7gehT5SjZc9++KVPQAAgD8AAAAAAAAAAAAAAABmZmY/zczMPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAMzNzP83MTD0AAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAKFwPP7BH4T4AAAAAAAAAAIFuFD8BNKY+9rvDPQAAAADNzEw/zMxMPgAAAAAAAAAAm8IXP7gehT5SjZc9++KVPWZmZj/NzMw9AAAAAAAAAADK1Ew/zczMPdyMzD0AAAAACtdjP65H4T0AAAAAAAAAAArXYz+uR+E9AAAAAAAAAACamRk/zczMPgAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAChcDz+wR+E+AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAgW4UPwE0pj72u8M9AAAAAJqZGT/NzMw+AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAABmZmY/zczMPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAJvCFz+4HoU+Uo2XPfvilT0zM3M/zcxMPQAAAAAAAAAAMzNzP83MTD0AAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAGZmZj/NzMw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAABSuRz+wR2E+AAAAAAAAAADNzEw/zcxMPgAAAAAAAAAAzcxMP83MTD4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAGZmZj/NzMw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAApHA9P7gehT4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAABmZmY/zczMPQAAAAAAAAAAZmZmP83MzD0AAAAAAAAAAMrUTD/NzMw93IzMPQAAAACkcD0/uB6FPgAAAAAAAAAApHA9P7gehT4AAAAAAAAAAMrUTD/NzMw93IzMPQAAAABmZmY/zczMPQAAAAAAAAAAZmZmP83MzD0AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAACkcD0/uB6FPgAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAADNzEw/zcxMPgAAAAAAAAAAzcxMP83MTD4AAAAAAAAAABSuRz+wR2E+AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAZmZmP83MzD0AAAAAAAAAAAAAAD8AAAA/AAAAAAAAAAAoXA8/sEfhPgAAAAAAAAAAKFwPP7BH4T4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAABmZmY/zczMPQAAAAAAAAAAytRMP83MzD3cjMw9AAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAMrUTD/NzMw93IzMPQAAAADNzEw/zcxMPgAAAAAAAAAAzcxMP83MTD4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAZmZmP83MzD0AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAoXA8/sEfhPgAAAAAAAAAAKFwPP7BH4T4AAAAAAAAAAAAAAD8AAAA/AAAAAAAAAABmZmY/zczMPQAAAAAAAAAAytRMP83MzD3cjMw9AAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAADNzEw/zcxMPgAAAAAAAAAAzcxMP83MTD4AAAAAAAAAAMrUTD/NzMw93IzMPQAAAAAAAIA/AAAAAAAAAAAAAAAAzcxMP8zMTD4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAzcxMP8zMTD4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAzcxMP8zMTD4AAAAAAAAAAIFuFD8BNKY+9rvDPQAAAAAAAIA/AAAAAAAAAAAAAAAAmpkZP83MzD4AAAAAAAAAAIFuFD8BNKY+9rvDPQAAAAAAAIA/AAAAAAAAAAAAAAAAMzNzP83MTD0AAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAAMzNzP83MTD0AAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAAMzNzP83MTD0AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAAMzNzP83MTD0AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAACtdjP65H4T0AAAAAAAAAAArXYz+uR+E9AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAArXYz+uR+E9AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAzcxMP83MTD4AAAAAAAAAAGZmZj/NzMw9AAAAAAAAAABmZmY/zczMPQAAAAAAAAAAZmZmP83MzD0AAAAAAAAAAM3MTD/NzEw+AAAAAAAAAADNzEw/zcxMPgAAAAAAAAAAexQuPwrXoz4AAAAAAAAAAFyPQj+PwnU+AAAAAAAAAABcj0I/j8J1PgAAAAAAAAAAXI9CP4/CdT4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAB7FC4/CtejPgAAAAAAAAAACtdjP65H4T0AAAAAAAAAAAAAQD8AAIA+AAAAAAAAAAAK12M/rkfhPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAQD8AAIA+AAAAAAAAAAAAAEA/AACAPgAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAJqZGT/NzMw+AAAAAAAAAAAK12M/rkfhPQAAAAAAAAAAzcxMP83MTD4AAAAAAAAAAM3MTD/NzEw+AAAAAAAAAACkcD0/uB6FPgAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAMzNzP83MTD0AAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAMzNzP83MTD0AAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAACtdjP65H4T0AAAAAAAAAAArXYz+uR+E9AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAAAAAD8AAAA/AAAAAAAAAACZmRk/zszMPgAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAAAAAD8AAAA/AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAA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AAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAGZmZj/NzMw9AAAAAAAAAABmZmY/zczMPQAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAwvUoP3sUrj4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAFyPQj+PwnU+AAAAAAAAAADC9Sg/exSuPgAAAAAAAAAAwvUoP3sUrj4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAEA/AACAPgAAAAAAAAAAAABAPwAAgD4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAADC9Sg/exSuPgAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAGZmZj/NzMw9AAAAAAAAAABmZmY/zczMPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAQD8AAIA+AAAAAAAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAPwAAAD8AAAAAAAAAAGZmZj/NzMw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAAMzNzP83MTD0AAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAMzNzP83MTD0AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAAMzNzP83MTD0AAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAAMzNzP83MTD0AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAzM3M/zcxMPQAAAAAAAAAAMzNzP83MTD0AAAAAAAAAADMzcz/NzEw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAA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AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAABmZmY/zczMPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAABmZmY/zczMPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAABmZmY/zczMPQAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAGZmZj/NzMw9AAAAAAAAAABmZmY/zczMPQAAAAAAAAAAZmZmP83MzD0AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAABmZmY/zczMPQAAAAAAAAAAZmZmP83MzD0AAAAAAAAAAGZmZj/NzMw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAABmZmY/zczMPQAAAAAAAAAAZmZmP83MzD0AAAAAAAAAAGZmZj/NzMw9AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAUrkc/sEdhPgAAAAAAAAAAFK5HP7BHYT4AAAAAAAAAAM3MTD/NzEw+AAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAGZmZj/NzMw9AAAAAAAAAABmZmY/zczMPQAAAAAAAAAAZmZmP83MzD0AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAGZmZj/NzMw9AAAAAAAAAABmZmY/zczMPQAAAAAAAAAAZmZmP83MzD0AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAM3MTD/NzEw+AAAAAAAAAADNzEw/zcxMPgAAAAAAAAAAzcxMP83MTD4AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAA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HAsyvz3BAr4ZCzI/Z8ECPhwLMr87wQK+GQsyP2nBAj4cCzK/PcECvhkLMj9pwQI+HAsyvz3BAr4ZCzI/acECPhwLMr89wQK+GQsyP2nBAj4cCzK/PcECvhkLMj9mwQI+HAsyvzrBAr4ZCzI/ZsECPhwLMr86wQK+GQsyP2bBAj4cCzK/OsECvhkLMj9pwQI+HAsyvz3BAr4ZCzI/Z8ECPhwLMr87wQK+GQsyP2bBAj4cCzK/OsECvhkLMj9pwQI+HAsyvz3BAr4ZCzI/aMECPhwLMr88wQK+GQsyP2bBAj4cCzK/OsECvhkLMj9owQI+HAsyvzzBAr4ZCzI/acECPhwLMr89wQK+GQsyP2nBAj4cCzK/PcECvhkLMj9pwQI+HAsyvz3BAr4ZCzI/acECPhwLMr89wQK+GQsyP2nBAj4cCzK/PcECvhkLMj9pwQI+HAsyvz3BAr4ZCzI/Z8ECPhwLMr87wQK+GQsyP2nBAj4cCzK/PcECvhkLMj9pwQI+HAsyvz3BAr4ZCzI/acECPhwLMr89wQK+GQsyP2nBAj4cCzK/PcECvhkLMj9pwQI+HAsyvz3BAr4ZCzI/acECPhwLMr89wQK+GQsyP2nBAj4cCzK/PcECvhkLMj9pwQI+HAsyvz3BAr4ZCzI/acECPhwLMr89wQK+GQsyP2bBAj4cCzK/OsECvhkLMj9pwQI+HAsyvz3BAr4ZCzI/acECPhwLMr89wQK+GQsyP2nBAj4cCzK/PcECvhkLMj9mwQI+HAsyvzrBAr4ZCzI/acECPhwLMr89wQK+GQsyP2bBAj4cCzK/OsECvhkLMj9nwQI+HAsyvzvBAr4ZCzI/ZsECPhwLMr86wQK+GQsyP2bBAj4cCzK/OsECvhkLMj9pwQI+HAsyvz3BAr4ZCzI/acECPhwLMr89wQK+GQsyP2bBAj4cCzK/OsECvhkLMj9owQI+HAsyvzzBAr4ZCzI/ZsECPhwLMr86wQK+GQsyP2bBAj4cCzK/OsECvhkLMj9mwQI+HAsyvzrBAr4ZCzI/ZsECPhwLMr86wQK+GQsyP2nBAj4cCzK/PcECvhkLMj9pwQI+HAsyvz3BAr4ZCzI/acECPhwLMr89wQK+GQsyP2nBAj4cCzK/PcECvhkLMj9nwQI+HAsyvzvBAr4ZCzI/Z8ECPhwLMr87wQK+GQsyP2nBAj4cCzK/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", + "byteLength": 119904 + } + ], + "images": [ + { + "uri": "data:image/png;base64,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", + "mimeType": "image/png" + } + ], + "materials": [ + { + "name": "fox_material", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0 + }, + "metallicFactor": 0, + "roughnessFactor": 0.58 + } + } + ], + "meshes": [ + { + "name": "fox1", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TEXCOORD_0": 1, + "JOINTS_0": 2, + "WEIGHTS_0": 3 + }, + "material": 0 + } + ] + } + ], + "nodes": [ + { + "children": [ + 1, + 2 + ], + "name": "root" + }, + { + "name": "fox", + "mesh": 0, + "skin": 0 + }, + { + "children": [ + 3 + ], + "name": "_rootJoint" + }, + { + "children": [ + 4 + ], + "name": "b_Root_00", + "rotation": [ + -0.7071080924875391, + 0.0, + 0.0, + 0.7071054698831242 + ] + }, + { + "children": [ + 5, + 15, + 18, + 22 + ], + "name": "b_Hip_01", + "rotation": [ + 0.12769094176175547, + -0.6954820192393762, + -0.12769022650601444, + 0.695481840425441 + ], + "translation": [ + 0, + 26.748403549194336, + 42.93817138671875 + ] + }, + { + "children": [ + 6 + ], + "name": "b_Spine01_02", + "rotation": [ + 0.0, + 0.0, + -0.5904157638238317, + 0.8070992664030376 + ], + "translation": [ + 12.850601196289062, + 0, + 0 + ] + }, + { + "children": [ + 7, + 9, + 12 + ], + "name": "b_Spine02_03", + "rotation": [ + 0.0, + 0.0, + 0.017411952404281082, + 0.9998484004655261 + ], + "translation": [ + 21.65575408935547, + -0.000118255615234375, + 0 + ] + }, + { + "children": [ + 8 + ], + "name": "b_Neck_04", + "rotation": [ + 0.0, + 0.0, + 0.30337914028264346, + 0.9528699267168443 + ], + "translation": [ + 25.64914321899414, + 0, + 0 + ] + }, + { + "name": "b_Head_05", + "rotation": [ + 0.0, + 0.0, + -0.4002854151487349, + 0.9163905206947555 + ], + "translation": [ + 13.376960754394531, + 0, + 0 + ] + }, + { + "children": [ + 10 + ], + "name": "b_RightUpperArm_06", + "rotation": [ + 0.0004673273262011562, + -0.0004461484692255928, + -0.7121792881110691, + 0.7019973248825985 + ], + "translation": [ + 18.677913665771484, + -4.297340393066406, + 6.9675750732421875 + ] + }, + { + "children": [ + 11 + ], + "name": "b_RightForeArm_07", + "rotation": [ + 0.0, + 0.0, + 0.03712589977348744, + 0.9993105961441663 + ], + "translation": [ + 23.04512596130371, + 0, + 0 + ] + }, + { + "name": "b_RightHand_08", + "rotation": [ + -0.012037406914797018, + -0.00782221012465276, + 0.4605623277185148, + 0.8875112709988741 + ], + "translation": [ + 19.350055694580078, + -0.14598655700683594, + 0 + ] + }, + { + "children": [ + 13 + ], + "name": "b_LeftUpperArm_09", + "rotation": [ + 0.0004972619220940174, + -0.0008821923166442875, + -0.7120874929914663, + 0.7020900061903927 + ], + "translation": [ + 18.67791748046875, + -4.297344207763672, + -6.967987060546875 + ] + }, + { + "children": [ + 14 + ], + "name": "b_LeftForeArm_010", + "rotation": [ + 0.0, + 0.0, + 0.03712589977348744, + 0.9993105961441663 + ], + "translation": [ + 23.045124053955078, + 0, + 0 + ] + }, + { + "name": "b_LeftHand_011", + "rotation": [ + 0.01651791440721507, + 0.014013739873997781, + 0.46007557521271, + 0.8876154790736099 + ], + "translation": [ + 19.350051879882812, + -0.14599037170410156, + 0 + ] + }, + { + "children": [ + 16 + ], + "name": "b_Tail01_012", + "rotation": [ + 0.0, + 0.0, + 0.9818928940656295, + 0.1894369145214904 + ], + "translation": [ + 4.2603759765625, + 15.958770751953125, + 0 + ] + }, + { + "children": [ + 17 + ], + "name": "b_Tail02_013", + "rotation": [ + 0.0, + 0.0, + -0.0696171663387466, + 0.9975737818081244 + ], + "translation": [ + 12.411918640136719, + 0, + 0 + ] + }, + { + "name": "b_Tail03_014", + "rotation": [ + 0.0, + 0.0, + -0.05383274484207684, + 0.9985499664927979 + ], + "translation": [ + 24.24032211303711, + 0, + 0 + ] + }, + { + "children": [ + 19 + ], + "name": "b_LeftLeg01_015", + "rotation": [ + 0.0, + -0.0001717522536559936, + 0.9700158834020681, + -0.2430414706359161 + ], + "translation": [ + 4.813770294189453, + 5.154018402099609, + -6.968006134033203 + ] + }, + { + "children": [ + 20 + ], + "name": "b_LeftLeg02_016", + "rotation": [ + 0.0, + 0.0, + -0.36804378855511655, + 0.9298084586117706 + ], + "translation": [ + 18.944175720214844, + 0, + 0 + ] + }, + { + "children": [ + 21 + ], + "name": "b_LeftFoot01_017", + "rotation": [ + 0.0002484105929664666, + 0.0, + 0.4584841122585099, + 0.888702569535333 + ], + "translation": [ + 17.942811965942383, + 0, + 0 + ] + }, + { + "name": "b_LeftFoot02_018", + "rotation": [ + 0.0, + 0.0, + 0.5472882949090243, + 0.8369441571906533 + ], + "translation": [ + 15.779938697814941, + 0, + 0 + ] + }, + { + "children": [ + 23 + ], + "name": "b_RightLeg01_019", + "rotation": [ + 0.0, + 0.0, + 0.9699585942054535, + -0.24327006705918533 + ], + "translation": [ + 4.813777923583984, + 5.154026031494141, + 6.967563629150391 + ] + }, + { + "children": [ + 24 + ], + "name": "b_RightLeg02_020", + "rotation": [ + 0.0, + 0.0, + -0.36804381432052885, + 0.9298084484131106 + ], + "translation": [ + 18.944183349609375, + 0, + 0 + ] + }, + { + "children": [ + 25 + ], + "name": "b_RightFoot01_021", + "rotation": [ + -0.00015345455876803163, + 0.0, + 0.4579093746168346, + 0.888998864504178 + ], + "translation": [ + 17.94281005859375, + 0, + 0 + ] + }, + { + "name": "b_RightFoot02_022", + "rotation": [ + 0.0, + 0.0, + 0.5472882949090243, + 0.8369441571906533 + ], + "translation": [ + 15.779935836791992, + 0, + 0 + ] + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9987 + } + ], + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "skins": [ + { + "inverseBindMatrices": 4, + "joints": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ], + "skeleton": 2 + } + ], + "textures": [ + { + "sampler": 0, + "source": 0 + } + ] +} diff --git a/gradio/media_assets/models3d/face.obj b/gradio/media_assets/models3d/face.obj new file mode 100644 index 0000000..a2e82ee --- /dev/null +++ b/gradio/media_assets/models3d/face.obj @@ -0,0 +1,2471 @@ +#### +# +# OBJ File Generated by Meshlab +# +#### +# Object face.obj +# +# Vertices: 845 +# Faces: 1610 +# +#### +v 54.126293 -49.502399 71.230705 0.384314 0.254902 0.200000 +v 51.424591 -54.578999 75.431709 0.337255 0.227451 0.172549 +v 44.556496 -61.237099 65.307907 0.384314 0.254902 0.192157 +v 18.416197 -75.205498 35.343601 0.529412 0.376471 0.298039 +v 13.523697 -77.731102 34.633598 0.513725 0.364706 0.290196 +v 11.454597 -74.201599 29.775103 0.639216 0.454902 0.372549 +v 5.855427 -77.386299 30.315100 0.603922 0.435294 0.356863 +v -0.378855 -77.287903 29.743700 0.611765 0.443137 0.360784 +v 6.812867 -79.041397 33.545300 0.505882 0.368627 0.298039 +v 21.881296 -70.810898 34.982002 0.588235 0.415686 0.329412 +v 17.653797 -69.034798 30.263802 0.674510 0.478431 0.388235 +v 39.436996 -66.435501 64.968597 0.164706 0.109804 0.082353 +v 33.808598 -70.338699 57.717205 0.364706 0.247059 0.184314 +v 31.909595 -68.409302 47.974705 0.439216 0.301961 0.227451 +v 38.451797 -64.744904 55.703503 0.427451 0.294118 0.219608 +v 28.802397 -72.939499 50.408905 0.372549 0.254902 0.192157 +v 22.955397 -75.486603 43.228905 0.400000 0.274510 0.215686 +v 26.511995 -70.622101 41.049904 0.494118 0.341176 0.262745 +v 24.465696 -76.217796 49.972805 0.345098 0.239216 0.184314 +v 15.571197 -78.986099 40.602001 0.396078 0.278431 0.219608 +v 17.437696 -79.466103 47.124001 0.352941 0.247059 0.196078 +v 28.846695 -74.018898 56.477203 0.164706 0.113725 0.086275 +v 9.321696 -81.938202 48.001999 0.172549 0.129412 0.105882 +v -0.898453 -82.647102 47.089699 0.172549 0.129412 0.105882 +v -0.725759 -82.110100 43.572498 0.400000 0.301961 0.254902 +v 18.763496 -79.316498 51.133400 0.172549 0.117647 0.094118 +v 8.731546 -81.557503 44.441002 0.392157 0.286275 0.235294 +v 27.614496 -65.079002 38.432205 0.596078 0.419608 0.321569 +v 33.274998 -61.490501 43.398605 0.568627 0.400000 0.301961 +v 38.784996 -60.440899 50.047802 0.513725 0.356863 0.266667 +v 42.862595 -58.651798 55.019802 0.490196 0.337255 0.250980 +v 7.726847 -80.575996 38.638500 0.415686 0.301961 0.247059 +v -0.490965 -80.930901 37.912300 0.427451 0.317647 0.262745 +v -0.419731 -79.354401 33.152100 0.513725 0.376471 0.305882 +v -0.332592 -73.782600 27.050400 0.705882 0.505882 0.415686 +v -0.212143 -66.515099 24.546000 0.815686 0.584314 0.501961 +v 11.557498 -67.907898 26.621702 0.749020 0.529412 0.443137 +v 23.830296 -66.563599 34.771904 0.631373 0.447059 0.352941 +v 35.915897 105.133003 38.176804 0.219608 0.152941 0.117647 +v 36.548496 101.879997 36.786003 0.454902 0.309804 0.235294 +v 27.391598 102.010002 31.503502 0.278431 0.188235 0.141176 +v 59.787598 -0.163902 47.832703 0.611765 0.403922 0.321569 +v 50.721996 -9.559180 38.382904 0.721569 0.490196 0.403922 +v 52.439697 -0.358719 38.911705 0.741176 0.501961 0.419608 +v 29.889196 -56.737301 37.395603 0.662745 0.470588 0.360784 +v 26.097795 -54.826500 33.666504 0.694118 0.486275 0.372549 +v 26.293499 -49.727501 32.217205 0.717647 0.501961 0.388235 +v 25.434498 -44.035900 30.291403 0.729412 0.509804 0.400000 +v 31.436096 -44.876701 34.064003 0.737255 0.533333 0.411765 +v 32.500095 -51.078300 37.202503 0.690196 0.490196 0.376471 +v 21.003098 -62.278099 31.444601 0.705882 0.501961 0.400000 +v 26.550097 -59.714100 35.467205 0.666667 0.474510 0.364706 +v 44.267796 -52.222500 50.556103 0.552941 0.384314 0.290196 +v 44.155598 -45.754700 45.234005 0.607843 0.423529 0.325490 +v 39.424698 -54.441200 45.685303 0.588235 0.411765 0.309804 +v 42.849094 -40.630600 40.559803 0.662745 0.462745 0.364706 +v 38.941998 -42.900600 37.800304 0.713725 0.505882 0.400000 +v 52.563396 -19.698900 43.829704 0.650980 0.443137 0.356863 +v 46.654697 -16.837099 36.168205 0.749020 0.517647 0.427451 +v 33.321995 -35.720200 31.409004 0.745098 0.541176 0.427451 +v 32.657997 -40.731899 33.174603 0.752941 0.549020 0.427451 +v 35.472797 -44.317001 36.017105 0.737255 0.533333 0.415686 +v 27.102999 -39.309502 30.560902 0.713725 0.494118 0.388235 +v 21.412498 -21.051500 22.568802 0.733333 0.509804 0.415686 +v 23.988897 -17.175900 24.872402 0.803922 0.549020 0.447059 +v 27.558098 -24.650700 25.424603 0.745098 0.533333 0.443137 +v 27.926598 -35.123001 29.129301 0.701961 0.494118 0.384314 +v 24.306997 -32.355598 25.602802 0.674510 0.478431 0.384314 +v 26.352098 -28.906099 25.178001 0.725490 0.537255 0.439216 +v 22.071098 -41.536701 27.588202 0.670588 0.447059 0.368627 +v 20.085999 -45.811401 26.494501 0.701961 0.486275 0.384314 +v 19.001799 -42.619900 24.806602 0.662745 0.431373 0.364706 +v 28.976398 -21.374100 26.378202 0.788235 0.556863 0.454902 +v 26.341497 -16.211300 25.941402 0.839216 0.584314 0.474510 +v 30.814697 -16.946400 27.290703 0.866667 0.611765 0.501961 +v 15.764998 -20.247000 19.382002 0.698039 0.478431 0.392157 +v 16.598598 -15.898800 21.267502 0.737255 0.486275 0.400000 +v 22.444199 -3.466390 25.169203 0.752941 0.505882 0.415686 +v 21.588697 -7.433040 25.304302 0.760784 0.513725 0.423529 +v 18.939598 -4.099130 23.521101 0.556863 0.321569 0.270588 +v 26.227198 -11.129200 26.175901 0.894118 0.639216 0.533333 +v 23.958097 -13.787900 25.571701 0.843137 0.584314 0.474510 +v 3.233079 -28.114599 12.996900 0.807843 0.517647 0.474510 +v 3.691189 -24.150000 13.560400 0.788235 0.545098 0.450980 +v 7.239779 -23.905701 14.043200 0.760784 0.533333 0.439216 +v 3.876879 -20.045601 14.070800 0.803922 0.580392 0.490196 +v 9.195189 -19.025499 15.793001 0.705882 0.498039 0.411765 +v 3.141149 -17.198000 13.771500 0.807843 0.584314 0.498039 +v 4.931409 -13.967200 13.240000 0.737255 0.509804 0.435294 +v 30.555098 -31.658199 28.330402 0.737255 0.537255 0.439216 +v 36.921795 -39.106701 34.709404 0.749020 0.541176 0.427451 +v 37.097694 -31.720600 32.396603 0.737255 0.521569 0.415686 +v 32.560898 -26.616501 28.413202 0.749020 0.533333 0.435294 +v 10.314199 -9.869830 13.888401 0.564706 0.352941 0.301961 +v 3.716749 -10.896600 9.270600 0.549020 0.333333 0.278431 +v 6.998729 -8.745080 9.093191 0.458824 0.274510 0.247059 +v 11.017399 -7.650950 11.325301 0.407843 0.235294 0.196078 +v 13.094099 -8.179750 13.619401 0.596078 0.372549 0.305882 +v 14.387198 -9.345590 16.544502 0.682353 0.423529 0.349020 +v 14.674398 -10.957500 19.197702 0.619608 0.352941 0.286275 +v 12.273198 -13.638600 18.082401 0.670588 0.415686 0.349020 +v 15.706099 -12.324600 21.310001 0.654902 0.400000 0.341176 +v 0.160743 -11.927500 9.164860 0.631373 0.415686 0.345098 +v 16.748499 -0.088195 17.466702 0.486275 0.262745 0.211765 +v 14.135499 2.999720 13.703901 0.498039 0.278431 0.223529 +v 15.227698 3.521270 17.552402 0.509804 0.286275 0.227451 +v 11.515499 5.295930 9.697881 0.517647 0.298039 0.235294 +v 12.664099 6.361880 14.138000 0.529412 0.305882 0.243137 +v 17.526098 -1.031370 20.210901 0.478431 0.250980 0.203922 +v 18.114098 -7.909870 23.033703 0.556863 0.313725 0.266667 +v 10.384699 8.630760 10.439901 0.541176 0.321569 0.250980 +v 17.828499 0.413114 21.808401 0.541176 0.305882 0.250980 +v 17.929298 -3.542410 20.437902 0.482353 0.247059 0.203922 +v 17.031097 -10.684200 22.274002 0.619608 0.368627 0.313725 +v 0.287526 -2.020300 -3.337250 0.925490 0.694118 0.627451 +v 3.060050 -2.048090 -2.924980 0.894118 0.650980 0.584314 +v 3.007730 -4.729930 -2.346410 0.870588 0.611765 0.537255 +v 5.864850 -1.934710 -1.552539 0.835294 0.592157 0.525490 +v 5.417400 -4.893230 -0.945915 0.788235 0.533333 0.458824 +v 8.198860 -5.353820 2.653151 0.627451 0.403922 0.329412 +v 5.432270 -7.772490 2.088590 0.639216 0.411765 0.337255 +v 6.036009 -8.309330 5.213161 0.458824 0.270588 0.223529 +v 8.222100 -6.896640 5.293671 0.498039 0.305882 0.247059 +v 5.315060 7.954630 2.505800 0.662745 0.400000 0.321569 +v 5.391600 3.642640 -0.798212 0.729412 0.454902 0.372549 +v 3.235490 4.316670 -1.382350 0.764706 0.482353 0.396078 +v 5.431940 1.019950 -1.779530 0.788235 0.521569 0.443137 +v 3.012540 0.964488 -2.729010 0.847059 0.572549 0.494118 +v 3.567950 -7.050370 -0.455861 0.772549 0.517647 0.439216 +v 0.254061 -9.470920 1.815530 0.694118 0.466667 0.388235 +v 3.185970 -9.021570 1.994320 0.686275 0.458824 0.380392 +v 3.347260 -9.952950 5.307401 0.568627 0.360784 0.294118 +v 11.644599 -4.814710 6.525851 0.666667 0.439216 0.360784 +v 10.245199 -6.916610 8.546041 0.431373 0.254902 0.207843 +v 14.322199 -5.140650 10.096601 0.682353 0.439216 0.360784 +v 13.032799 -7.158690 11.593801 0.564706 0.352941 0.286275 +v 16.428497 -5.656990 13.658102 0.650980 0.396078 0.321569 +v 15.041399 -7.855270 14.466301 0.674510 0.423529 0.345098 +v 17.493698 -6.043210 17.118502 0.572549 0.313725 0.254902 +v 16.415197 -8.234700 17.133102 0.619608 0.356863 0.290196 +v 0.234891 4.747230 -1.584250 0.788235 0.498039 0.407843 +v 7.301090 1.952910 -0.069825 0.709804 0.443137 0.364706 +v 9.047320 -0.970925 1.557191 0.690196 0.443137 0.368627 +v 9.465290 3.177850 3.547511 0.596078 0.352941 0.282353 +v 17.660997 -6.569220 20.282202 0.505882 0.258824 0.215686 +v 16.557198 -9.033780 19.871101 0.552941 0.290196 0.235294 +v 20.394499 -10.617700 24.778902 0.776471 0.517647 0.427451 +v 19.250498 -13.611500 23.636002 0.780392 0.513725 0.423529 +v 11.990899 1.078080 6.089581 0.576471 0.337255 0.266667 +v 12.157999 -1.685630 5.705281 0.647059 0.403922 0.329412 +v 8.839929 9.251840 7.807421 0.568627 0.341176 0.262745 +v 8.096309 14.803000 11.047301 0.600000 0.368627 0.290196 +v 11.552299 11.023700 14.935801 0.584314 0.360784 0.278431 +v 8.089608 23.007601 17.162300 0.607843 0.376471 0.290196 +v 12.042598 18.267500 20.407701 0.627451 0.396078 0.305882 +v 21.344799 3.444300 24.364801 0.760784 0.525490 0.435294 +v 10.378798 43.596600 19.092800 0.760784 0.545098 0.439216 +v 11.674199 50.650799 15.859901 0.796078 0.596078 0.509804 +v 54.439495 58.174599 40.267605 0.525490 0.352941 0.262745 +v 56.231796 64.326599 44.901005 0.525490 0.349020 0.258824 +v 20.091297 46.555901 20.626602 0.615686 0.435294 0.352941 +v 12.458998 40.488800 23.149502 0.682353 0.470588 0.364706 +v 11.517699 58.712601 14.732701 0.894118 0.666667 0.576471 +v 20.941198 69.476898 17.756903 0.811765 0.568627 0.458824 +v 28.594099 70.756203 20.988201 0.768627 0.537255 0.427451 +v 36.831398 72.183800 26.344303 0.705882 0.490196 0.380392 +v 44.649097 71.571800 32.518906 0.650980 0.447059 0.337255 +v 44.312595 65.052498 30.244703 0.650980 0.450980 0.349020 +v 51.160194 68.888397 38.786102 0.580392 0.392157 0.290196 +v 50.121696 63.216999 35.800903 0.592157 0.400000 0.301961 +v 29.676498 38.193501 27.577202 0.278431 0.203922 0.172549 +v 32.242195 35.520000 28.930403 0.176471 0.149020 0.133333 +v 36.059795 38.007198 28.101904 0.337255 0.254902 0.223529 +v 29.195498 28.762300 30.062502 0.537255 0.407843 0.376471 +v 35.387295 30.454100 30.872303 0.494118 0.427451 0.419608 +v 40.619995 36.623299 30.930103 0.376471 0.290196 0.262745 +v 40.689995 30.856199 33.447403 0.560784 0.435294 0.415686 +v 46.166798 34.721901 35.938004 0.427451 0.274510 0.219608 +v 24.791199 28.750900 30.347101 0.631373 0.439216 0.380392 +v 23.558298 30.911301 30.865202 0.654902 0.525490 0.490196 +v 23.990099 35.138500 30.007702 0.588235 0.525490 0.505882 +v 18.925999 31.543200 29.964102 0.607843 0.411765 0.345098 +v 22.978798 22.609301 29.898302 0.698039 0.478431 0.415686 +v 44.176495 24.024099 36.367702 0.666667 0.462745 0.368627 +v 48.844097 28.759600 40.696102 0.568627 0.372549 0.290196 +v 29.935898 20.776899 30.510502 0.764706 0.541176 0.462745 +v 37.725395 21.069901 32.600403 0.768627 0.552941 0.450980 +v 43.673397 41.639000 29.921003 0.639216 0.447059 0.364706 +v 30.249598 43.423698 24.863802 0.698039 0.498039 0.396078 +v 21.971298 38.089298 28.511002 0.552941 0.376471 0.305882 +v 18.851599 37.250599 29.058102 0.600000 0.403922 0.317647 +v 26.141699 38.694302 27.701101 0.415686 0.305882 0.258824 +v 24.366098 42.716400 25.250301 0.670588 0.470588 0.364706 +v 33.047497 40.919201 25.876904 0.588235 0.415686 0.341176 +v 38.202995 45.220299 25.513803 0.741176 0.541176 0.447059 +v 37.181396 41.326099 26.587204 0.592157 0.415686 0.341176 +v 40.767696 39.773300 28.946104 0.533333 0.368627 0.305882 +v 17.566698 40.536701 25.680403 0.682353 0.478431 0.372549 +v 12.599898 34.387402 26.919203 0.635294 0.439216 0.356863 +v 9.244298 36.241699 22.858601 0.670588 0.454902 0.349020 +v 42.106094 27.525101 34.601604 0.588235 0.396078 0.337255 +v 9.833448 28.316700 23.639999 0.592157 0.392157 0.298039 +v 18.639698 24.671700 29.695803 0.650980 0.447059 0.376471 +v 17.292599 20.688499 27.482101 0.654902 0.439216 0.341176 +v 13.333098 24.688499 26.033302 0.607843 0.407843 0.305882 +v 22.784698 17.533800 28.735502 0.721569 0.498039 0.407843 +v 30.601099 15.688600 30.290201 0.780392 0.560784 0.466667 +v 41.169495 16.181299 33.723103 0.792157 0.564706 0.458824 +v 47.120296 20.681999 37.970406 0.709804 0.501961 0.400000 +v 51.931698 25.841200 43.142902 0.639216 0.443137 0.341176 +v 52.340096 32.697899 43.622303 0.576471 0.384314 0.298039 +v 55.356895 32.597599 46.411205 0.619608 0.439216 0.341176 +v 53.222797 37.287899 42.089905 0.568627 0.392157 0.301961 +v 29.678698 24.733801 30.073902 0.717647 0.501961 0.431373 +v 36.489197 25.085699 31.599504 0.698039 0.498039 0.427451 +v 7.209608 -47.131500 19.961300 0.694118 0.474510 0.392157 +v 14.446698 -46.892300 22.922901 0.701961 0.486275 0.392157 +v 14.611198 -50.406300 25.080801 0.741176 0.521569 0.407843 +v 59.776695 32.152901 51.172405 0.615686 0.435294 0.333333 +v 55.757698 22.840000 45.449802 0.650980 0.447059 0.341176 +v 50.009796 17.205099 38.838005 0.725490 0.501961 0.396078 +v 41.713795 8.508140 32.452805 0.839216 0.580392 0.486275 +v 57.126797 40.041698 45.909004 0.552941 0.380392 0.294118 +v 60.104397 43.092098 50.342705 0.509804 0.345098 0.262745 +v 54.672398 42.407398 40.515404 0.556863 0.380392 0.305882 +v 56.906197 46.631001 42.925304 0.462745 0.305882 0.239216 +v 51.822495 43.786900 36.258904 0.596078 0.415686 0.337255 +v 54.271397 50.864399 37.835804 0.419608 0.282353 0.223529 +v 47.592297 46.765598 30.486204 0.627451 0.454902 0.384314 +v 49.821495 53.293598 31.830803 0.427451 0.298039 0.243137 +v 42.391796 49.445400 25.742804 0.607843 0.454902 0.388235 +v 44.034695 54.943100 26.506004 0.443137 0.317647 0.266667 +v 36.751995 50.112900 22.968603 0.545098 0.407843 0.349020 +v 37.753296 55.541100 22.525805 0.490196 0.364706 0.313725 +v 28.692497 48.042801 21.970001 0.600000 0.435294 0.356863 +v 31.105598 56.180599 19.828001 0.596078 0.450980 0.392157 +v 36.435097 81.925400 28.781404 0.682353 0.470588 0.364706 +v 45.338295 80.322098 35.563904 0.603922 0.411765 0.305882 +v 24.592999 80.095299 21.969002 0.772549 0.541176 0.427451 +v 11.213599 69.043297 15.977401 0.835294 0.588235 0.474510 +v 11.825998 81.413200 18.962801 0.803922 0.564706 0.447059 +v 21.538898 53.003700 17.177603 0.588235 0.439216 0.380392 +v -0.179365 43.542702 17.002100 0.898039 0.666667 0.564706 +v 5.625159 37.419102 18.996799 0.803922 0.576471 0.466667 +v 4.198629 23.955999 12.407200 0.686275 0.435294 0.345098 +v 14.535299 -2.114900 9.443011 0.631373 0.380392 0.309804 +v 15.685598 8.142320 20.454601 0.658824 0.427451 0.337255 +v 30.598497 1.488610 27.147802 0.839216 0.576471 0.490196 +v 29.978397 -6.397050 26.571701 0.894118 0.635294 0.545098 +v 41.297398 -1.405200 31.022003 0.854902 0.596078 0.509804 +v 36.111397 -6.540340 28.005903 0.882353 0.623529 0.537255 +v 44.276794 -9.991890 32.788902 0.811765 0.568627 0.478431 +v 36.580196 -12.147500 28.853804 0.874510 0.623529 0.525490 +v 23.545498 26.550501 30.193302 0.670588 0.454902 0.392157 +v 7.536978 -50.700600 22.940800 0.737255 0.513725 0.407843 +v 0.049966 -50.745201 22.357901 0.713725 0.490196 0.403922 +v -0.059187 -47.707500 19.745001 0.662745 0.450980 0.376471 +v 13.614799 -61.189201 26.770203 0.772549 0.549020 0.447059 +v 7.402448 -54.763500 24.320801 0.792157 0.549020 0.443137 +v 5.676798 -60.044102 24.120001 0.815686 0.568627 0.478431 +v 0.007680 -58.305000 23.528999 0.823529 0.568627 0.478431 +v 14.550098 -55.003899 26.621601 0.780392 0.549020 0.431373 +v 20.750698 -49.841400 28.390602 0.729412 0.505882 0.396078 +v 14.341599 -43.669201 21.142101 0.690196 0.443137 0.392157 +v 9.106318 -44.220402 18.280300 0.749020 0.454902 0.427451 +v 4.624769 -44.514198 17.213200 0.768627 0.458824 0.439216 +v 13.494699 1.203290 9.375581 0.541176 0.305882 0.247059 +v 17.587399 -3.090150 17.170603 0.521569 0.282353 0.231373 +v 15.390899 0.616895 13.529201 0.525490 0.290196 0.239216 +v 16.498499 -2.610490 13.351402 0.580392 0.333333 0.270588 +v 0.114789 -17.205601 13.556900 0.803922 0.576471 0.490196 +v 0.213039 -10.739900 5.333460 0.631373 0.419608 0.345098 +v 0.155695 9.698840 1.605760 0.737255 0.458824 0.372549 +v 63.699497 35.875702 57.276005 0.541176 0.364706 0.274510 +v 62.952995 26.737499 54.915302 0.588235 0.400000 0.301961 +v 62.913197 46.593300 56.027405 0.486275 0.321569 0.239216 +v 66.067696 49.149700 63.298607 0.435294 0.282353 0.207843 +v 67.025894 36.883900 64.329010 0.478431 0.313725 0.235294 +v 66.947090 22.464199 62.047707 0.505882 0.325490 0.243137 +v 62.186794 10.766600 50.973103 0.576471 0.376471 0.290196 +v 68.964989 11.545400 68.556404 0.415686 0.266667 0.200000 +v 64.644791 3.438430 56.958504 0.498039 0.317647 0.243137 +v 7.315678 30.263700 20.278500 0.635294 0.407843 0.309804 +v 16.478199 14.480500 23.730602 0.674510 0.435294 0.341176 +v 22.498598 11.176800 26.488401 0.752941 0.505882 0.411765 +v 30.975899 9.046680 28.818901 0.800000 0.545098 0.447059 +v 59.705097 18.493999 48.250504 0.619608 0.415686 0.317647 +v 53.774796 10.607700 40.580803 0.741176 0.505882 0.411765 +v 4.194539 31.168200 16.627800 0.733333 0.498039 0.388235 +v -0.049233 31.606899 15.159500 0.800000 0.552941 0.435294 +v 60.551994 58.758701 51.876305 0.482353 0.313725 0.231373 +v 64.570694 60.979000 60.018402 0.403922 0.266667 0.196078 +v 58.582897 51.884102 46.769802 0.482353 0.317647 0.239216 +v 58.461395 70.455803 49.594505 0.466667 0.313725 0.231373 +v 52.436497 76.538399 42.343403 0.533333 0.356863 0.262745 +v -0.208444 49.913601 15.256100 0.917647 0.698039 0.611765 +v -0.124523 58.086498 14.326200 0.905882 0.674510 0.584314 +v 0.274219 81.382401 18.191799 0.819608 0.576471 0.458824 +v 0.481933 93.290398 21.740499 0.725490 0.498039 0.384314 +v 12.405998 93.661400 22.874903 0.713725 0.486275 0.376471 +v 24.880598 93.906502 26.578302 0.658824 0.447059 0.341176 +v 36.480595 93.078102 32.634003 0.576471 0.388235 0.294118 +v 46.004696 90.203003 39.649605 0.505882 0.345098 0.258824 +v 53.584496 85.144096 46.494904 0.450980 0.305882 0.223529 +v 62.545998 71.367798 56.403603 0.364706 0.250980 0.188235 +v 59.255596 78.845100 52.443905 0.400000 0.274510 0.203922 +v 61.191795 85.050301 57.241005 0.333333 0.235294 0.180392 +v 65.452393 75.230003 61.807304 0.286275 0.207843 0.160784 +v 68.381790 63.713902 67.643311 0.266667 0.188235 0.149020 +v 69.324196 50.443100 71.447411 0.341176 0.227451 0.172549 +v 70.128990 37.903400 72.923409 0.360784 0.239216 0.180392 +v 70.487595 24.768299 72.393105 0.396078 0.250980 0.192157 +v 10.803699 -24.072800 15.435301 0.713725 0.501961 0.407843 +v 15.684198 -24.840200 18.155502 0.698039 0.494118 0.403922 +v 14.923498 -28.631399 17.459002 0.729412 0.498039 0.431373 +v 19.121199 -30.573601 20.995502 0.694118 0.478431 0.403922 +v 21.119598 -26.506599 21.583101 0.701961 0.509804 0.423529 +v 36.499996 -22.712601 30.213703 0.811765 0.572549 0.466667 +v 40.448395 -36.176701 36.400604 0.725490 0.509804 0.407843 +v 44.144596 -32.603699 38.731205 0.705882 0.490196 0.392157 +v 38.536495 -47.817600 40.317505 0.666667 0.474510 0.364706 +v 46.546295 -37.311100 43.387104 0.635294 0.439216 0.349020 +v 50.373398 -34.235699 47.127705 0.611765 0.423529 0.333333 +v 49.724796 -40.818298 50.185303 0.588235 0.403922 0.317647 +v 49.255295 -27.889099 42.907104 0.658824 0.454902 0.360784 +v 44.257797 -23.479799 35.807602 0.752941 0.525490 0.427451 +v 40.645798 -27.765900 34.063705 0.764706 0.537255 0.431373 +v 40.754898 -19.093399 32.012905 0.815686 0.576471 0.478431 +v 10.416199 -27.973200 14.961201 0.756863 0.498039 0.443137 +v 0.075350 -28.306400 12.898400 0.803922 0.521569 0.466667 +v 0.092755 -24.430799 13.505000 0.772549 0.529412 0.443137 +v 0.110785 -20.513000 13.908800 0.807843 0.588235 0.501961 +v 0.085609 -54.512001 23.610901 0.784314 0.537255 0.443137 +v 20.909399 -54.946800 30.047201 0.729412 0.509804 0.392157 +v 35.645298 -55.715099 42.223003 0.623529 0.439216 0.337255 +v 49.814896 -48.565498 56.933704 0.517647 0.356863 0.274510 +v 47.977795 -55.414200 62.039703 0.443137 0.301961 0.231373 +v 55.688496 -28.994600 53.447903 0.560784 0.380392 0.301961 +v 55.983391 -43.000198 65.692207 0.450980 0.305882 0.239216 +v 56.362797 -36.061699 59.322903 0.517647 0.349020 0.274510 +v 57.892796 -19.573200 52.540302 0.560784 0.376471 0.294118 +v 70.365593 -2.993800 89.379608 0.321569 0.211765 0.164706 +v 71.177193 8.937180 81.271606 0.321569 0.211765 0.164706 +v 72.558990 10.622100 91.867805 0.298039 0.200000 0.156863 +v 66.125992 -21.471800 81.920410 0.341176 0.223529 0.168627 +v 67.528091 -10.306600 77.117210 0.364706 0.239216 0.180392 +v 61.701691 -27.660801 67.740509 0.443137 0.294118 0.227451 +v 68.337189 -0.022244 71.967606 0.380392 0.243137 0.188235 +v 63.424397 -15.896200 64.452606 0.443137 0.290196 0.219608 +v 68.125694 -16.648800 92.619408 0.301961 0.200000 0.156863 +v 63.768192 -32.212200 86.796310 0.305882 0.203922 0.156863 +v 61.167294 -36.372501 75.628105 0.376471 0.250980 0.192157 +v 59.626293 -41.868698 80.668411 0.333333 0.223529 0.172549 +v 64.162796 -6.066620 60.428703 0.454902 0.294118 0.223529 +v 57.393696 -11.239100 47.991703 0.611765 0.411765 0.329412 +v 72.524994 24.387699 82.025009 0.282353 0.188235 0.145098 +v 73.856789 35.723499 88.540108 0.105882 0.074510 0.062745 +v 73.683792 23.761801 91.300209 0.243137 0.164706 0.129412 +v 74.501190 30.125500 94.839706 0.094118 0.062745 0.050980 +v 74.034592 22.480499 96.303810 0.141176 0.098039 0.078431 +v 72.831192 7.859240 98.770004 0.149020 0.101961 0.082353 +v 69.296593 -13.425300 99.241905 0.305882 0.200000 0.160784 +v 71.395790 -3.606020 100.302010 0.325490 0.215686 0.172549 +v 71.129791 -7.143250 103.242004 0.188235 0.125490 0.101961 +v 68.107590 -18.211399 101.004005 0.133333 0.086275 0.070588 +v 72.363594 -0.137348 102.838005 0.176471 0.117647 0.094118 +v 46.213596 99.739601 44.501404 0.388235 0.266667 0.200000 +v 54.685898 92.834099 51.120205 0.376471 0.258824 0.192157 +v 55.596695 97.970802 54.777103 0.294118 0.207843 0.160784 +v 59.781796 92.442200 58.018204 0.125490 0.090196 0.066667 +v 50.586895 101.808998 50.572002 0.172549 0.121569 0.094118 +v 43.422997 104.478996 44.205505 0.184314 0.125490 0.098039 +v 72.007690 49.799099 78.898109 0.250980 0.176471 0.137255 +v 73.057594 49.127201 82.269409 0.094118 0.066667 0.054902 +v 49.949097 58.855202 33.782906 0.564706 0.388235 0.301961 +v 44.336697 60.042801 28.311203 0.603922 0.419608 0.337255 +v 37.959198 59.937000 23.450504 0.674510 0.482353 0.400000 +v 4.464459 16.161301 7.486950 0.682353 0.423529 0.341176 +v 0.094371 16.649401 6.087460 0.745098 0.466667 0.376471 +v 0.014745 24.425800 10.934700 0.745098 0.482353 0.384314 +v 71.478493 58.516998 75.568504 0.105882 0.074510 0.058824 +v 68.260490 72.263603 66.620407 0.137255 0.101961 0.082353 +v 70.731094 66.735497 71.686806 0.109804 0.082353 0.066667 +v 72.507889 38.181301 81.341507 0.254902 0.176471 0.137255 +v 64.500496 82.174896 61.573006 0.149020 0.109804 0.082353 +v 55.767296 99.280998 55.781605 0.145098 0.101961 0.078431 +v 12.876298 100.102997 25.744701 0.317647 0.215686 0.164706 +v 0.286601 -4.793040 -2.721520 0.894118 0.643137 0.568627 +v 64.638489 -29.722000 98.167007 0.121569 0.082353 0.062745 +v 58.149193 -45.228298 86.664307 0.290196 0.196078 0.152941 +v 62.356594 -36.605999 93.365707 0.274510 0.184314 0.141176 +v 60.716892 -40.186600 93.595604 0.113725 0.078431 0.058824 +v 56.109493 -48.650299 86.931511 0.141176 0.098039 0.074510 +v 49.385094 -57.278099 77.067108 0.160784 0.109804 0.082353 +v 66.355392 -24.387800 96.374504 0.270588 0.184314 0.141176 +v 37.257298 34.215500 30.609003 0.415686 0.392157 0.388235 +v 39.520298 33.790501 31.976805 0.529412 0.494118 0.490196 +v 27.428999 33.649700 29.901602 0.372549 0.333333 0.309804 +v 23.755198 -35.746101 28.257301 0.482353 0.278431 0.235294 +v 3.793149 -35.116299 16.927900 0.600000 0.278431 0.294118 +v 3.855958 -38.129200 17.080799 0.756863 0.376471 0.407843 +v -0.023884 -38.223301 16.909401 0.749020 0.368627 0.400000 +v 8.071998 -35.035198 18.102501 0.588235 0.266667 0.286275 +v 8.431998 -37.966801 18.149799 0.717647 0.352941 0.384314 +v 13.239598 -35.231899 20.689901 0.545098 0.262745 0.274510 +v 13.372499 -37.816101 20.728703 0.631373 0.317647 0.337255 +v 17.666399 -35.273201 23.658503 0.447059 0.227451 0.227451 +v 17.640099 -37.751598 24.088802 0.556863 0.282353 0.286275 +v 21.365198 -34.941399 26.083202 0.478431 0.266667 0.231373 +v 21.738897 -38.277302 27.387302 0.564706 0.325490 0.298039 +v 21.377197 -34.903000 26.050001 0.490196 0.278431 0.239216 +v -0.125513 -44.742699 17.141100 0.749020 0.454902 0.419608 +v 14.317698 -31.063400 17.972801 0.662745 0.403922 0.364706 +v 6.533319 -27.864201 13.520501 0.788235 0.505882 0.458824 +v 17.674997 -35.227402 23.629501 0.427451 0.219608 0.211765 +v 18.364597 -33.324001 22.496202 0.580392 0.341176 0.301961 +v 13.245798 -35.168499 20.655602 0.537255 0.258824 0.270588 +v 13.862498 -33.057598 19.452902 0.596078 0.333333 0.313725 +v 8.065038 -34.963299 18.048201 0.580392 0.270588 0.282353 +v 9.277578 -32.971199 17.116100 0.631373 0.349020 0.337255 +v 3.799159 -35.048901 16.841700 0.600000 0.278431 0.294118 +v 5.629269 -33.251099 16.003500 0.643137 0.349020 0.341176 +v 2.758119 -33.165501 15.103900 0.650980 0.345098 0.341176 +v 0.038323 -35.330101 16.649599 0.396078 0.176471 0.192157 +v 0.067189 -33.274799 14.942100 0.662745 0.345098 0.352941 +v 2.880549 -31.155199 13.597900 0.701961 0.388235 0.380392 +v 5.936328 -30.985800 14.264301 0.694118 0.396078 0.380392 +v 4.098669 -41.269699 16.459999 0.811765 0.458824 0.486275 +v 8.892108 -41.014599 17.645000 0.760784 0.427451 0.450980 +v 9.781240 -30.931801 15.664701 0.682353 0.400000 0.376471 +v 13.905798 -40.587799 20.457802 0.658824 0.360784 0.364706 +v 18.077698 -39.992001 24.048601 0.603922 0.341176 0.325490 +v 0.069898 -31.269300 13.405600 0.725490 0.411765 0.407843 +v 37.322296 65.013603 24.625504 0.709804 0.498039 0.396078 +v 29.652199 63.115200 19.714302 0.780392 0.560784 0.466667 +v 21.287199 60.616798 16.368002 0.847059 0.623529 0.533333 +v 0.127883 -14.200400 12.254200 0.741176 0.509804 0.435294 +v 0.278404 1.040420 -3.071150 0.870588 0.596078 0.513725 +v -0.116712 37.803699 17.313200 0.882353 0.650980 0.533333 +v 0.279360 -7.378550 -0.992634 0.819608 0.560784 0.478431 +v 0.046502 68.758400 15.460200 0.854902 0.607843 0.494118 +v 0.599951 99.454498 24.243299 0.349020 0.235294 0.180392 +v 0.031221 -35.395699 16.746401 0.458824 0.203922 0.219608 +v -0.106864 -41.376099 16.290199 0.800000 0.447059 0.474510 +v -53.841106 -48.983299 71.426689 0.407843 0.274510 0.215686 +v -51.275307 -53.996498 75.538391 0.368627 0.247059 0.192157 +v -44.535206 -60.645199 65.560791 0.411765 0.274510 0.211765 +v -18.750103 -74.796997 35.786999 0.529412 0.372549 0.298039 +v -14.085703 -77.391403 34.978100 0.509804 0.364706 0.290196 +v -11.957403 -73.963402 30.096098 0.635294 0.450980 0.368627 +v -6.566503 -77.262100 30.449800 0.596078 0.431373 0.349020 +v -7.564453 -78.871101 33.695099 0.501961 0.360784 0.290196 +v -22.016403 -70.478699 35.529198 0.588235 0.415686 0.329412 +v -17.863401 -68.837303 30.791098 0.674510 0.478431 0.388235 +v -39.540703 -65.852203 65.266602 0.180392 0.121569 0.090196 +v -33.939205 -69.790604 58.118694 0.388235 0.262745 0.200000 +v -31.925804 -67.925903 48.449497 0.458824 0.317647 0.239216 +v -38.444904 -64.204102 56.125298 0.450980 0.305882 0.231373 +v -28.939505 -72.407097 50.816597 0.392157 0.270588 0.203922 +v -23.188004 -74.982597 43.634598 0.415686 0.290196 0.223529 +v -26.559404 -70.188103 41.544697 0.505882 0.352941 0.270588 +v -24.695004 -75.705597 50.416195 0.364706 0.250980 0.192157 +v -16.072004 -78.604698 40.981701 0.403922 0.282353 0.223529 +v -17.844904 -79.063103 47.658199 0.360784 0.250980 0.196078 +v -29.056807 -73.493698 56.917194 0.176471 0.117647 0.090196 +v -10.376904 -81.684601 48.519402 0.168627 0.121569 0.101961 +v -19.158804 -78.889801 51.744801 0.176471 0.121569 0.094118 +v -9.669264 -81.344200 44.934101 0.376471 0.274510 0.223529 +v -27.571703 -64.815102 39.005997 0.603922 0.423529 0.329412 +v -33.198803 -61.133202 43.968197 0.576471 0.403922 0.305882 +v -38.691704 -59.988998 50.588497 0.525490 0.364706 0.274510 +v -42.734406 -58.145699 55.489697 0.501961 0.341176 0.258824 +v -8.533234 -80.387604 38.906601 0.407843 0.294118 0.239216 +v -11.915502 -67.795502 26.995598 0.745098 0.529412 0.443137 +v -23.853804 -66.357597 35.367195 0.635294 0.447059 0.352941 +v -34.301003 104.745003 38.021896 0.207843 0.141176 0.109804 +v -35.090004 101.537003 36.596096 0.427451 0.282353 0.215686 +v -26.006802 101.713997 31.525198 0.270588 0.176471 0.133333 +v -59.540703 -0.172551 48.112595 0.619608 0.403922 0.325490 +v -50.470104 -9.587020 38.842495 0.729412 0.490196 0.411765 +v -52.236305 -0.444375 39.310497 0.745098 0.501961 0.419608 +v -29.805405 -56.547798 37.997498 0.666667 0.474510 0.364706 +v -26.022703 -54.744801 34.242695 0.694118 0.490196 0.376471 +v -26.218403 -49.663700 32.767998 0.717647 0.498039 0.388235 +v -25.394102 -44.041801 30.750198 0.729412 0.505882 0.396078 +v -31.448503 -44.790001 34.639797 0.737255 0.525490 0.403922 +v -32.419804 -50.899601 37.823597 0.690196 0.490196 0.376471 +v -21.044302 -62.170700 31.980398 0.705882 0.498039 0.400000 +v -26.501503 -59.568001 36.049995 0.670588 0.474510 0.368627 +v -44.034306 -51.844898 51.117294 0.560784 0.384314 0.294118 +v -43.882004 -45.489799 45.821396 0.611765 0.419608 0.329412 +v -39.261204 -54.099098 46.284496 0.592157 0.411765 0.309804 +v -42.625404 -40.450100 41.156296 0.662745 0.458824 0.360784 +v -38.801704 -42.761002 38.411594 0.713725 0.501961 0.396078 +v -52.231606 -19.653799 44.279896 0.658824 0.443137 0.356863 +v -46.410305 -16.803400 36.662895 0.756863 0.517647 0.427451 +v -33.261505 -35.711102 31.876297 0.741176 0.537255 0.423529 +v -32.671204 -40.674500 33.693596 0.749020 0.545098 0.423529 +v -35.453705 -44.188400 36.643997 0.733333 0.529412 0.411765 +v -27.096502 -39.330601 30.955198 0.713725 0.498039 0.384314 +v -21.293402 -21.040701 22.753899 0.729412 0.498039 0.407843 +v -23.889301 -17.153200 25.090498 0.796078 0.537255 0.431373 +v -27.442001 -24.677700 25.678598 0.741176 0.529412 0.435294 +v -27.894802 -35.143299 29.500797 0.698039 0.494118 0.384314 +v -24.245901 -32.390999 25.918598 0.666667 0.470588 0.380392 +v -26.250002 -28.942699 25.473698 0.721569 0.529412 0.431373 +v -22.010002 -41.590900 27.971397 0.670588 0.450980 0.364706 +v -20.023802 -45.808498 26.881199 0.705882 0.486275 0.384314 +v -18.923801 -42.662899 25.164799 0.666667 0.435294 0.364706 +v -28.864202 -21.376200 26.642797 0.784314 0.545098 0.443137 +v -26.229301 -16.210501 26.192198 0.827451 0.568627 0.454902 +v -30.681902 -16.933201 27.620897 0.858824 0.603922 0.490196 +v -15.621302 -20.221901 19.470098 0.690196 0.470588 0.380392 +v -16.465702 -15.840100 21.345999 0.729412 0.474510 0.384314 +v -22.337402 -3.399040 25.406399 0.741176 0.494118 0.396078 +v -21.458002 -7.348170 25.506098 0.721569 0.474510 0.380392 +v -18.854101 -4.005630 23.601898 0.533333 0.301961 0.250980 +v -26.091501 -11.087000 26.476297 0.878431 0.623529 0.513725 +v -23.841803 -13.780800 25.798899 0.831373 0.572549 0.458824 +v -3.088731 -28.148701 13.002100 0.811765 0.521569 0.478431 +v -3.510681 -24.156099 13.543200 0.788235 0.541176 0.450980 +v -7.061972 -23.912500 14.032299 0.764706 0.529412 0.435294 +v -3.687661 -20.046000 14.031400 0.807843 0.584314 0.494118 +v -9.043321 -19.022600 15.774699 0.705882 0.494118 0.403922 +v -2.931611 -17.201000 13.721800 0.807843 0.584314 0.494118 +v -4.743171 -14.022100 13.188100 0.737255 0.505882 0.431373 +v -30.459501 -31.690300 28.703999 0.729412 0.537255 0.435294 +v -36.833103 -39.026501 35.269897 0.749020 0.537255 0.423529 +v -36.962105 -31.691099 32.883995 0.741176 0.517647 0.411765 +v -32.433002 -26.637400 28.764599 0.749020 0.529412 0.427451 +v -10.186301 -10.045900 13.821699 0.560784 0.341176 0.290196 +v -3.445531 -10.998200 9.252610 0.556863 0.337255 0.278431 +v -6.762481 -8.908630 9.055569 0.494118 0.294118 0.258824 +v -10.854001 -7.895510 11.215299 0.427451 0.250980 0.211765 +v -12.950301 -8.438200 13.560799 0.588235 0.368627 0.305882 +v -14.310402 -9.532510 16.465097 0.674510 0.411765 0.333333 +v -14.624502 -11.047600 19.142298 0.615686 0.345098 0.282353 +v -12.154902 -13.660100 18.041199 0.662745 0.403922 0.337255 +v -15.637602 -12.306800 21.315199 0.635294 0.376471 0.313725 +v -16.678001 0.018672 17.475899 0.454902 0.239216 0.188235 +v -14.077101 3.077540 13.715799 0.470588 0.254902 0.203922 +v -15.178302 3.616140 17.563799 0.490196 0.270588 0.215686 +v -11.450801 5.402890 9.661229 0.498039 0.278431 0.219608 +v -12.646701 6.457730 14.109399 0.513725 0.294118 0.231373 +v -17.444702 -0.924688 20.193298 0.450980 0.227451 0.184314 +v -18.059002 -7.877630 23.064198 0.529412 0.286275 0.243137 +v -10.368801 8.722010 10.384299 0.521569 0.301961 0.235294 +v -17.744202 0.517129 21.854698 0.521569 0.294118 0.239216 +v -17.907402 -3.455530 20.442198 0.458824 0.231373 0.188235 +v -16.974201 -10.677600 22.292898 0.596078 0.341176 0.286275 +v -2.506120 -2.028510 -3.080180 0.913725 0.674510 0.607843 +v -2.469770 -4.768590 -2.486050 0.886275 0.623529 0.552941 +v -5.439900 -1.886710 -1.785390 0.847059 0.584314 0.517647 +v -4.991440 -4.960640 -1.139691 0.803922 0.541176 0.462745 +v -7.917010 -5.451350 2.487329 0.647059 0.415686 0.341176 +v -5.044560 -7.893640 1.983989 0.650980 0.415686 0.349020 +v -5.685171 -8.434240 5.155859 0.486275 0.290196 0.243137 +v -7.915720 -7.024910 5.173709 0.537255 0.337255 0.282353 +v -5.069190 8.041960 2.428190 0.662745 0.396078 0.317647 +v -5.017550 3.745630 -0.921486 0.737255 0.454902 0.372549 +v -2.789870 4.372350 -1.462200 0.772549 0.486275 0.396078 +v -4.999330 1.117930 -1.968431 0.792157 0.509804 0.427451 +v -2.481880 1.025610 -2.840810 0.854902 0.576471 0.494118 +v -3.088550 -7.134860 -0.568535 0.784314 0.529412 0.447059 +v -2.741080 -9.109190 1.948480 0.694118 0.462745 0.384314 +v -2.959080 -10.054000 5.285800 0.576471 0.360784 0.298039 +v -11.491201 -4.921930 6.367449 0.674510 0.447059 0.372549 +v -10.020901 -7.086960 8.412829 0.474510 0.290196 0.243137 +v -14.259301 -5.239610 9.939149 0.690196 0.443137 0.368627 +v -12.878401 -7.387150 11.456599 0.564706 0.352941 0.290196 +v -16.464901 -5.716880 13.556098 0.647059 0.388235 0.317647 +v -14.983801 -8.065290 14.371499 0.670588 0.415686 0.337255 +v -17.583902 -6.038910 17.100498 0.560784 0.305882 0.247059 +v -16.457602 -8.372230 17.068798 0.623529 0.352941 0.286275 +v -6.973730 2.091660 -0.242824 0.705882 0.435294 0.356863 +v -8.818600 -0.891477 1.383239 0.690196 0.439216 0.364706 +v -9.290710 3.329020 3.458919 0.580392 0.337255 0.266667 +v -17.710503 -6.541460 20.280298 0.498039 0.250980 0.207843 +v -16.597502 -9.103660 19.840698 0.560784 0.290196 0.239216 +v -20.254002 -10.584600 24.919098 0.725490 0.466667 0.380392 +v -19.110601 -13.563800 23.751698 0.756863 0.490196 0.400000 +v -11.853701 1.210450 5.993569 0.552941 0.313725 0.247059 +v -12.033501 -1.648010 5.569239 0.631373 0.388235 0.313725 +v -8.771261 9.366440 7.745239 0.549020 0.317647 0.247059 +v -8.091181 14.909600 10.967199 0.588235 0.352941 0.278431 +v -11.574701 11.120500 14.904899 0.564706 0.341176 0.262745 +v -8.252582 23.095100 17.156300 0.592157 0.356863 0.274510 +v -12.164402 18.366899 20.500298 0.607843 0.372549 0.286275 +v -21.284002 3.497090 24.570698 0.768627 0.525490 0.431373 +v -10.742502 43.732601 19.227800 0.772549 0.552941 0.447059 +v -12.018201 50.872398 15.973799 0.796078 0.596078 0.513725 +v -53.977802 58.171299 39.694695 0.501961 0.325490 0.250980 +v -55.620903 64.281502 44.285595 0.509804 0.329412 0.247059 +v -20.308903 46.723099 20.792599 0.607843 0.427451 0.345098 +v -12.843102 40.605900 23.333698 0.694118 0.474510 0.368627 +v -11.662601 58.885700 14.851099 0.890196 0.662745 0.576471 +v -20.507902 69.580101 17.878298 0.800000 0.549020 0.435294 +v -27.933802 70.843803 20.968597 0.749020 0.509804 0.400000 +v -36.014503 72.235001 26.102497 0.690196 0.466667 0.356863 +v -43.836502 71.610802 32.086895 0.631373 0.423529 0.317647 +v -43.620102 65.109001 29.763796 0.635294 0.427451 0.329412 +v -50.451702 68.894699 38.239597 0.564706 0.372549 0.278431 +v -49.542004 63.241699 35.253395 0.576471 0.380392 0.290196 +v -29.960102 38.037498 27.851099 0.282353 0.203922 0.168627 +v -32.476303 35.348900 29.197298 0.176471 0.149020 0.133333 +v -36.246204 37.764400 28.355696 0.329412 0.254902 0.223529 +v -29.328802 28.733900 30.342098 0.529412 0.392157 0.360784 +v -35.523403 30.304701 31.130497 0.482353 0.411765 0.400000 +v -40.727005 36.324501 31.155497 0.392157 0.313725 0.294118 +v -40.839203 30.600300 33.689297 0.568627 0.435294 0.411765 +v -46.191402 34.452000 36.131695 0.427451 0.274510 0.223529 +v -24.977001 28.765301 30.636297 0.627451 0.431373 0.372549 +v -23.804102 30.909100 31.155798 0.686275 0.556863 0.521569 +v -24.319902 35.130699 30.242397 0.592157 0.521569 0.505882 +v -19.257402 31.576700 30.228998 0.611765 0.411765 0.341176 +v -23.152302 22.654400 30.191898 0.721569 0.498039 0.431373 +v -44.272903 23.796400 36.654297 0.666667 0.454902 0.360784 +v -48.903404 28.476500 40.922897 0.552941 0.352941 0.274510 +v -30.033503 20.755199 30.809198 0.784314 0.556863 0.474510 +v -37.799706 20.942499 32.892296 0.772549 0.545098 0.443137 +v -43.489803 41.473999 30.150797 0.639216 0.439216 0.360784 +v -30.361101 43.423199 25.189299 0.698039 0.494118 0.392157 +v -22.342802 38.114101 28.789198 0.560784 0.376471 0.301961 +v -19.248802 37.309799 29.324799 0.603922 0.396078 0.309804 +v -26.459501 38.629799 27.978798 0.427451 0.305882 0.258824 +v -24.593601 42.781898 25.542498 0.670588 0.466667 0.360784 +v -33.258904 40.763699 26.193096 0.592157 0.419608 0.345098 +v -37.950806 45.187599 25.795197 0.741176 0.533333 0.435294 +v -37.224903 41.155800 26.895596 0.592157 0.411765 0.337255 +v -40.750904 39.547798 29.199097 0.537255 0.368627 0.305882 +v -17.907701 40.627102 25.913097 0.686275 0.478431 0.372549 +v -12.980102 34.449100 27.123398 0.639216 0.439216 0.352941 +v -9.614821 36.339699 23.000999 0.678431 0.458824 0.352941 +v -42.234406 27.275101 34.868195 0.588235 0.388235 0.329412 +v -10.124502 28.367300 23.751200 0.580392 0.372549 0.282353 +v -18.888203 24.715599 29.982798 0.658824 0.454902 0.380392 +v -17.454302 20.764400 27.755598 0.639216 0.419608 0.321569 +v -13.572302 24.756201 26.250998 0.592157 0.388235 0.286275 +v -22.865002 17.581499 29.052998 0.733333 0.505882 0.411765 +v -30.623402 15.645700 30.650799 0.803922 0.576471 0.482353 +v -41.184105 16.006500 34.079998 0.796078 0.560784 0.450980 +v -47.160904 20.448400 38.277298 0.705882 0.490196 0.388235 +v -51.919205 25.598900 43.368195 0.623529 0.423529 0.321569 +v -52.268005 32.498501 43.699497 0.568627 0.372549 0.290196 +v -55.200302 32.461800 46.434196 0.611765 0.419608 0.329412 +v -53.071705 37.138699 42.057297 0.576471 0.388235 0.301961 +v -29.791203 24.710300 30.338198 0.729412 0.513725 0.439216 +v -36.564205 24.958700 31.851896 0.698039 0.490196 0.415686 +v -7.289592 -47.139000 20.103399 0.694118 0.474510 0.392157 +v -14.413102 -46.868698 23.217598 0.705882 0.486275 0.392157 +v -14.603202 -50.371601 25.371597 0.741176 0.517647 0.407843 +v -59.491802 32.046799 51.147495 0.603922 0.415686 0.321569 +v -55.639103 22.632401 45.670296 0.639216 0.431373 0.325490 +v -49.984604 16.973101 39.163597 0.725490 0.494118 0.388235 +v -41.621704 8.346640 32.884995 0.843137 0.576471 0.478431 +v -56.876003 39.973801 45.675396 0.556863 0.372549 0.298039 +v -59.767105 43.045399 49.976097 0.501961 0.329412 0.258824 +v -54.420605 42.338902 40.295395 0.568627 0.384314 0.313725 +v -56.593704 46.599098 42.486496 0.454902 0.294118 0.239216 +v -51.546104 43.681000 36.144497 0.615686 0.423529 0.349020 +v -53.910103 50.838902 37.405296 0.403922 0.266667 0.215686 +v -47.176605 46.687801 30.461897 0.658824 0.470588 0.400000 +v -49.335606 53.263500 31.465897 0.419608 0.286275 0.239216 +v -41.835705 49.451500 25.770996 0.623529 0.458824 0.388235 +v -43.368805 54.963402 26.223997 0.435294 0.305882 0.258824 +v -36.252106 50.171902 23.068497 0.549020 0.400000 0.341176 +v -37.054005 55.634499 22.359797 0.482353 0.352941 0.301961 +v -28.583101 48.147099 22.169699 0.588235 0.423529 0.345098 +v -30.643501 56.322201 19.778698 0.596078 0.443137 0.384314 +v -35.441105 81.907204 28.633097 0.662745 0.443137 0.337255 +v -44.335304 80.277100 35.155895 0.584314 0.388235 0.286275 +v -23.805302 80.078201 22.057999 0.756863 0.517647 0.403922 +v -11.024801 69.106598 16.104898 0.835294 0.584314 0.470588 +v -11.198602 81.384598 19.098099 0.800000 0.556863 0.435294 +v -21.602503 53.225300 17.283098 0.596078 0.443137 0.384314 +v -5.917871 37.487801 19.084600 0.811765 0.580392 0.466667 +v -4.247971 24.037399 12.356400 0.686275 0.435294 0.345098 +v -14.467901 -2.056030 9.347089 0.611765 0.356863 0.290196 +v -15.684502 8.225480 20.506998 0.647059 0.411765 0.325490 +v -30.492302 1.468230 27.575397 0.847059 0.576471 0.486275 +v -29.855103 -6.375450 26.988998 0.890196 0.627451 0.537255 +v -41.125404 -1.490210 31.515596 0.854902 0.580392 0.494118 +v -35.953903 -6.553680 28.526196 0.882353 0.615686 0.525490 +v -44.049503 -10.001100 33.293495 0.819608 0.564706 0.478431 +v -36.408905 -12.117100 29.324495 0.878431 0.615686 0.517647 +v -23.772902 26.589300 30.467398 0.690196 0.466667 0.400000 +v -7.501412 -50.686699 23.065300 0.737255 0.509804 0.403922 +v -13.754702 -61.147598 27.150497 0.772549 0.545098 0.443137 +v -7.330832 -54.754902 24.476400 0.796078 0.552941 0.447059 +v -5.799892 -60.043400 24.272900 0.815686 0.568627 0.478431 +v -14.562901 -54.969002 26.952198 0.784314 0.549020 0.431373 +v -20.703901 -49.802700 28.816797 0.729412 0.501961 0.392157 +v -14.287502 -43.688099 21.461899 0.698039 0.447059 0.392157 +v -9.185982 -44.236801 18.529900 0.749020 0.454902 0.427451 +v -4.844681 -44.535198 17.332600 0.772549 0.458824 0.443137 +v -13.385301 1.321240 9.319969 0.517647 0.282353 0.223529 +v -17.594803 -2.992360 17.184399 0.494118 0.258824 0.207843 +v -15.316101 0.718187 13.531699 0.490196 0.262745 0.211765 +v -16.484503 -2.517690 13.317098 0.556863 0.305882 0.247059 +v -63.277405 35.791199 57.119396 0.525490 0.345098 0.266667 +v -62.593704 26.610701 54.955097 0.576471 0.380392 0.290196 +v -62.468002 46.489700 55.637897 0.466667 0.301961 0.227451 +v -65.474205 48.985901 62.915993 0.419608 0.262745 0.196078 +v -66.472511 36.767601 64.102196 0.454902 0.290196 0.219608 +v -66.524406 22.424500 62.011292 0.490196 0.309804 0.235294 +v -61.938904 10.699000 51.148296 0.568627 0.364706 0.286275 +v -68.543007 11.577000 68.501892 0.411765 0.258824 0.200000 +v -64.340508 3.476640 57.093597 0.498039 0.313725 0.247059 +v -7.594552 30.348700 20.315800 0.627451 0.396078 0.298039 +v -16.549402 14.557100 23.902597 0.662745 0.419608 0.325490 +v -22.511202 11.211700 26.775698 0.764706 0.505882 0.407843 +v -30.923302 8.988230 29.229298 0.815686 0.549020 0.447059 +v -59.490303 18.310400 48.448795 0.607843 0.396078 0.301961 +v -53.647404 10.434700 40.909695 0.737255 0.494118 0.403922 +v -4.381762 31.257401 16.638100 0.737255 0.494118 0.388235 +v -60.029804 58.640598 51.293797 0.462745 0.294118 0.219608 +v -64.019104 60.767300 59.519794 0.392157 0.250980 0.188235 +v -58.184303 51.842499 46.189598 0.458824 0.298039 0.227451 +v -57.762302 70.295197 48.925396 0.447059 0.290196 0.215686 +v -51.554703 76.438698 41.738796 0.513725 0.337255 0.247059 +v -11.345502 93.571404 23.007797 0.709804 0.482353 0.372549 +v -23.729801 93.736099 26.669699 0.647059 0.435294 0.329412 +v -35.271004 92.898697 32.481598 0.556863 0.364706 0.274510 +v -44.764503 89.974899 39.158897 0.482353 0.313725 0.235294 +v -52.434803 84.860397 45.720497 0.427451 0.278431 0.207843 +v -61.925304 71.077103 55.743397 0.360784 0.239216 0.180392 +v -58.381702 78.523102 51.614697 0.380392 0.250980 0.188235 +v -60.005302 84.621002 56.387897 0.313725 0.215686 0.168627 +v -64.720604 74.840401 61.180397 0.278431 0.196078 0.152941 +v -67.735107 63.471001 67.175995 0.274510 0.188235 0.149020 +v -68.669807 50.221901 71.097496 0.333333 0.215686 0.168627 +v -69.477310 37.736198 72.644493 0.352941 0.227451 0.176471 +v -69.931206 24.711700 72.207596 0.388235 0.239216 0.188235 +v -10.646601 -24.065701 15.460499 0.713725 0.498039 0.403922 +v -15.570902 -24.833599 18.256699 0.694118 0.490196 0.400000 +v -14.838702 -28.652700 17.550598 0.725490 0.498039 0.427451 +v -19.051203 -30.614100 21.181898 0.694118 0.478431 0.403922 +v -21.021603 -26.519899 21.785698 0.698039 0.501961 0.415686 +v -36.361103 -22.694599 30.639095 0.811765 0.568627 0.462745 +v -40.278202 -36.076199 36.963898 0.725490 0.509804 0.403922 +v -43.906403 -32.487301 39.282295 0.705882 0.486275 0.392157 +v -38.385506 -47.603401 40.941395 0.666667 0.470588 0.364706 +v -46.264404 -37.117699 43.960796 0.643137 0.439216 0.349020 +v -50.024704 -34.044201 47.640896 0.619608 0.419608 0.337255 +v -49.385105 -40.522999 50.694595 0.596078 0.403922 0.321569 +v -48.941204 -27.787399 43.411995 0.662745 0.450980 0.364706 +v -44.039505 -23.420401 36.323795 0.756863 0.529412 0.431373 +v -40.469105 -27.708000 34.571297 0.768627 0.537255 0.435294 +v -40.584103 -19.038799 32.502396 0.819608 0.576471 0.474510 +v -10.270801 -27.988501 14.987799 0.760784 0.501961 0.447059 +v -20.882402 -54.889301 30.517797 0.729412 0.509804 0.392157 +v -35.545204 -55.400200 42.849697 0.627451 0.439216 0.337255 +v -49.520702 -48.135101 57.375698 0.533333 0.360784 0.282353 +v -47.788506 -54.890400 62.359497 0.466667 0.313725 0.243137 +v -55.262604 -28.814100 53.853397 0.572549 0.380392 0.305882 +v -55.605106 -42.532398 65.974190 0.466667 0.313725 0.247059 +v -55.941505 -35.714100 59.684597 0.529412 0.349020 0.282353 +v -57.457302 -19.495899 52.872997 0.568627 0.376471 0.301961 +v -69.852905 -2.797300 89.256996 0.333333 0.215686 0.168627 +v -70.631210 9.026070 81.100189 0.333333 0.211765 0.164706 +v -72.047607 10.667300 91.656296 0.305882 0.200000 0.160784 +v -65.526306 -21.184401 81.961395 0.352941 0.227451 0.176471 +v -66.953011 -10.110300 77.137192 0.368627 0.235294 0.180392 +v -61.182209 -27.386299 67.969193 0.447059 0.290196 0.227451 +v -67.878204 0.099053 71.940689 0.384314 0.243137 0.188235 +v -62.927803 -15.756000 64.638695 0.447059 0.286275 0.223529 +v -67.671104 -16.365200 92.531090 0.329412 0.215686 0.168627 +v -63.259407 -31.738800 86.818794 0.321569 0.211765 0.164706 +v -60.684906 -35.904400 75.737091 0.392157 0.258824 0.203922 +v -59.222309 -41.333500 80.770195 0.352941 0.235294 0.184314 +v -63.768204 -5.981940 60.569996 0.462745 0.294118 0.231373 +v -57.061104 -11.211700 48.339996 0.615686 0.407843 0.329412 +v -71.885208 24.282400 81.760689 0.294118 0.184314 0.149020 +v -73.277611 35.372601 88.210091 0.105882 0.070588 0.058824 +v -73.102806 23.570700 90.919289 0.247059 0.160784 0.129412 +v -74.068710 29.841700 94.291794 0.094118 0.062745 0.050980 +v -73.606911 22.268999 95.906693 0.137255 0.090196 0.074510 +v -72.477509 7.978640 98.615295 0.152941 0.101961 0.082353 +v -69.024506 -13.136300 99.036591 0.333333 0.215686 0.172549 +v -71.098709 -3.316490 100.109993 0.341176 0.223529 0.180392 +v -70.885109 -6.826520 103.005989 0.192157 0.125490 0.101961 +v -67.819908 -17.923401 100.792992 0.145098 0.094118 0.074510 +v -72.112007 0.218792 102.677994 0.180392 0.117647 0.094118 +v -44.566803 99.373398 43.969296 0.364706 0.243137 0.184314 +v -53.142704 92.472603 50.275696 0.345098 0.231373 0.176471 +v -53.622902 97.571404 53.947998 0.262745 0.176471 0.137255 +v -58.044403 92.007202 57.123795 0.121569 0.086275 0.066667 +v -48.604305 101.376999 49.915794 0.156863 0.105882 0.082353 +v -41.549603 104.077003 43.819798 0.172549 0.113725 0.086275 +v -71.393410 49.452900 78.631592 0.247059 0.168627 0.137255 +v -72.491310 48.692600 82.087189 0.094118 0.066667 0.054902 +v -49.436703 58.866501 33.259895 0.545098 0.364706 0.290196 +v -43.665604 60.113602 27.862896 0.592157 0.403922 0.325490 +v -37.226006 60.068802 23.153795 0.670588 0.474510 0.392157 +v -4.352591 16.224400 7.395720 0.682353 0.419608 0.337255 +v -70.843208 58.216900 75.191589 0.101961 0.074510 0.058824 +v -67.510704 71.916199 66.095291 0.133333 0.098039 0.078431 +v -69.946907 66.516602 71.206192 0.109804 0.078431 0.066667 +v -71.842804 37.894199 81.078789 0.254902 0.168627 0.137255 +v -63.468903 81.661102 60.812897 0.137255 0.094118 0.074510 +v -53.695305 98.844704 54.975895 0.129412 0.086275 0.066667 +v -11.574702 100.000999 25.843098 0.317647 0.211765 0.160784 +v -64.204407 -29.280001 98.039894 0.129412 0.086275 0.066667 +v -57.808506 -44.627602 86.710495 0.313725 0.207843 0.164706 +v -61.904308 -36.036499 93.323090 0.290196 0.192157 0.152941 +v -60.306007 -39.574100 93.578392 0.125490 0.082353 0.066667 +v -55.835407 -48.028099 86.987991 0.156863 0.105882 0.082353 +v -49.323906 -56.665699 77.172592 0.176471 0.117647 0.090196 +v -65.933609 -24.037500 96.256691 0.290196 0.188235 0.149020 +v -37.446705 33.968201 30.858696 0.407843 0.384314 0.380392 +v -39.703102 33.516899 32.215996 0.572549 0.533333 0.541176 +v -27.674603 33.585201 30.179197 0.356863 0.309804 0.290196 +v -23.713802 -35.788601 28.594898 0.482353 0.282353 0.235294 +v -3.734711 -35.148399 16.966900 0.596078 0.274510 0.290196 +v -3.894611 -38.155800 17.165199 0.756863 0.372549 0.407843 +v -8.034562 -35.082500 18.211599 0.584314 0.262745 0.282353 +v -8.452592 -38.003201 18.352200 0.713725 0.349020 0.380392 +v -13.179702 -35.288399 20.849298 0.541176 0.258824 0.270588 +v -13.330002 -37.864399 20.994598 0.635294 0.317647 0.337255 +v -17.577602 -35.332802 23.885798 0.447059 0.223529 0.223529 +v -17.583403 -37.785702 24.382399 0.549020 0.274510 0.278431 +v -21.285702 -34.999901 26.378998 0.474510 0.262745 0.231373 +v -21.699402 -38.312199 27.727598 0.564706 0.325490 0.294118 +v -21.295403 -34.956501 26.346899 0.486275 0.270588 0.239216 +v -14.235902 -31.116699 18.050999 0.666667 0.407843 0.368627 +v -6.377821 -27.889299 13.529899 0.796078 0.509804 0.466667 +v -17.588001 -35.285000 23.855999 0.427451 0.215686 0.211765 +v -18.272202 -33.384800 22.694597 0.576471 0.337255 0.298039 +v -13.185702 -35.228500 20.810898 0.533333 0.254902 0.266667 +v -13.791502 -33.117401 19.544298 0.596078 0.329412 0.313725 +v -8.025412 -35.018002 18.154800 0.576471 0.266667 0.282353 +v -9.206802 -33.017300 17.168400 0.631373 0.345098 0.337255 +v -3.731441 -35.080601 16.883200 0.596078 0.274510 0.290196 +v -5.548481 -33.276402 16.041901 0.643137 0.341176 0.337255 +v -2.653011 -33.159401 15.113800 0.650980 0.341176 0.341176 +v -2.764671 -31.169201 13.618300 0.705882 0.388235 0.384314 +v -5.831661 -30.993200 14.292099 0.698039 0.388235 0.380392 +v -4.266491 -41.302898 16.592800 0.807843 0.458824 0.490196 +v -8.956841 -41.049999 17.898399 0.749020 0.415686 0.439216 +v -9.671581 -30.964100 15.692699 0.686275 0.400000 0.376471 +v -13.864402 -40.635300 20.778898 0.654902 0.360784 0.364706 +v -18.022501 -40.030899 24.391897 0.603922 0.345098 0.325490 +v -36.597202 65.101097 24.312796 0.701961 0.478431 0.380392 +v -29.114101 63.241402 19.640799 0.772549 0.541176 0.447059 +v -21.118702 60.808300 16.469297 0.843137 0.607843 0.517647 +# 845 vertices, 0 vertices normals + +f 1 2 3 +f 4 5 6 +f 7 6 5 +f 8 7 9 +f 10 4 6 +f 3 12 13 +f 14 15 13 +f 14 16 17 +f 4 10 18 +f 17 16 19 +f 19 16 13 +f 5 4 17 +f 20 17 19 +f 21 19 22 +f 22 13 12 +f 23 24 25 +f 26 23 27 +f 10 6 11 +f 18 28 29 +f 14 29 30 +f 3 15 31 +f 32 20 21 +f 32 33 34 +f 27 25 33 +f 9 5 20 +f 35 36 37 +f 7 8 35 +f 31 15 30 +f 18 10 38 +f 39 40 41 +f 42 43 44 +f 45 46 47 +f 47 48 49 +f 45 47 50 +f 28 38 51 +f 46 45 52 +f 29 28 52 +f 53 31 30 +f 54 53 55 +f 56 54 57 +f 61 62 49 +f 61 48 63 +f 64 65 66 +f 67 68 69 +f 48 70 63 +f 71 72 70 +f 73 74 75 +f 64 76 77 +f 78 79 80 +f 79 81 82 +f 83 84 85 +f 84 86 87 +f 86 88 89 +f 91 60 92 +f 60 90 93 +f 94 89 95 +f 94 95 96 +f 94 96 97 +f 94 97 98 +f 94 98 99 +f 94 99 100 +f 94 100 101 +f 94 101 89 +f 101 100 102 +f 103 95 89 +f 104 105 106 +f 107 108 105 +f 112 109 106 +f 113 109 80 +f 115 116 117 +f 116 118 119 +f 119 120 121 +f 122 121 120 +f 124 125 126 +f 125 127 128 +f 127 118 116 +f 129 117 119 +f 130 131 132 +f 131 121 122 +f 120 133 134 +f 133 135 136 +f 135 137 138 +f 137 139 140 +f 141 126 128 +f 125 142 127 +f 144 142 125 +f 96 95 132 +f 139 145 146 +f 145 110 114 +f 110 147 114 +f 147 148 114 +f 147 82 148 +f 104 109 113 +f 144 107 149 +f 96 123 134 +f 120 143 150 +f 143 144 149 +f 151 152 153 +f 152 154 155 +f 100 146 114 +f 145 113 80 +f 78 112 156 +f 161 162 157 +f 166 167 168 +f 167 169 170 +f 169 160 159 +f 172 171 173 +f 190 192 182 +f 193 189 190 +f 189 194 192 +f 195 196 194 +f 188 197 196 +f 162 198 199 +f 198 191 182 +f 178 185 184 +f 202 199 182 +f 203 204 205 +f 203 183 206 +f 183 186 207 +f 186 187 208 +f 187 184 209 +f 184 185 210 +f 185 211 212 +f 213 211 178 +f 179 174 214 +f 174 175 215 +f 201 215 175 +f 216 217 218 +f 210 212 219 +f 209 210 220 +f 221 222 208 +f 223 224 219 +f 225 226 223 +f 227 228 226 +f 229 230 228 +f 229 231 232 +f 231 233 234 +f 233 235 236 +f 166 237 238 +f 165 239 237 +f 164 239 165 +f 240 241 239 +f 161 242 236 +f 243 157 244 +f 245 154 152 +f 246 135 133 +f 153 247 106 +f 106 247 156 +f 156 248 249 +f 248 250 251 +f 250 252 251 +f 250 44 43 +f 253 251 252 +f 82 74 65 +f 202 205 155 +f 214 215 187 +f 183 254 214 +f 182 180 179 +f 193 190 191 +f 189 193 235 +f 195 189 233 +f 188 195 231 +f 178 188 229 +f 178 225 213 +f 196 197 176 +f 194 196 173 +f 192 194 171 +f 182 192 181 +f 255 256 257 +f 258 37 36 +f 259 260 261 +f 262 259 255 +f 71 263 218 +f 264 72 71 +f 265 264 217 +f 266 265 216 +f 266 216 257 +f 150 149 267 +f 143 118 127 +f 144 125 124 +f 80 79 147 +f 109 104 106 +f 104 113 268 +f 105 104 269 +f 107 105 269 +f 106 105 108 +f 151 111 107 +f 267 149 107 +f 151 107 144 +f 268 113 145 +f 109 112 80 +f 270 246 267 +f 268 270 269 +f 270 268 139 +f 246 270 137 +f 119 118 143 +f 131 129 121 +f 136 98 97 +f 138 140 99 +f 100 99 140 +f 102 114 148 +f 101 102 77 +f 87 89 101 +f 89 88 271 +f 103 272 132 +f 129 131 130 +f 128 116 115 +f 126 141 273 +f 153 108 111 +f 211 213 223 +f 219 274 275 +f 220 219 275 +f 274 276 277 +f 275 274 278 +f 275 279 280 +f 281 282 280 +f 154 283 202 +f 155 205 204 +f 284 204 206 +f 285 206 207 +f 286 207 208 +f 221 220 287 +f 288 287 280 +f 286 222 250 +f 285 286 248 +f 284 285 156 +f 155 284 247 +f 182 203 205 +f 289 244 200 +f 244 289 290 +f 202 283 200 +f 276 274 219 +f 291 292 277 +f 293 291 276 +f 291 293 159 +f 294 291 160 +f 169 295 294 +f 158 157 243 +f 163 158 296 +f 240 163 297 +f 298 241 240 +f 241 298 299 +f 239 241 300 +f 237 239 301 +f 238 237 302 +f 295 238 303 +f 295 169 167 +f 294 295 304 +f 305 292 291 +f 305 294 306 +f 305 306 307 +f 292 305 308 +f 277 292 309 +f 278 277 310 +f 279 278 311 +f 312 281 279 +f 42 44 280 +f 313 314 315 +f 313 87 76 +f 87 313 85 +f 69 68 316 +f 66 69 317 +f 64 317 314 +f 77 148 65 +f 92 93 318 +f 319 56 57 +f 56 319 320 +f 57 321 62 +f 57 54 321 +f 54 56 322 +f 323 324 322 +f 325 323 322 +f 326 327 318 +f 253 328 318 +f 66 73 93 +f 93 73 75 +f 82 81 75 +f 82 147 79 +f 249 81 79 +f 253 249 251 +f 249 253 75 +f 85 313 329 +f 314 317 316 +f 76 87 77 +f 84 83 330 +f 86 84 331 +f 88 86 332 +f 36 261 260 +f 259 262 258 +f 259 333 256 +f 47 46 334 +f 55 335 50 +f 262 334 51 +f 334 262 218 +f 46 52 51 +f 11 37 258 +f 38 11 51 +f 335 29 45 +f 55 30 29 +f 53 54 324 +f 336 337 31 +f 91 61 60 +f 327 320 319 +f 326 325 320 +f 59 328 252 +f 325 338 323 +f 1 337 336 +f 339 336 324 +f 340 324 323 +f 338 325 58 +f 342 343 344 +f 345 346 342 +f 346 345 347 +f 348 346 349 +f 350 351 345 +f 345 351 352 +f 352 353 1 +f 347 352 339 +f 349 347 338 +f 354 349 341 +f 43 58 59 +f 355 58 43 +f 282 354 355 +f 281 348 354 +f 344 343 356 +f 343 342 346 +f 343 348 281 +f 356 343 312 +f 356 312 311 +f 357 358 356 +f 358 357 359 +f 344 358 360 +f 342 344 361 +f 362 350 342 +f 48 47 263 +f 49 62 321 +f 61 91 57 +f 61 49 48 +f 363 364 365 +f 366 364 363 +f 326 58 325 +f 326 59 58 +f 326 328 59 +f 183 182 254 +f 184 187 215 +f 303 367 368 +f 302 40 367 +f 369 370 307 +f 371 369 368 +f 372 371 367 +f 373 374 357 +f 301 41 40 +f 226 293 224 +f 228 159 293 +f 230 375 159 +f 232 376 375 +f 234 377 376 +f 234 236 377 +f 161 235 193 +f 162 200 244 +f 162 161 198 +f 242 161 158 +f 378 379 380 +f 245 289 283 +f 380 290 289 +f 378 124 273 +f 152 151 124 +f 381 382 383 +f 384 311 310 +f 381 374 373 +f 385 307 370 +f 308 307 385 +f 309 308 382 +f 373 310 309 +f 306 304 368 +f 352 351 353 +f 65 74 73 +f 82 65 148 +f 44 250 222 +f 288 222 221 +f 371 386 369 +f 387 300 299 +f 301 300 387 +f 386 370 369 +f 39 372 40 +f 388 117 129 +f 390 391 392 +f 390 393 394 +f 2 394 12 +f 391 395 389 +f 390 2 1 +f 390 353 351 +f 391 351 395 +f 395 351 350 +f 395 362 365 +f 397 175 396 +f 173 176 397 +f 397 178 177 +f 175 397 177 +f 180 398 174 +f 398 180 181 +f 178 397 176 +f 172 398 171 +f 172 173 396 +f 172 396 175 +f 399 68 67 +f 400 401 402 +f 403 404 401 +f 405 406 404 +f 407 408 406 +f 409 410 408 +f 399 410 409 +f 410 399 63 +f 413 315 316 +f 411 415 416 +f 415 417 418 +f 417 419 420 +f 419 421 422 +f 421 423 422 +f 421 424 425 +f 426 427 422 +f 426 83 414 +f 399 67 63 +f 266 428 429 +f 427 430 420 +f 70 410 63 +f 264 265 429 +f 72 264 431 +f 410 70 72 +f 406 408 432 +f 404 406 431 +f 401 404 429 +f 428 266 412 +f 402 401 428 +f 413 316 416 +f 430 413 418 +f 329 315 413 +f 414 329 430 +f 433 426 423 +f 330 83 426 +f 159 375 170 +f 375 376 168 +f 376 377 434 +f 435 434 377 +f 165 166 434 +f 164 165 435 +f 436 163 164 +f 436 435 236 +f 436 242 158 +f 1 3 337 +f 7 5 9 +f 8 9 34 +f 11 6 37 +f 3 13 15 +f 14 13 16 +f 14 17 18 +f 4 18 17 +f 19 13 22 +f 5 17 20 +f 20 19 21 +f 21 22 26 +f 23 25 27 +f 26 27 21 +f 10 11 38 +f 18 29 14 +f 14 30 15 +f 3 31 337 +f 32 21 27 +f 32 34 9 +f 27 33 32 +f 9 20 32 +f 35 37 6 +f 35 6 7 +f 18 38 28 +f 47 49 50 +f 45 50 335 +f 28 51 52 +f 29 52 45 +f 53 30 55 +f 54 55 321 +f 60 61 67 +f 61 63 67 +f 67 69 90 +f 71 70 48 +f 64 77 65 +f 78 80 112 +f 83 85 414 +f 84 87 85 +f 86 89 87 +f 67 90 60 +f 91 92 319 +f 60 93 92 +f 103 89 437 +f 115 117 388 +f 116 119 117 +f 119 121 129 +f 122 120 123 +f 123 96 122 +f 124 126 273 +f 125 128 126 +f 127 116 128 +f 130 132 272 +f 131 122 132 +f 120 134 123 +f 133 136 134 +f 135 138 136 +f 137 140 138 +f 141 128 438 +f 143 142 144 +f 96 132 122 +f 139 146 140 +f 145 114 146 +f 96 134 97 +f 120 150 133 +f 143 149 150 +f 151 153 111 +f 152 155 153 +f 108 107 111 +f 100 114 102 +f 145 80 110 +f 157 158 161 +f 164 163 240 +f 167 170 168 +f 169 159 170 +f 182 183 203 +f 188 178 197 +f 190 182 191 +f 189 192 190 +f 195 194 189 +f 188 196 195 +f 162 199 200 +f 198 182 199 +f 178 201 177 +f 178 184 201 +f 202 182 205 +f 203 206 204 +f 183 207 206 +f 186 208 207 +f 187 209 208 +f 184 210 209 +f 185 212 210 +f 179 214 254 +f 174 215 214 +f 201 175 177 +f 216 218 255 +f 210 219 220 +f 209 220 221 +f 221 208 209 +f 178 211 185 +f 223 219 212 +f 225 223 213 +f 227 226 225 +f 229 228 227 +f 229 232 230 +f 231 234 232 +f 233 236 234 +f 166 238 167 +f 165 237 166 +f 240 239 164 +f 161 236 235 +f 243 244 439 +f 245 152 378 +f 246 133 150 +f 153 106 108 +f 106 156 112 +f 156 249 78 +f 248 251 249 +f 250 43 252 +f 253 252 328 +f 214 187 186 +f 183 214 186 +f 182 179 254 +f 193 191 198 +f 189 235 233 +f 195 233 231 +f 188 231 229 +f 178 229 227 +f 178 227 225 +f 197 178 176 +f 196 176 173 +f 194 173 171 +f 192 171 181 +f 182 181 180 +f 255 257 216 +f 258 36 260 +f 259 261 333 +f 262 255 218 +f 71 218 217 +f 264 71 217 +f 265 217 216 +f 266 257 412 +f 150 267 246 +f 143 127 142 +f 144 124 151 +f 80 147 110 +f 107 269 267 +f 268 145 139 +f 270 267 269 +f 268 269 104 +f 270 139 137 +f 246 137 135 +f 119 143 120 +f 136 97 134 +f 98 136 138 +f 138 99 98 +f 100 140 146 +f 102 148 77 +f 101 77 87 +f 89 271 437 +f 103 132 95 +f 129 130 440 +f 128 115 438 +f 211 223 212 +f 220 275 287 +f 274 277 278 +f 275 278 279 +f 275 280 287 +f 281 280 279 +f 154 202 155 +f 155 204 284 +f 284 206 285 +f 285 207 286 +f 286 208 222 +f 221 287 288 +f 288 280 44 +f 286 250 248 +f 285 248 156 +f 284 156 247 +f 155 247 153 +f 289 200 283 +f 244 290 439 +f 202 200 199 +f 276 219 224 +f 291 277 276 +f 293 276 224 +f 291 159 160 +f 169 294 160 +f 158 243 296 +f 163 296 297 +f 240 297 441 +f 298 240 441 +f 241 299 300 +f 239 300 301 +f 237 301 302 +f 238 302 303 +f 295 303 304 +f 295 167 238 +f 294 304 306 +f 305 291 294 +f 305 307 308 +f 292 308 309 +f 277 309 310 +f 278 310 311 +f 279 311 312 +f 42 280 282 +f 313 315 329 +f 313 76 314 +f 69 316 317 +f 66 317 64 +f 64 314 76 +f 92 318 327 +f 319 57 91 +f 56 320 322 +f 54 322 324 +f 325 322 320 +f 326 318 328 +f 253 318 75 +f 90 69 66 +f 66 93 90 +f 93 75 318 +f 82 75 74 +f 249 79 78 +f 249 75 81 +f 85 329 414 +f 314 316 315 +f 84 330 331 +f 86 331 332 +f 88 332 271 +f 259 258 260 +f 259 256 255 +f 47 334 263 +f 55 50 321 +f 262 51 258 +f 334 218 263 +f 46 51 334 +f 11 258 51 +f 55 29 335 +f 53 324 336 +f 336 31 53 +f 327 319 92 +f 326 320 327 +f 1 336 339 +f 339 324 340 +f 340 323 338 +f 338 58 341 +f 341 58 355 +f 345 342 350 +f 346 347 349 +f 348 349 354 +f 345 352 347 +f 352 1 339 +f 347 339 340 +f 347 340 338 +f 349 338 341 +f 354 341 355 +f 43 59 252 +f 355 43 42 +f 282 355 42 +f 281 354 282 +f 344 356 358 +f 343 346 348 +f 343 281 312 +f 356 311 384 +f 357 356 384 +f 358 359 360 +f 344 360 361 +f 342 361 363 +f 362 342 363 +f 48 263 71 +f 49 321 50 +f 61 57 62 +f 363 365 362 +f 184 215 201 +f 303 368 304 +f 302 367 303 +f 369 307 368 +f 371 368 367 +f 372 367 40 +f 373 357 384 +f 301 40 302 +f 226 224 223 +f 228 293 226 +f 230 159 228 +f 232 375 230 +f 234 376 232 +f 161 193 198 +f 162 244 157 +f 378 380 245 +f 245 283 154 +f 380 289 245 +f 378 273 379 +f 152 124 378 +f 384 310 373 +f 308 385 382 +f 309 382 381 +f 373 309 381 +f 306 368 307 +f 65 73 66 +f 44 222 288 +f 387 299 442 +f 301 387 41 +f 388 129 440 +f 390 392 393 +f 390 394 2 +f 2 12 3 +f 391 389 392 +f 390 1 353 +f 390 351 391 +f 395 350 362 +f 395 365 389 +f 173 397 396 +f 180 174 179 +f 398 181 171 +f 172 175 174 +f 172 174 398 +f 400 402 443 +f 403 401 400 +f 405 404 403 +f 407 406 405 +f 409 408 407 +f 411 68 399 +f 411 416 68 +f 415 418 416 +f 417 420 418 +f 419 422 420 +f 421 425 423 +f 426 422 423 +f 426 414 427 +f 266 429 265 +f 427 420 422 +f 264 429 431 +f 72 431 432 +f 410 72 432 +f 408 410 432 +f 406 432 431 +f 404 431 429 +f 401 429 428 +f 428 412 444 +f 402 428 444 +f 413 416 418 +f 430 418 420 +f 316 68 416 +f 329 413 430 +f 414 430 427 +f 433 423 425 +f 330 426 433 +f 375 168 170 +f 376 434 168 +f 435 377 236 +f 166 168 434 +f 165 434 435 +f 164 435 436 +f 436 236 242 +f 436 158 163 +f 363 361 366 +f 445 447 446 +f 448 450 449 +f 451 449 450 +f 8 452 451 +f 453 450 448 +f 447 456 455 +f 457 456 458 +f 457 460 459 +f 448 461 453 +f 460 462 459 +f 462 456 459 +f 449 460 448 +f 463 462 460 +f 464 465 462 +f 465 455 456 +f 466 25 24 +f 467 468 466 +f 453 454 450 +f 461 470 469 +f 457 471 470 +f 447 472 458 +f 473 464 463 +f 473 34 33 +f 468 33 25 +f 452 463 449 +f 35 474 36 +f 451 35 8 +f 472 471 458 +f 461 475 453 +f 476 478 477 +f 479 481 480 +f 482 484 483 +f 484 486 485 +f 482 487 484 +f 469 488 475 +f 483 489 482 +f 470 489 469 +f 490 471 472 +f 491 492 490 +f 493 494 491 +f 498 486 499 +f 498 500 485 +f 501 503 502 +f 504 506 505 +f 485 500 507 +f 508 507 509 +f 510 512 511 +f 501 514 513 +f 515 517 516 +f 516 519 518 +f 520 522 521 +f 521 524 523 +f 523 526 525 +f 528 529 497 +f 497 530 527 +f 531 532 526 +f 531 533 532 +f 531 534 533 +f 531 535 534 +f 531 536 535 +f 531 537 536 +f 531 538 537 +f 531 526 538 +f 538 539 537 +f 103 526 532 +f 540 542 541 +f 543 541 544 +f 548 542 545 +f 549 517 545 +f 115 552 551 +f 551 554 553 +f 554 556 555 +f 557 555 556 +f 559 561 560 +f 560 563 562 +f 562 551 553 +f 564 554 552 +f 130 566 565 +f 565 557 556 +f 555 568 567 +f 567 570 569 +f 569 572 571 +f 571 574 573 +f 141 563 561 +f 560 562 575 +f 577 560 575 +f 533 566 532 +f 573 579 578 +f 578 550 546 +f 546 550 580 +f 580 550 581 +f 580 581 519 +f 540 549 545 +f 577 582 543 +f 533 568 558 +f 555 583 576 +f 576 582 577 +f 584 586 585 +f 585 588 587 +f 537 550 579 +f 578 517 549 +f 515 589 548 +f 594 590 595 +f 599 601 600 +f 600 603 602 +f 602 592 593 +f 605 606 604 +f 623 615 625 +f 626 623 622 +f 622 625 627 +f 628 627 629 +f 621 629 630 +f 595 632 631 +f 631 615 624 +f 611 617 618 +f 635 615 632 +f 636 638 637 +f 636 639 616 +f 616 640 619 +f 619 641 620 +f 620 642 617 +f 617 643 618 +f 618 645 644 +f 646 611 644 +f 612 647 607 +f 607 648 608 +f 634 608 648 +f 649 651 650 +f 643 652 645 +f 642 653 643 +f 654 641 655 +f 656 652 657 +f 658 656 659 +f 660 659 661 +f 662 661 663 +f 662 665 664 +f 664 667 666 +f 666 669 668 +f 599 671 670 +f 598 670 672 +f 597 598 672 +f 673 672 674 +f 594 669 675 +f 243 676 590 +f 677 585 587 +f 678 567 569 +f 586 542 679 +f 542 589 679 +f 589 681 680 +f 680 683 682 +f 682 683 684 +f 682 480 481 +f 685 684 683 +f 519 502 511 +f 635 588 638 +f 647 620 648 +f 616 647 686 +f 615 612 613 +f 626 624 623 +f 622 668 626 +f 628 666 622 +f 621 664 628 +f 611 662 621 +f 611 646 658 +f 629 609 630 +f 627 606 629 +f 625 604 627 +f 615 614 625 +f 687 257 256 +f 688 36 474 +f 689 261 690 +f 691 687 689 +f 508 651 692 +f 693 508 509 +f 694 650 693 +f 695 649 694 +f 695 257 649 +f 583 696 582 +f 576 562 553 +f 577 559 560 +f 517 580 516 +f 545 542 540 +f 540 697 549 +f 541 698 540 +f 543 698 541 +f 542 544 541 +f 584 543 547 +f 696 543 582 +f 584 577 543 +f 697 578 549 +f 545 517 548 +f 699 696 678 +f 697 698 699 +f 699 573 697 +f 678 571 699 +f 554 576 553 +f 565 556 564 +f 570 534 535 +f 572 536 574 +f 537 574 536 +f 539 581 550 +f 538 514 539 +f 524 538 526 +f 526 271 525 +f 103 566 272 +f 564 130 565 +f 563 115 551 +f 561 273 141 +f 586 547 544 +f 644 656 646 +f 652 701 700 +f 653 701 652 +f 700 703 702 +f 701 704 700 +f 701 706 705 +f 707 706 708 +f 587 635 709 +f 588 637 638 +f 710 639 637 +f 711 640 639 +f 712 641 640 +f 654 713 653 +f 714 706 713 +f 712 682 655 +f 711 680 712 +f 710 589 711 +f 588 679 710 +f 615 638 636 +f 715 633 676 +f 676 290 715 +f 635 633 709 +f 702 652 700 +f 716 703 717 +f 718 702 716 +f 716 592 718 +f 719 593 716 +f 602 719 720 +f 591 243 590 +f 596 296 591 +f 673 297 596 +f 298 673 674 +f 674 299 298 +f 672 721 674 +f 670 722 672 +f 671 723 670 +f 720 724 671 +f 720 600 602 +f 719 725 720 +f 726 716 717 +f 726 727 719 +f 726 728 727 +f 717 729 726 +f 703 730 717 +f 704 731 703 +f 705 732 704 +f 733 705 707 +f 479 706 481 +f 734 736 735 +f 734 513 524 +f 524 522 734 +f 506 737 505 +f 503 738 506 +f 501 735 738 +f 514 502 581 +f 529 739 530 +f 740 494 493 +f 493 741 740 +f 494 499 742 +f 494 742 491 +f 491 743 493 +f 744 743 745 +f 746 743 744 +f 747 739 748 +f 685 739 749 +f 503 530 510 +f 530 512 510 +f 519 512 518 +f 519 516 580 +f 681 516 518 +f 685 683 681 +f 681 512 685 +f 522 750 734 +f 735 737 738 +f 513 514 524 +f 521 330 520 +f 523 331 521 +f 525 332 523 +f 36 690 261 +f 689 688 691 +f 689 256 333 +f 484 751 483 +f 492 487 752 +f 691 488 751 +f 751 651 691 +f 483 488 489 +f 454 688 474 +f 475 488 454 +f 752 482 470 +f 492 470 471 +f 490 745 491 +f 753 472 754 +f 528 497 498 +f 748 740 741 +f 747 741 746 +f 496 684 749 +f 746 744 755 +f 445 753 754 +f 756 745 753 +f 757 744 745 +f 755 495 746 +f 759 761 760 +f 762 759 763 +f 763 764 762 +f 765 766 763 +f 767 762 768 +f 762 769 768 +f 769 445 770 +f 764 756 769 +f 766 755 764 +f 771 758 766 +f 480 496 495 +f 772 480 495 +f 708 772 771 +f 707 771 765 +f 761 773 760 +f 760 763 759 +f 760 707 765 +f 773 733 760 +f 773 732 733 +f 774 773 775 +f 775 776 774 +f 761 777 775 +f 759 778 761 +f 779 759 767 +f 485 692 484 +f 486 742 499 +f 498 494 528 +f 498 485 486 +f 780 782 781 +f 783 780 781 +f 747 746 495 +f 747 495 496 +f 747 496 749 +f 616 686 615 +f 617 648 620 +f 724 785 784 +f 723 784 477 +f 786 728 787 +f 788 785 786 +f 789 784 788 +f 790 774 791 +f 722 477 478 +f 659 657 718 +f 661 718 592 +f 663 592 792 +f 665 792 793 +f 667 793 794 +f 667 794 669 +f 594 626 668 +f 595 676 633 +f 595 631 594 +f 675 591 594 +f 795 380 379 +f 677 709 715 +f 380 715 290 +f 795 273 559 +f 585 559 584 +f 796 798 797 +f 799 731 732 +f 796 790 791 +f 800 787 728 +f 729 800 728 +f 730 797 729 +f 790 730 731 +f 727 785 725 +f 769 770 768 +f 502 510 511 +f 519 581 502 +f 481 655 682 +f 714 654 655 +f 788 786 801 +f 802 299 721 +f 722 802 721 +f 801 786 787 +f 476 477 789 +f 388 564 552 +f 804 806 805 +f 804 808 807 +f 446 455 808 +f 805 803 809 +f 804 445 446 +f 804 768 770 +f 805 809 768 +f 809 767 768 +f 809 782 779 +f 811 810 608 +f 606 811 609 +f 811 610 611 +f 608 610 811 +f 613 607 812 +f 812 614 613 +f 611 609 811 +f 605 604 812 +f 605 810 606 +f 605 608 810 +f 813 504 505 +f 814 402 815 +f 816 815 817 +f 818 817 819 +f 820 819 821 +f 822 821 823 +f 813 822 823 +f 823 500 813 +f 825 737 736 +f 824 828 827 +f 827 830 829 +f 829 832 831 +f 831 834 833 +f 833 834 835 +f 833 425 424 +f 836 834 837 +f 836 826 520 +f 813 500 504 +f 695 839 838 +f 837 832 840 +f 507 500 823 +f 693 839 694 +f 509 841 693 +f 823 509 507 +f 819 842 821 +f 817 841 819 +f 815 839 817 +f 838 412 695 +f 402 838 815 +f 825 828 737 +f 840 830 825 +f 750 825 736 +f 826 840 750 +f 433 835 836 +f 330 836 520 +f 592 603 792 +f 792 601 793 +f 793 843 794 +f 844 794 843 +f 598 843 599 +f 597 844 598 +f 845 597 596 +f 845 669 844 +f 845 591 675 +f 445 754 447 +f 451 452 449 +f 8 34 452 +f 454 474 450 +f 447 458 456 +f 457 459 456 +f 457 461 460 +f 448 460 461 +f 462 465 456 +f 449 463 460 +f 463 464 462 +f 464 467 465 +f 466 468 25 +f 467 464 468 +f 453 475 454 +f 461 457 470 +f 457 458 471 +f 447 754 472 +f 473 468 464 +f 473 452 34 +f 468 473 33 +f 452 473 463 +f 35 450 474 +f 35 451 450 +f 461 469 475 +f 484 487 486 +f 482 752 487 +f 469 489 488 +f 470 482 489 +f 490 492 471 +f 491 742 492 +f 497 504 498 +f 498 504 500 +f 504 527 506 +f 508 485 507 +f 501 502 514 +f 515 548 517 +f 520 826 522 +f 521 522 524 +f 523 524 526 +f 504 497 527 +f 528 740 529 +f 497 529 530 +f 103 437 526 +f 115 388 552 +f 551 552 554 +f 554 564 556 +f 557 558 555 +f 558 557 533 +f 559 273 561 +f 560 561 563 +f 562 563 551 +f 130 272 566 +f 565 566 557 +f 555 558 568 +f 567 568 570 +f 569 570 572 +f 571 572 574 +f 141 438 563 +f 576 577 575 +f 533 557 566 +f 573 574 579 +f 578 579 550 +f 533 534 568 +f 555 567 583 +f 576 583 582 +f 584 547 586 +f 585 586 588 +f 544 547 543 +f 537 539 550 +f 578 546 517 +f 590 594 591 +f 597 673 596 +f 600 601 603 +f 602 603 592 +f 615 636 616 +f 621 630 611 +f 623 624 615 +f 622 623 625 +f 628 622 627 +f 621 628 629 +f 595 633 632 +f 631 632 615 +f 611 610 634 +f 611 634 617 +f 635 638 615 +f 636 637 639 +f 616 639 640 +f 619 640 641 +f 620 641 642 +f 617 642 643 +f 618 643 645 +f 612 686 647 +f 607 647 648 +f 634 610 608 +f 649 687 651 +f 643 653 652 +f 642 654 653 +f 654 642 641 +f 611 618 644 +f 656 645 652 +f 658 646 656 +f 660 658 659 +f 662 660 661 +f 662 663 665 +f 664 665 667 +f 666 667 669 +f 599 600 671 +f 598 599 670 +f 673 597 672 +f 594 668 669 +f 243 439 676 +f 677 795 585 +f 678 583 567 +f 586 544 542 +f 542 548 589 +f 589 515 681 +f 680 681 683 +f 682 684 480 +f 685 749 684 +f 647 619 620 +f 616 619 647 +f 615 686 612 +f 626 631 624 +f 622 666 668 +f 628 664 666 +f 621 662 664 +f 611 660 662 +f 611 658 660 +f 630 609 611 +f 629 606 609 +f 627 604 606 +f 625 614 604 +f 615 613 614 +f 687 649 257 +f 688 690 36 +f 689 333 261 +f 691 651 687 +f 508 650 651 +f 693 650 508 +f 694 649 650 +f 695 412 257 +f 583 678 696 +f 576 575 562 +f 577 584 559 +f 517 546 580 +f 543 696 698 +f 697 573 578 +f 699 698 696 +f 697 540 698 +f 699 571 573 +f 678 569 571 +f 554 555 576 +f 570 568 534 +f 535 572 570 +f 572 535 536 +f 537 579 574 +f 539 514 581 +f 538 524 514 +f 526 437 271 +f 103 532 566 +f 564 440 130 +f 563 438 115 +f 644 645 656 +f 653 713 701 +f 700 704 703 +f 701 705 704 +f 701 713 706 +f 707 705 706 +f 587 588 635 +f 588 710 637 +f 710 711 639 +f 711 712 640 +f 712 655 641 +f 654 714 713 +f 714 481 706 +f 712 680 682 +f 711 589 680 +f 710 679 589 +f 588 586 679 +f 715 709 633 +f 676 439 290 +f 635 632 633 +f 702 657 652 +f 716 702 703 +f 718 657 702 +f 716 593 592 +f 602 593 719 +f 591 296 243 +f 596 297 296 +f 673 441 297 +f 298 441 673 +f 674 721 299 +f 672 722 721 +f 670 723 722 +f 671 724 723 +f 720 725 724 +f 720 671 600 +f 719 727 725 +f 726 719 716 +f 726 729 728 +f 717 730 729 +f 703 731 730 +f 704 732 731 +f 705 733 732 +f 479 708 706 +f 734 750 736 +f 734 735 513 +f 506 738 737 +f 503 501 738 +f 501 513 735 +f 529 748 739 +f 740 528 494 +f 493 743 741 +f 491 745 743 +f 746 741 743 +f 747 749 739 +f 685 512 739 +f 527 503 506 +f 503 527 530 +f 530 739 512 +f 519 511 512 +f 681 515 516 +f 681 518 512 +f 522 826 750 +f 735 736 737 +f 521 331 330 +f 523 332 331 +f 525 271 332 +f 689 690 688 +f 689 687 256 +f 484 692 751 +f 492 742 487 +f 691 688 488 +f 751 692 651 +f 483 751 488 +f 454 488 688 +f 492 752 470 +f 490 753 745 +f 753 490 472 +f 748 529 740 +f 747 748 741 +f 445 756 753 +f 756 757 745 +f 757 755 744 +f 755 758 495 +f 758 772 495 +f 762 767 759 +f 763 766 764 +f 765 771 766 +f 762 764 769 +f 769 756 445 +f 764 757 756 +f 764 755 757 +f 766 758 755 +f 771 772 758 +f 480 684 496 +f 772 479 480 +f 708 479 772 +f 707 708 771 +f 761 775 773 +f 760 765 763 +f 760 733 707 +f 773 799 732 +f 774 799 773 +f 775 777 776 +f 761 778 777 +f 759 780 778 +f 779 780 759 +f 485 508 692 +f 486 487 742 +f 498 499 494 +f 780 779 782 +f 617 634 648 +f 724 725 785 +f 723 724 784 +f 786 785 728 +f 788 784 785 +f 789 477 784 +f 790 799 774 +f 722 723 477 +f 659 656 657 +f 661 659 718 +f 663 661 592 +f 665 663 792 +f 667 665 793 +f 594 631 626 +f 595 590 676 +f 795 677 380 +f 677 587 709 +f 380 677 715 +f 795 379 273 +f 585 795 559 +f 799 790 731 +f 729 797 800 +f 730 796 797 +f 790 796 730 +f 727 728 785 +f 502 503 510 +f 481 714 655 +f 802 442 299 +f 722 478 802 +f 388 440 564 +f 804 807 806 +f 804 446 808 +f 446 447 455 +f 805 806 803 +f 804 770 445 +f 804 805 768 +f 809 779 767 +f 809 803 782 +f 606 810 811 +f 613 612 607 +f 812 604 614 +f 605 607 608 +f 605 812 607 +f 814 443 402 +f 816 814 815 +f 818 816 817 +f 820 818 819 +f 822 820 821 +f 824 813 505 +f 824 505 828 +f 827 828 830 +f 829 830 832 +f 831 832 834 +f 833 835 425 +f 836 835 834 +f 836 837 826 +f 695 694 839 +f 837 834 832 +f 693 841 839 +f 509 842 841 +f 823 842 509 +f 821 842 823 +f 819 841 842 +f 817 839 841 +f 815 838 839 +f 838 444 412 +f 402 444 838 +f 825 830 828 +f 840 832 830 +f 737 828 505 +f 750 840 825 +f 826 837 840 +f 433 425 835 +f 330 433 836 +f 792 603 601 +f 793 601 843 +f 844 669 794 +f 599 843 601 +f 598 844 843 +f 597 845 844 +f 845 675 669 +f 845 596 591 +f 780 783 778 +# 1610 faces, 0 coords texture + +# End of File diff --git a/gradio/media_assets/models3d/sofia.stl b/gradio/media_assets/models3d/sofia.stl new file mode 100644 index 0000000..8a811c8 Binary files /dev/null and b/gradio/media_assets/models3d/sofia.stl differ diff --git a/gradio/media_assets/videos/a.mp4 b/gradio/media_assets/videos/a.mp4 new file mode 100644 index 0000000..95a61f6 Binary files /dev/null and b/gradio/media_assets/videos/a.mp4 differ diff --git a/gradio/media_assets/videos/b.avi b/gradio/media_assets/videos/b.avi new file mode 100644 index 0000000..6a146c5 Binary files /dev/null and b/gradio/media_assets/videos/b.avi differ diff --git a/gradio/media_assets/videos/b.mp4 b/gradio/media_assets/videos/b.mp4 new file mode 100644 index 0000000..7b2d7c7 Binary files /dev/null and b/gradio/media_assets/videos/b.mp4 differ diff --git a/gradio/media_assets/videos/muted_b.mp4 b/gradio/media_assets/videos/muted_b.mp4 new file mode 100644 index 0000000..031284a Binary files /dev/null and b/gradio/media_assets/videos/muted_b.mp4 differ diff --git a/gradio/media_assets/videos/world.mp4 b/gradio/media_assets/videos/world.mp4 new file mode 100644 index 0000000..b11552f Binary files /dev/null and b/gradio/media_assets/videos/world.mp4 differ diff --git a/gradio/monitoring_dashboard.py b/gradio/monitoring_dashboard.py new file mode 100644 index 0000000..9d11e6b --- /dev/null +++ b/gradio/monitoring_dashboard.py @@ -0,0 +1,105 @@ +import random +import time + +import pandas as pd + +import gradio as gr + +data = {"data": {}} + +with gr.Blocks() as demo: + gr.Markdown("# Monitoring Dashboard") + timer = gr.Timer(5) + with gr.Row(): + start = gr.DateTime("now - 24h", label="Start Time") + end = gr.DateTime("now", label="End Time") + selected_fn = gr.Dropdown( + ["All"], + value="All", + label="Endpoint", + info="Select the function to see analytics for, or 'All' for aggregate.", + ) + demo.load( + lambda: gr.Dropdown( + choices=["All"] + + list({row["function"] for row in data["data"].values()}) # type: ignore + ), + None, + selected_fn, + ) + + with gr.Group(): + with gr.Row(): + unique_users = gr.Label(label="Unique Users") + total_requests = gr.Label(label="Total Requests") + process_time = gr.Label(label="Avg Process Time") + + plot = gr.BarPlot( + x="time", + y="function", + color="status", + title="Requests over Time", + y_title="Requests", + x_bin="1m", + y_aggregate="count", + color_map={ + "success": "#22c55e", + "failure": "#ef4444", + "pending": "#eab308", + "queued": "#3b82f6", + }, + ) + + @gr.on( + [demo.load, timer.tick, start.change, end.change, selected_fn.change], + inputs=[start, end, selected_fn], + outputs=[plot, unique_users, total_requests, process_time], + ) + def gen_plot(start, end, selected_fn): + if len(data["data"]) == 0: + return {plot: gr.skip()} + df = pd.DataFrame(list(data["data"].values())) + if selected_fn != "All": + df = df[df["function"] == selected_fn] + df = df[(df["time"] >= start) & (df["time"] <= end)] + df["time"] = pd.to_datetime(df["time"], unit="s") # type: ignore + + unique_users = len(df["session_hash"].unique()) # type: ignore + total_requests = len(df) + process_time = round(df["process_time"].mean(), 2) + + duration = end - start + x_bin = ( + "1h" + if duration >= 60 * 60 * 24 + else "15m" + if duration >= 60 * 60 * 3 + else "1m" + ) + df = df.drop(columns=["session_hash"]) # type: ignore + assert isinstance(df, pd.DataFrame) # noqa: S101 + return ( + gr.BarPlot(value=df, x_bin=x_bin, x_lim=[start, end]), + unique_users, + total_requests, + process_time, + ) + + +if __name__ == "__main__": + data["data"] = {} + for _ in range(random.randint(300, 500)): + timedelta = random.randint(0, 60 * 60 * 24 * 3) + data["data"][random.randint(1, 100000)] = { + "time": time.time() - timedelta, + "status": random.choice( + ["success", "success", "failure"] + if timedelta > 30 * 60 + else ["queued", "pending"] + ), + "function": random.choice(["predict", "chat", "chat"]), + "process_time": random.randint(0, 10), + "session_hash": str(random.randint(0, 4)), + } + + demo.launch() diff --git a/gradio/networking.py b/gradio/networking.py new file mode 100644 index 0000000..7b97b71 --- /dev/null +++ b/gradio/networking.py @@ -0,0 +1,83 @@ +""" +Defines helper methods useful for setting up ports, launching servers, and +creating tunnels. +""" + +from __future__ import annotations + +import os +import time +import warnings +from pathlib import Path + +import httpx + +from gradio.exceptions import ShareCertificateWriteError +from gradio.routes import App # HACK: to avoid circular import # noqa: F401 +from gradio.tunneling import CERTIFICATE_PATH, Tunnel + +GRADIO_API_SERVER = "https://api.gradio.app/v3/tunnel-request" +GRADIO_SHARE_SERVER_ADDRESS = os.getenv("GRADIO_SHARE_SERVER_ADDRESS") + + +def setup_tunnel( + local_host: str, + local_port: int, + share_token: str, + share_server_address: str | None, + share_server_tls_certificate: str | None, +) -> str: + share_server_address = ( + GRADIO_SHARE_SERVER_ADDRESS + if share_server_address is None + else share_server_address + ) + if share_server_address is None: + try: + response = httpx.get(GRADIO_API_SERVER, timeout=30) + payload = response.json()[0] + remote_host, remote_port = payload["host"], int(payload["port"]) + certificate = payload["root_ca"] + except Exception as e: + raise RuntimeError( + "Could not get share link from Gradio API Server." + ) from e + try: + Path(CERTIFICATE_PATH).parent.mkdir(parents=True, exist_ok=True) + with open(CERTIFICATE_PATH, "w") as f: + f.write(certificate) + except Exception as e: + raise ShareCertificateWriteError( + f"{e}. This can happen when the current working directory is read-only." + ) from e + share_server_tls_certificate = CERTIFICATE_PATH + + else: + remote_host, remote_port = share_server_address.split(":") + remote_port = int(remote_port) + tunnel = Tunnel( + remote_host, + remote_port, + local_host, + local_port, + share_token, + share_server_tls_certificate, + ) + address = tunnel.start_tunnel() + return address + + +def url_ok(url: str) -> bool: + try: + for _ in range(5): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + r = httpx.head(url, timeout=3, verify=False) + if ( + r.status_code in (200, 401, 301, 302, 303, 307, 308) + ): # 401 or 302 if auth is set; 303 or 307 are alternatives to 302 for temporary redirects; 301 and 308 are permanent redirects + return True + time.sleep(0.500) + except (ConnectionError, httpx.ConnectError, httpx.TimeoutException): + return False + return False diff --git a/gradio/node_server.py b/gradio/node_server.py new file mode 100644 index 0000000..a775397 --- /dev/null +++ b/gradio/node_server.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import os +import signal +import socket +import subprocess +import sys +import time +import warnings +from concurrent.futures import TimeoutError +from contextlib import closing +from http.client import HTTPConnection +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: # Only import for type checking (to avoid circular imports). + pass + +# By default, the local server will try to open on localhost, port 7860. +# If that is not available, then it will try 7861, 7862, ... 7959. +INITIAL_PORT_VALUE = int(os.getenv("GRADIO_SERVER_PORT", "7860")) +TRY_NUM_PORTS = int(os.getenv("GRADIO_NODE_NUM_PORTS", "100")) +LOCALHOST_NAME = os.getenv( + "GRADIO_NODE_SERVER_NAME", os.getenv("GRADIO_SERVER_NAME", "127.0.0.1") +) + + +def start_node_server( + server_name: str | None = None, + server_port: int | None = None, + node_path: str | None = None, + python_port: int | None = None, + python_host: str | None = None, + static_worker_ports: list[int] | None = None, + debug: bool = False, +) -> tuple[str | None, subprocess.Popen[bytes] | None, int | None]: + """Launches the Node SSR server as a front proxy. + + Parameters: + server_name: to make app accessible on local network, set this to "0.0.0.0". Can be set by environment variable GRADIO_SERVER_NAME. + server_port: will start gradio app on this port (if available). Can be set by environment variable GRADIO_SERVER_PORT. + node_path: the path to the node executable. Can be set by environment variable GRADIO_NODE_PATH. + python_port: the port of the main Python (FastAPI) server that Node will proxy to. + python_host: the host of the main Python server (default 127.0.0.1). + static_worker_ports: ports of static file worker processes for round-robin proxying. + + Returns: + server_name: the name of the server (default is "localhost") + node_process: the node process that is running the SSR app + node_port: the port the node server is running on + """ + + server_name = server_name or LOCALHOST_NAME + + # Strip IPv6 brackets from the address if they exist. + # This is needed as http://[::1]:port/ is a valid browser address, + # but not a valid IPv6 address, so asyncio will throw an exception. + if server_name.startswith("[") and server_name.endswith("]"): + host = server_name[1:-1] + else: + host = server_name + + server_ports = ( + [server_port] + if server_port is not None + else range(INITIAL_PORT_VALUE, INITIAL_PORT_VALUE + TRY_NUM_PORTS) + ) + + node_process, node_port = start_node_process( + node_path=node_path or os.getenv("GRADIO_NODE_PATH"), + server_name=host, + server_ports=server_ports, + python_port=python_port, + python_host=python_host or "127.0.0.1", + static_worker_ports=static_worker_ports or [], + debug=debug, + ) + + return server_name, node_process, node_port + + +GRADIO_LOCAL_DEV_MODE = os.getenv("GRADIO_LOCAL_DEV_MODE") is not None +SSR_APP_PATH = Path(__file__).parent.joinpath("templates", "node", "build") + + +def start_node_process( + node_path: str | None, + server_name: str, + server_ports: list[int] | range, + python_port: int | None = None, + python_host: str = "127.0.0.1", + static_worker_ports: list[int] | None = None, + debug: bool = False, +) -> tuple[subprocess.Popen[bytes] | None, int | None]: + if GRADIO_LOCAL_DEV_MODE: + return None, 9876 + if not node_path: + return None, None + + node_process = None + + for port in server_ports: + try: + # The fastest way to check if a port is available is to try to bind to it with socket. + # If the port is not available, socket will throw an OSError. + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # Really, we should be checking if (server_name, server_port) is available, but + # socket.bind() doesn't seem to throw an OSError with ipv6 addresses, based on my testing. + # Instead, we just check if the port is available on localhost. + s.bind((server_name, port)) + s.close() + + # Set environment variables for the Node server + env = os.environ + env["PORT"] = str(port) + env["HOST"] = server_name + if GRADIO_LOCAL_DEV_MODE: + env["GRADIO_LOCAL_DEV_MODE"] = "1" + + # Proxy configuration: tell Node where Python and workers are + if python_port is not None: + env["GRADIO_PYTHON_PORT"] = str(python_port) + env["GRADIO_PYTHON_HOST"] = python_host + if static_worker_ports: + env["GRADIO_STATIC_WORKER_PORTS"] = ",".join( + str(p) for p in static_worker_ports + ) + + register_file = str( + Path(__file__).parent.joinpath("templates", "register.mjs") + ) + + if sys.platform == "win32": + register_file = "file://" + register_file + + node_process = subprocess.Popen( + [node_path, "--import", register_file, SSR_APP_PATH], + env=env, + stdout=subprocess.DEVNULL if not debug else None, + stderr=subprocess.DEVNULL if not debug else None, + ) + + # Node starts only after the Python backend is already + # listening (Blocks.launch defers the front-proxy start), so we + # can verify that Node actually renders a page rather than merely + # opening its TCP port. Polling HEAD / until it succeeds means the + # SSR runtime is warm before we return — otherwise the user-facing + # port is reachable while the first requests 502 as SSR initialises. + is_working = verify_server_startup(server_name, port, timeout=30) + if is_working: + signal.signal( + signal.SIGTERM, lambda _, __: handle_sigterm(node_process) + ) + signal.signal(signal.SIGINT, lambda _, __: handle_sigterm(node_process)) + + return node_process, port + + else: + # If verification failed, terminate the process and try the next port + node_process.terminate() + node_process.wait(timeout=2) + node_process = None + + except OSError: + continue + except Exception as e: + warnings.warn( + f"Unexpected error while starting Node server: {e}. Trying next port..." + ) + if node_process: + node_process.terminate() + node_process = None + continue + + # If all attempts fail + print( + f"Cannot start Node server on any port in the range {server_ports[0]}-{server_ports[-1]}." + ) + print( + "Please install Node 20 or higher and set the environment variable GRADIO_NODE_PATH to the path of your Node executable." + ) + print( + "You can explicitly specify a port by setting the environment variable GRADIO_NODE_PORT." + ) + + return None, None + + +def attempt_connection(host: str, port: int) -> bool: + """Attempts a single connection to the server.""" + try: + with closing(socket.create_connection((host, port), timeout=1)): + return True + except (TimeoutError, ConnectionRefusedError): + return False + except Exception: + return False + + +def verify_server_startup(host: str, port: int, timeout: float = 15.0) -> bool: + """Polls ``HEAD /`` until the server returns a non-5xx status (or the + timeout elapses), confirming it can actually serve requests rather than + just that its TCP port is open.""" + start_time = time.time() + while time.time() - start_time < timeout: + try: + conn = HTTPConnection(host, port, timeout=2) + conn.request("HEAD", "/") + resp = conn.getresponse() + conn.close() + if resp.status < 500: + return True + except Exception: + pass + time.sleep(0.2) + return False + + +def handle_sigterm(node_process: subprocess.Popen[bytes] | None): + if node_process is not None: + print("\nStopping Node.js server...") + node_process.terminate() + node_process.wait() + sys.exit(0) diff --git a/gradio/oauth.py b/gradio/oauth.py new file mode 100644 index 0000000..0622fcd --- /dev/null +++ b/gradio/oauth.py @@ -0,0 +1,378 @@ +from __future__ import annotations + +import hashlib +import os +import time +import typing +import urllib.parse +import warnings +from dataclasses import dataclass, field + +import fastapi +from fastapi.responses import RedirectResponse +from huggingface_hub import get_token, whoami + +from gradio.utils import get_space + +OAUTH_CLIENT_ID = os.environ.get("OAUTH_CLIENT_ID") +OAUTH_CLIENT_SECRET = os.environ.get("OAUTH_CLIENT_SECRET") +OAUTH_SCOPES = os.environ.get("OAUTH_SCOPES") +OPENID_PROVIDER_URL = os.environ.get("OPENID_PROVIDER_URL") + +MAX_REDIRECTS = 2 + + +def attach_oauth(app: fastapi.FastAPI): + try: + from starlette.middleware.sessions import SessionMiddleware + except ImportError as e: + raise ImportError( + "Cannot initialize OAuth to due a missing library. Please run `pip install gradio[oauth]` or add " + "`gradio[oauth]` to your requirements.txt file in order to install the required dependencies." + ) from e + + # Add `/login/huggingface`, `/login/callback` and `/logout` routes to enable OAuth in the Gradio app. + # If the app is running in a Space, OAuth is enabled normally. Otherwise, we mock the "real" routes to make the + # user log in with a fake user profile - without any calls to hf.co. + + if get_space() is not None: + _add_oauth_routes(app) + else: + _add_mocked_oauth_routes(app) + + # Session Middleware requires a secret key to sign the cookies. Let's use a hash + # of the OAuth secret key to make it unique to the Space + updated in case OAuth + # config gets updated. + session_secret = (OAUTH_CLIENT_SECRET or "") + "-v4" + # ^ if we change the session cookie format in the future, we can bump the version of the session secret to make + # sure cookies are invalidated. Otherwise some users with an old cookie format might get a HTTP 500 error. + app.add_middleware( + SessionMiddleware, # type: ignore + secret_key=hashlib.sha256(session_secret.encode()).hexdigest(), + same_site="none", + https_only=True, + ) + + +def _add_oauth_routes(app: fastapi.FastAPI) -> None: + """Add OAuth routes to the FastAPI app (login, callback handler and logout).""" + try: + from authlib.integrations.base_client.errors import MismatchingStateError + from authlib.integrations.starlette_client import OAuth + except ImportError as e: + raise ImportError( + "Cannot initialize OAuth to due a missing library. Please run `pip install gradio[oauth]` or add " + "`gradio[oauth]` to your requirements.txt file in order to install the required dependencies." + ) from e + + # Check environment variables + msg = ( + "OAuth is required but {} environment variable is not set. Make sure you've enabled OAuth in your Space by" + " setting `hf_oauth: true` in the Space metadata." + ) + + if OAUTH_CLIENT_ID is None: + raise ValueError(msg.format("OAUTH_CLIENT_ID")) + if OAUTH_CLIENT_SECRET is None: + raise ValueError(msg.format("OAUTH_CLIENT_SECRET")) + if OAUTH_SCOPES is None: + raise ValueError(msg.format("OAUTH_SCOPES")) + if OPENID_PROVIDER_URL is None: + raise ValueError(msg.format("OPENID_PROVIDER_URL")) + + # Register OAuth server + oauth = OAuth() + oauth.register( + name="huggingface", + client_id=OAUTH_CLIENT_ID, + client_secret=OAUTH_CLIENT_SECRET, + client_kwargs={"scope": OAUTH_SCOPES}, + server_metadata_url=OPENID_PROVIDER_URL + "/.well-known/openid-configuration", + ) + + # Define OAuth routes + @app.get("/login/huggingface") + async def oauth_login(request: fastapi.Request): + """Endpoint that redirects to HF OAuth page.""" + # Define target (where to redirect after login) + redirect_uri = _generate_redirect_uri(request) + return await oauth.huggingface.authorize_redirect(request, redirect_uri) # type: ignore + + @app.get("/login/callback") + async def oauth_redirect_callback(request: fastapi.Request) -> RedirectResponse: + """Endpoint that handles the OAuth callback.""" + try: + oauth_info = await oauth.huggingface.authorize_access_token(request) # type: ignore + except MismatchingStateError: + # If the state mismatch, it is very likely that the cookie is corrupted. + # There is a bug reported in authlib that causes the token to grow indefinitely if the user tries to login + # repeatedly. Since cookies cannot get bigger than 4kb, the token will be truncated at some point - hence + # losing the state. A workaround is to delete the cookie and redirect the user to the login page again. + # See https://github.com/lepture/authlib/issues/622 for more details. + + # Delete all keys that are related to the OAuth state, just in case + for key in list(request.session.keys()): + if key.startswith("_state_huggingface"): + request.session.pop(key) + + # Parse query params + nb_redirects = int(request.query_params.get("_nb_redirects", 0)) + target_url = request.query_params.get("_target_url") + + # Build /login URI with the same query params as before and bump nb_redirects count + query_params: dict[str, str | int] = {"_nb_redirects": nb_redirects + 1} + if target_url: + query_params["_target_url"] = target_url + + login_uri = f"/login/huggingface?{urllib.parse.urlencode(query_params)}" + + # If the user is redirected more than 3 times, it is very likely that the cookie is not working properly. + # (e.g. browser is blocking third-party cookies in iframe). In this case, redirect the user in the + # non-iframe view. + if nb_redirects > MAX_REDIRECTS: + host = os.environ.get("SPACE_HOST") + if host is None: # cannot happen in a Space + raise RuntimeError( + "Gradio is not running in a Space (SPACE_HOST environment variable is not set)." + " Cannot redirect to non-iframe view." + ) from None + host_url = "https://" + host.rstrip("/") + return RedirectResponse(host_url + login_uri) + + # Redirect the user to the login page again + return RedirectResponse(login_uri) + + # OAuth login worked => store the user info in the session and redirect + request.session["oauth_info"] = oauth_info + return _redirect_to_target(request) + + @app.get("/logout") + async def oauth_logout(request: fastapi.Request) -> RedirectResponse: + """Endpoint that logs out the user (e.g. delete cookie session).""" + request.session.pop("oauth_info", None) + return _redirect_to_target(request) + + +def _add_mocked_oauth_routes(app: fastapi.FastAPI) -> None: + """Add fake oauth routes if Gradio is run locally and OAuth is enabled. + + Clicking on a gr.LoginButton will have the same behavior as in a Space (i.e. gets redirected in a new tab) but + instead of authenticating with HF, a mocked user profile is added to the session. + """ + warnings.warn( + "Gradio does not support OAuth features outside of a Space environment. To help" + " you debug your app locally, the login and logout buttons are mocked with your" + " profile. To make it work, your machine must be logged in to Huggingface." + ) + mocked_oauth_info = _get_mocked_oauth_info() + + # Define OAuth routes + @app.get("/login/huggingface") + async def oauth_login(request: fastapi.Request): # noqa: ARG001 + """Fake endpoint that redirects to HF OAuth page.""" + # Define target (where to redirect after login) + redirect_uri = _generate_redirect_uri(request) + return RedirectResponse( + "/login/callback?" + urllib.parse.urlencode({"_target_url": redirect_uri}) + ) + + @app.get("/login/callback") + async def oauth_redirect_callback(request: fastapi.Request) -> RedirectResponse: + """Endpoint that handles the OAuth callback.""" + # `mocked_oauth_info` is computed once at startup, so its `expires_at` would + # eventually be in the past and every login would be treated as expired. + request.session["oauth_info"] = { + **mocked_oauth_info, + "expires_at": int(time.time()) + 8 * 60 * 60, # 8 hours + } + return _redirect_to_target(request) + + @app.get("/logout") + async def oauth_logout(request: fastapi.Request) -> RedirectResponse: + """Endpoint that logs out the user (e.g. delete cookie session).""" + request.session.pop("oauth_info", None) + return _redirect_to_target(request) + + +def _generate_redirect_uri(request: fastapi.Request) -> str: + if "_target_url" in request.query_params: + # if `_target_url` already in query params => respect it + target = request.query_params["_target_url"] + else: + # otherwise => keep query params + target = "/?" + urllib.parse.urlencode(request.query_params) + + # On Spaces, the redirect URI must always be https:///login/callback, + # so if a custom domain is used, we need to replace it with the hf.space URL + if space_host := os.getenv("SPACE_HOST"): + print(f"SPACE_HOST: {space_host}") + space_host = space_host.split(",")[ + 0 + ] # When custom domain is used, SPACE_HOST is a comma-separated list + print(f"SPACE_HOST after split: {space_host}") + redirect_uri = f"https://{space_host}/login/callback?{urllib.parse.urlencode({'_target_url': target})}" + print(f"Redirect URI: {redirect_uri}") + return redirect_uri + + redirect_uri = request.url_for("oauth_redirect_callback").include_query_params( + _target_url=target + ) + redirect_uri_as_str = str(redirect_uri) + return redirect_uri_as_str + + +def _redirect_to_target( + request: fastapi.Request, default_target: str = "/" +) -> RedirectResponse: + target = request.query_params.get("_target_url", default_target) + # Prevent open redirect by stripping scheme/host and only using the path. + parsed = urllib.parse.urlparse(target) + # Collapse any leading slashes/backslashes so the result is always a + # single-slash local path. urlparse leaves 4+ leading slashes in `.path` + # (e.g. "////evil.com" -> "//evil.com"), which browsers resolve as a + # scheme-relative URL to an external host — restoring the CVE-2026-28415 + # open redirect (GHSA-vwgg-rgg9-xx9q). + safe_target = "/" + (parsed.path or "").lstrip("/\\") + if parsed.query: + safe_target += "?" + parsed.query + if parsed.fragment: + safe_target += "#" + parsed.fragment + return RedirectResponse(safe_target) + + +def _get_valid_oauth_info_from_session( + session: typing.MutableMapping[str, typing.Any], +) -> dict[str, typing.Any] | None: + oauth_info = session.get("oauth_info") + if oauth_info is None: + return None + + expires_at = oauth_info.get("expires_at") + if expires_at is not None and expires_at < time.time(): + session.pop("oauth_info", None) + return None + + return oauth_info + + +@dataclass +class OAuthProfile(typing.Dict): # inherit from Dict for backward compatibility + """ + A Gradio OAuthProfile object that can be used to inject the profile of a user in a + function. If a function expects `OAuthProfile` or `Optional[OAuthProfile]` as input, + the value will be injected from the FastAPI session if the user is logged in. If the + user is not logged in and the function expects `OAuthProfile`, an error will be + raised. + + Attributes: + name (str): The name of the user (e.g. 'Abubakar Abid'). + username (str): The username of the user (e.g. 'abidlabs') + profile (str): The profile URL of the user (e.g. 'https://huggingface.co/abidlabs'). + picture (str): The profile picture URL of the user. + + Example: + import gradio as gr + from typing import Optional + + + def hello(profile: Optional[gr.OAuthProfile]) -> str: + if profile is None: + return "I don't know you." + return f"Hello {profile.name}" + + + with gr.Blocks() as demo: + gr.LoginButton() + gr.Markdown().attach_load_event(hello, None) + """ + + name: str = field(init=False) + username: str = field(init=False) + profile: str = field(init=False) + picture: str = field(init=False) + + def __init__(self, data: dict): # hack to make OAuthProfile backward compatible + self.update(data) + self.name = self["name"] + self.username = self["preferred_username"] + self.profile = self["profile"] + self.picture = self["picture"] + + +@dataclass +class OAuthToken: + """ + A Gradio OAuthToken object that can be used to inject the access token of a user in a + function. If a function expects `OAuthToken` or `Optional[OAuthToken]` as input, + the value will be injected from the FastAPI session if the user is logged in. If the + user is not logged in and the function expects `OAuthToken`, an error will be + raised. + + Attributes: + token (str): The access token of the user. + scope (str): The scope of the access token. + expires_at (int): The expiration timestamp of the access token. + + Example: + import gradio as gr + from typing import Optional + from huggingface_hub import whoami + + + def list_organizations(oauth_token: Optional[gr.OAuthToken]) -> str: + if oauth_token is None: + return "Please log in to list organizations." + org_names = [org["name"] for org in whoami(oauth_token.token)["orgs"]] + return f"You belong to {', '.join(org_names)}." + + + with gr.Blocks() as demo: + gr.LoginButton() + gr.Markdown().attach_load_event(list_organizations, None) + """ + + token: str + scope: str + expires_at: int + + +def _get_mocked_oauth_info() -> typing.Dict: + token = get_token() + if token is None: + raise ValueError( + "Your machine must be logged in to HF to debug a Gradio app locally. Please" + " run `huggingface-cli login` or set `HF_TOKEN` as environment variable " + "with one of your access token. You can generate a new token in your " + "settings page (https://huggingface.co/settings/tokens)." + ) + + user = whoami() + if user["type"] != "user": + raise ValueError( + "Your machine is not logged in with a personal account. Please use a " + "personal access token. You can generate a new token in your settings page" + " (https://huggingface.co/settings/tokens)." + ) + + return { + "access_token": "mock-oauth-token-for-local-dev", + "token_type": "bearer", + "expires_in": 3600, + "id_token": "AAAAAAAAAAAAAAAAAAAAAAAAAA", + "scope": "openid profile", + "expires_at": int(time.time()) + 8 * 60 * 60, # 8 hours + "userinfo": { + "sub": "11111111111111111111111", + "name": user["fullname"], + "preferred_username": user["name"], + "profile": f"https://huggingface.co/{user['name']}", + "picture": user["avatarUrl"], + "website": "", + "aud": "00000000-0000-0000-0000-000000000000", + "auth_time": 1691672844, + "nonce": "aaaaaaaaaaaaaaaaaaa", + "iat": 1691672844, + "exp": 1691676444, + "iss": "https://huggingface.co", + }, + } diff --git a/gradio/package.json b/gradio/package.json new file mode 100644 index 0000000..1912343 --- /dev/null +++ b/gradio/package.json @@ -0,0 +1,6 @@ +{ + "name": "gradio", + "version": "6.20.0", + "description": "This is the package.json", + "python": "true" +} \ No newline at end of file diff --git a/gradio/pipelines.py b/gradio/pipelines.py new file mode 100644 index 0000000..f42a60f --- /dev/null +++ b/gradio/pipelines.py @@ -0,0 +1,120 @@ +"""This module should not be used directly as its API is subject to change. Instead, +please use the `gr.Interface.from_pipeline()` function.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from gradio.pipelines_utils import ( + handle_diffusers_pipeline, + handle_transformers_js_pipeline, + handle_transformers_pipeline, +) + +if TYPE_CHECKING: + import diffusers # ty: ignore[unresolved-import] + import transformers + + +def load_from_pipeline( + pipeline: transformers.Pipeline | diffusers.DiffusionPipeline, # type: ignore +) -> dict: + """ + Gets the appropriate Interface kwargs for a given Hugging Face transformers.Pipeline or diffusers.DiffusionPipeline. + pipeline (transformers.Pipeline): the transformers.Pipeline from which to create an interface + Returns: + (dict): a dictionary of kwargs that can be used to construct an Interface object + """ + + if str(type(pipeline).__module__).startswith("transformers.pipelines."): + pipeline_info = handle_transformers_pipeline(pipeline) + elif str(type(pipeline).__module__).startswith("diffusers.pipelines."): + pipeline_info = handle_diffusers_pipeline(pipeline) + else: + raise ValueError( + "pipeline must be a transformers.pipeline or diffusers.pipeline" + ) + + def fn(*params): + if pipeline_info: + data = pipeline_info["preprocess"](*params) + if str(type(pipeline).__module__).startswith("transformers.pipelines"): + from transformers import pipelines + + # special cases that needs to be handled differently + if isinstance( + pipeline, + ( + pipelines.text_classification.TextClassificationPipeline, # type: ignore + pipelines.text2text_generation.Text2TextGenerationPipeline, # type: ignore + pipelines.text2text_generation.TranslationPipeline, # type: ignore + pipelines.token_classification.TokenClassificationPipeline, # type: ignore + ), + ): + data = pipeline(*data) + else: + data = pipeline(**data) # type: ignore + # special case for object-detection and token-classification pipelines + # original input image / text sent to postprocess function + if isinstance( + pipeline, + ( + pipelines.object_detection.ObjectDetectionPipeline, # type: ignore + pipelines.token_classification.TokenClassificationPipeline, # type: ignore + ), + ): + output = pipeline_info["postprocess"](data, params[0]) + else: + output = pipeline_info["postprocess"](data) + return output + + elif str(type(pipeline).__module__).startswith("diffusers.pipelines"): + data = pipeline(**data) # type: ignore + output = pipeline_info["postprocess"](data) + return output + else: + raise ValueError("pipeline_info can not be None.") + + interface_info = pipeline_info.copy() if pipeline_info else {} + interface_info["fn"] = fn + del interface_info["preprocess"] + del interface_info["postprocess"] + + # define the title/description of the Interface + interface_info["title"] = ( + pipeline.model.config.name_or_path # type: ignore + if str(type(pipeline).__module__).startswith("transformers.pipelines") + else pipeline.__class__.__name__ + ) + + return interface_info + + +def load_from_js_pipeline(pipeline) -> dict: + if str(type(pipeline).__module__).startswith("transformers_js_py."): + pipeline_info = handle_transformers_js_pipeline(pipeline) + else: + raise ValueError("pipeline must be a transformers_js_py's pipeline") + + async def fn(*params): + preprocess = pipeline_info["preprocess"] + postprocess = pipeline_info["postprocess"] + postprocess_takes_inputs = pipeline_info.get("postprocess_takes_inputs", False) + + preprocessed_params = preprocess(*params) if preprocess else params + pipeline_output = await pipeline(*preprocessed_params) + postprocessed_output = ( + postprocess(pipeline_output, *(params if postprocess_takes_inputs else ())) + if postprocess + else pipeline_output + ) + + return postprocessed_output + + interface_info = { + "fn": fn, + "inputs": pipeline_info["inputs"], + "outputs": pipeline_info["outputs"], + "title": f"{pipeline.task} ({pipeline.model.config._name_or_path})", + } + return interface_info diff --git a/gradio/pipelines_utils.py b/gradio/pipelines_utils.py new file mode 100644 index 0000000..048e1fb --- /dev/null +++ b/gradio/pipelines_utils.py @@ -0,0 +1,810 @@ +""" +Defines internal helper methods for handling transformers and diffusers pipelines. +These are used by load_from_pipeline method in pipelines.py. +""" + +from typing import Any + +import numpy as np +from PIL import Image + +from gradio import components + + +def handle_transformers_pipeline(pipeline: Any) -> dict[str, Any] | None: + try: + import transformers + except ImportError as ie: + raise ImportError( + "transformers not installed. Please try `pip install transformers`" + ) from ie + + def is_transformers_pipeline_type(pipeline, class_name: str): + cls = getattr(transformers, class_name, None) + return cls and isinstance(pipeline, cls) + + # Handle the different pipelines. The has_attr() checks to make sure the pipeline exists in the + # version of the transformers library that the user has installed. + if is_transformers_pipeline_type(pipeline, "AudioClassificationPipeline"): + return { + "inputs": components.Audio(type="filepath", label="Input", render=False), + "outputs": components.Label(label="Class", render=False), + "preprocess": lambda i: {"inputs": i}, + "postprocess": lambda r: {i["label"]: i["score"] for i in r}, + } + if is_transformers_pipeline_type(pipeline, "AutomaticSpeechRecognitionPipeline"): + return { + "inputs": components.Audio(type="filepath", label="Input", render=False), + "outputs": components.Textbox(label="Output", render=False), + "preprocess": lambda i: {"inputs": i}, + "postprocess": lambda r: r["text"], + } + if is_transformers_pipeline_type(pipeline, "FeatureExtractionPipeline"): + return { + "inputs": components.Textbox(label="Input", render=False), + "outputs": components.Dataframe(label="Output", render=False), + "preprocess": lambda x: {"inputs": x}, + "postprocess": lambda r: r[0], + } + if is_transformers_pipeline_type(pipeline, "FillMaskPipeline"): + return { + "inputs": components.Textbox(label="Input", render=False), + "outputs": components.Label(label="Classification", render=False), + "preprocess": lambda x: {"inputs": x}, + "postprocess": lambda r: {i["token_str"]: i["score"] for i in r}, + } + if is_transformers_pipeline_type(pipeline, "ImageClassificationPipeline"): + return { + "inputs": components.Image( + type="filepath", label="Input Image", render=False + ), + "outputs": components.Label(label="Classification", render=False), + "preprocess": lambda i: {"images": i}, + "postprocess": lambda r: {i["label"]: i["score"] for i in r}, + } + if is_transformers_pipeline_type(pipeline, "QuestionAnsweringPipeline"): + return { + "inputs": [ + components.Textbox(lines=7, label="Context", render=False), + components.Textbox(label="Question", render=False), + ], + "outputs": [ + components.Textbox(label="Answer", render=False), + components.Label(label="Score", render=False), + ], + "preprocess": lambda c, q: {"context": c, "question": q}, + "postprocess": lambda r: (r["answer"], r["score"]), + } + if is_transformers_pipeline_type(pipeline, "SummarizationPipeline"): + return { + "inputs": components.Textbox(lines=7, label="Input", render=False), + "outputs": components.Textbox(label="Summary", render=False), + "preprocess": lambda x: {"inputs": x}, + "postprocess": lambda r: r[0]["summary_text"], + } + if is_transformers_pipeline_type(pipeline, "TextClassificationPipeline"): + return { + "inputs": components.Textbox(label="Input", render=False), + "outputs": components.Label(label="Classification", render=False), + "preprocess": lambda x: [x], + "postprocess": lambda r: {i["label"]: i["score"] for i in r}, + } + if is_transformers_pipeline_type(pipeline, "TokenClassificationPipeline"): + return { + "inputs": components.Textbox(label="Input", render=False), + "outputs": components.HighlightedText(label="Entities", render=False), + "preprocess": lambda x: [x], + "postprocess": lambda r, text: { + "text": text, + "entities": r, + }, + } + if is_transformers_pipeline_type(pipeline, "TextGenerationPipeline"): + return { + "inputs": components.Textbox(label="Input", render=False), + "outputs": components.Textbox(label="Output", render=False), + "preprocess": lambda x: {"text_inputs": x}, + "postprocess": lambda r: r[0]["generated_text"], + } + if is_transformers_pipeline_type(pipeline, "TranslationPipeline"): + return { + "inputs": components.Textbox(label="Input", render=False), + "outputs": components.Textbox(label="Translation", render=False), + "preprocess": lambda x: [x], + "postprocess": lambda r: r[0]["translation_text"], + } + if is_transformers_pipeline_type(pipeline, "Text2TextGenerationPipeline"): + return { + "inputs": components.Textbox(label="Input", render=False), + "outputs": components.Textbox(label="Generated Text", render=False), + "preprocess": lambda x: [x], + "postprocess": lambda r: r[0]["generated_text"], + } + if is_transformers_pipeline_type(pipeline, "ZeroShotClassificationPipeline"): + return { + "inputs": [ + components.Textbox(label="Input", render=False), + components.Textbox( + label="Possible class names (comma-separated)", render=False + ), + components.Checkbox(label="Allow multiple true classes", render=False), + ], + "outputs": components.Label(label="Classification", render=False), + "preprocess": lambda i, c, m: { + "sequences": i, + "candidate_labels": c, + "multi_label": m, + }, + "postprocess": lambda r: { + r["labels"][i]: r["scores"][i] for i in range(len(r["labels"])) + }, + } + if is_transformers_pipeline_type(pipeline, "DocumentQuestionAnsweringPipeline"): + return { + "inputs": [ + components.Image(type="filepath", label="Input Document", render=False), + components.Textbox(label="Question", render=False), + ], + "outputs": components.Label(label="Label", render=False), + "preprocess": lambda img, q: {"image": img, "question": q}, + "postprocess": lambda r: {i["answer"]: i["score"] for i in r}, + } + if is_transformers_pipeline_type(pipeline, "VisualQuestionAnsweringPipeline"): + return { + "inputs": [ + components.Image(type="filepath", label="Input Image", render=False), + components.Textbox(label="Question", render=False), + ], + "outputs": components.Label(label="Score", render=False), + "preprocess": lambda img, q: {"image": img, "question": q}, + "postprocess": lambda r: {i["answer"]: i["score"] for i in r}, + } + if is_transformers_pipeline_type(pipeline, "ImageToTextPipeline"): + return { + "inputs": components.Image( + type="filepath", label="Input Image", render=False + ), + "outputs": components.Textbox(label="Text", render=False), + "preprocess": lambda i: {"images": i}, + "postprocess": lambda r: r[0]["generated_text"], + } + if is_transformers_pipeline_type(pipeline, "ObjectDetectionPipeline"): + return { + "inputs": components.Image( + type="filepath", label="Input Image", render=False + ), + "outputs": components.AnnotatedImage( + label="Objects Detected", render=False + ), + "preprocess": lambda i: {"inputs": i}, + "postprocess": lambda r, img: ( + img, + [ + ( + ( + i["box"]["xmin"], + i["box"]["ymin"], + i["box"]["xmax"], + i["box"]["ymax"], + ), + i["label"], + ) + for i in r + ], + ), + } + raise ValueError(f"Unsupported transformers pipeline type: {type(pipeline)}") + + +def handle_diffusers_pipeline(pipeline: Any) -> dict[str, Any] | None: + try: + import diffusers # ty: ignore[unresolved-import] + except ImportError as ie: + raise ImportError( + "diffusers not installed. Please try `pip install diffusers`" + ) from ie + + def is_diffusers_pipeline_type(pipeline, class_name: str): + cls = getattr(diffusers, class_name, None) + return cls and isinstance(pipeline, cls) + + if is_diffusers_pipeline_type(pipeline, "StableDiffusionPipeline"): + return { + "inputs": [ + components.Textbox(label="Prompt", render=False), + components.Textbox(label="Negative prompt", render=False), + components.Slider( + label="Number of inference steps", + minimum=1, + maximum=500, + value=50, + step=1, + ), + components.Slider( + label="Guidance scale", + minimum=1, + maximum=20, + value=7.5, + step=0.5, + ), + ], + "outputs": components.Image( + label="Generated Image", render=False, type="pil" + ), + "preprocess": lambda prompt, n_prompt, num_inf_steps, g_scale: { + "prompt": prompt, + "negative_prompt": n_prompt, + "num_inference_steps": num_inf_steps, + "guidance_scale": g_scale, + }, + "postprocess": lambda r: r["images"][0], + } + if is_diffusers_pipeline_type(pipeline, "StableDiffusionImg2ImgPipeline"): + return { + "inputs": [ + components.Textbox(label="Prompt", render=False), + components.Textbox(label="Negative prompt", render=False), + components.Image(type="filepath", label="Image", render=False), + components.Slider( + label="Strength", minimum=0, maximum=1, value=0.8, step=0.1 + ), + components.Slider( + label="Number of inference steps", + minimum=1, + maximum=500, + value=50, + step=1, + ), + components.Slider( + label="Guidance scale", + minimum=1, + maximum=20, + value=7.5, + step=0.5, + ), + ], + "outputs": components.Image( + label="Generated Image", render=False, type="pil" + ), + "preprocess": lambda prompt, + n_prompt, + image, + strength, + num_inf_steps, + g_scale: { + "prompt": prompt, + "image": Image.open(image).resize((768, 768)), + "negative_prompt": n_prompt, + "num_inference_steps": num_inf_steps, + "guidance_scale": g_scale, + "strength": strength, + }, + "postprocess": lambda r: r["images"][0], + } + if is_diffusers_pipeline_type(pipeline, "StableDiffusionInpaintPipeline"): + return { + "inputs": [ + components.Textbox(label="Prompt", render=False), + components.Textbox(label="Negative prompt", render=False), + components.Image(type="filepath", label="Image", render=False), + components.Image(type="filepath", label="Mask Image", render=False), + components.Slider( + label="Strength", minimum=0, maximum=1, value=0.8, step=0.1 + ), + components.Slider( + label="Number of inference steps", + minimum=1, + maximum=500, + value=50, + step=1, + ), + components.Slider( + label="Guidance scale", + minimum=1, + maximum=20, + value=7.5, + step=0.5, + ), + ], + "outputs": components.Image( + label="Generated Image", render=False, type="pil" + ), + "preprocess": lambda prompt, + n_prompt, + image, + mask_image, + strength, + num_inf_steps, + g_scale: { + "prompt": prompt, + "image": Image.open(image).resize((768, 768)), + "mask_image": Image.open(mask_image).resize((768, 768)), + "negative_prompt": n_prompt, + "num_inference_steps": num_inf_steps, + "guidance_scale": g_scale, + "strength": strength, + }, + "postprocess": lambda r: r["images"][0], + } + if is_diffusers_pipeline_type(pipeline, "StableDiffusionDepth2ImgPipeline"): + return { + "inputs": [ + components.Textbox(label="Prompt", render=False), + components.Textbox(label="Negative prompt", render=False), + components.Image(type="filepath", label="Image", render=False), + components.Slider( + label="Strength", minimum=0, maximum=1, value=0.8, step=0.1 + ), + components.Slider( + label="Number of inference steps", + minimum=1, + maximum=500, + value=50, + step=1, + ), + components.Slider( + label="Guidance scale", + minimum=1, + maximum=20, + value=7.5, + step=0.5, + ), + ], + "outputs": components.Image( + label="Generated Image", render=False, type="pil" + ), + "preprocess": lambda prompt, + n_prompt, + image, + strength, + num_inf_steps, + g_scale: { + "prompt": prompt, + "image": Image.open(image).resize((768, 768)), + "negative_prompt": n_prompt, + "num_inference_steps": num_inf_steps, + "guidance_scale": g_scale, + "strength": strength, + }, + "postprocess": lambda r: r["images"][0], + } + if is_diffusers_pipeline_type(pipeline, "StableDiffusionImageVariationPipeline"): + return { + "inputs": [ + components.Image(type="filepath", label="Image", render=False), + components.Slider( + label="Number of inference steps", + minimum=1, + maximum=500, + value=50, + step=1, + ), + components.Slider( + label="Guidance scale", + minimum=1, + maximum=20, + value=7.5, + step=0.5, + ), + ], + "outputs": components.Image( + label="Generated Image", render=False, type="pil" + ), + "preprocess": lambda image, num_inf_steps, g_scale: { + "image": Image.open(image).resize((768, 768)), + "num_inference_steps": num_inf_steps, + "guidance_scale": g_scale, + }, + "postprocess": lambda r: r["images"][0], + } + if is_diffusers_pipeline_type(pipeline, "StableDiffusionInstructPix2PixPipeline"): + return { + "inputs": [ + components.Textbox(label="Prompt", render=False), + components.Textbox(label="Negative prompt", render=False), + components.Image(type="filepath", label="Image", render=False), + components.Slider( + label="Number of inference steps", + minimum=1, + maximum=500, + value=50, + step=1, + ), + components.Slider( + label="Guidance scale", + minimum=1, + maximum=20, + value=7.5, + step=0.5, + ), + components.Slider( + label="Image Guidance scale", + minimum=1, + maximum=5, + value=1.5, + step=0.5, + ), + ], + "outputs": components.Image( + label="Generated Image", render=False, type="pil" + ), + "preprocess": lambda prompt, + n_prompt, + image, + num_inf_steps, + g_scale, + img_g_scale: { + "prompt": prompt, + "image": Image.open(image).resize((768, 768)), + "negative_prompt": n_prompt, + "num_inference_steps": num_inf_steps, + "guidance_scale": g_scale, + "image_guidance_scale": img_g_scale, + }, + "postprocess": lambda r: r["images"][0], + } + if is_diffusers_pipeline_type(pipeline, "StableDiffusionUpscalePipeline"): + return { + "inputs": [ + components.Textbox(label="Prompt", render=False), + components.Textbox(label="Negative prompt", render=False), + components.Image(type="filepath", label="Image", render=False), + components.Slider( + label="Number of inference steps", + minimum=1, + maximum=500, + value=50, + step=1, + ), + components.Slider( + label="Guidance scale", + minimum=1, + maximum=20, + value=7.5, + step=0.5, + ), + components.Slider( + label="Noise level", minimum=1, maximum=100, value=20, step=1 + ), + ], + "outputs": components.Image( + label="Generated Image", render=False, type="pil" + ), + "preprocess": lambda prompt, + n_prompt, + image, + num_inf_steps, + g_scale, + noise_level: { + "prompt": prompt, + "image": Image.open(image).resize((768, 768)), + "negative_prompt": n_prompt, + "num_inference_steps": num_inf_steps, + "guidance_scale": g_scale, + "noise_level": noise_level, + }, + "postprocess": lambda r: r["images"][0], + } + raise ValueError(f"Unsupported diffusers pipeline type: {type(pipeline)}") + + +def handle_transformers_js_pipeline(pipeline: Any) -> dict[str, Any]: + try: + from transformers_js_py import as_url, read_audio # type: ignore + except ImportError as ie: + raise ImportError( + "transformers_js_py not installed. Please add `transformers_js_py` to the requirements of your Gradio-Lite app" + ) from ie + + ## Natural Language Processing ## + if pipeline.task == "fill-mask": + return { + "inputs": components.Textbox(label="Input"), + "outputs": components.Label(label="Classification"), + "preprocess": None, + "postprocess": lambda r: {i["token_str"]: i["score"] for i in r}, + } + if pipeline.task == "question-answering": + return { + "inputs": [ + components.Textbox(lines=7, label="Context"), + components.Textbox(label="Question"), + ], + "outputs": [ + components.Textbox(label="Answer"), + components.Label(label="Score"), + ], + "preprocess": lambda c, q: ( + q, + c, + ), # Placed the context first in the input UI to match `handle_transformers_pipeline`'s order of inputs, but Transformers.js' question-answering pipeline expects the question first. + "postprocess": lambda r: (r["answer"], r["score"]), + } + if pipeline.task == "summarization": + return { + "inputs": [ + components.Textbox(lines=7, label="Input"), + components.Slider( + label="The maximum numbers of tokens to generate", + minimum=1, + maximum=500, + value=100, + step=1, + ), + ], + "outputs": components.Textbox(label="Summary"), + "preprocess": lambda text, max_new_tokens: ( + text, + {"max_new_tokens": max_new_tokens}, + ), + "postprocess": lambda r: r[0]["summary_text"], + } + if pipeline.task == "text-classification": + return { + "inputs": [ + components.Textbox(label="Input"), + components.Number(label="Top k", value=5), + ], + "outputs": components.Label(label="Classification"), + "preprocess": lambda text, topk: (text, {"topk": topk}), + "postprocess": lambda r: {i["label"]: i["score"] for i in r}, + } + if pipeline.task == "text-generation": + return { + "inputs": components.Textbox(label="Input"), + "outputs": components.Textbox(label="Output"), + "preprocess": None, + "postprocess": lambda r: r[0]["generated_text"], + } + if pipeline.task == "text2text-generation": + return { + "inputs": [ + components.Textbox(label="Input"), + components.Slider( + label="The maximum numbers of tokens to generate", + minimum=1, + maximum=500, + value=100, + step=1, + ), + ], + "outputs": components.Textbox(label="Generated Text"), + "preprocess": lambda text, max_new_tokens: ( + text, + {"max_new_tokens": max_new_tokens}, + ), + "postprocess": lambda r: r[0]["generated_text"], + } + if pipeline.task == "token-classification": + return { + "inputs": components.Textbox(label="Input"), + "outputs": components.JSON(label="Output"), + "preprocess": None, + "postprocess": None, + "postprocess_takes_inputs": True, + } + if pipeline.task in {"translation", "translation_xx_to_yy"}: + return { + "inputs": [ + components.Textbox(label="Input"), + components.Textbox(label="Source Language"), + components.Textbox(label="Target Language"), + ], + "outputs": components.Textbox(label="Translation"), + "preprocess": lambda x, s, t: (x, {"src_lang": s, "tgt_lang": t}), + "postprocess": lambda r: r[0]["translation_text"], + } + if pipeline.task == "zero-shot-classification": + return { + "inputs": [ + components.Textbox(label="Input"), + components.Textbox(label="Possible class names (comma-separated)"), + ], + "outputs": components.Label(label="Classification"), + "preprocess": lambda text, classnames: ( + text, + [c.strip() for c in classnames.split(",")], + ), + "postprocess": lambda result: dict( + zip(result["labels"], result["scores"], strict=False) + ), + } + if pipeline.task == "feature-extraction": + return { + "inputs": components.Textbox(label="Input"), + "outputs": components.Dataframe(label="Output"), + "preprocess": None, + "postprocess": lambda tensor: tensor.to_numpy()[0], + } + + ## Vision ## + if pipeline.task == "depth-estimation": + return { + "inputs": components.Image(type="filepath", label="Input Image"), + "outputs": components.Image(label="Depth"), + "preprocess": lambda image_path: (as_url(image_path),), + "postprocess": lambda result: result["depth"].to_pil(), + } + if pipeline.task == "image-classification": + return { + "inputs": [ + components.Image(type="filepath", label="Input Image"), + components.Number(label="Top k", value=5), + ], + "outputs": components.Label(label="Classification"), + "preprocess": lambda image_path, topk: (as_url(image_path), {"topk": topk}), + "postprocess": lambda result: { + item["label"]: item["score"] for item in result + }, + } + if pipeline.task == "image-segmentation": + return { + "inputs": components.Image(type="filepath", label="Input Image"), + "outputs": components.AnnotatedImage(label="Segmentation"), + "preprocess": lambda image_path: (as_url(image_path),), + "postprocess": lambda result, image_path: ( + image_path, + [ + ( + item["mask"].to_numpy()[:, :, 0] + / 255.0, # Reshape ([h,w,1] -> [h,w]) and normalize ([0,255] -> [0,1]) + f"{item['label']} ({item['score']})", + ) + for item in result + ], + ), + "postprocess_takes_inputs": True, + } + if pipeline.task == "image-to-image": + return { + "inputs": components.Image(type="filepath", label="Input Image"), + "outputs": components.Image(label="Output Image"), + "preprocess": lambda image_path: (as_url(image_path),), + "postprocess": lambda result: result.to_pil(), + } + if pipeline.task == "object-detection": + return { + "inputs": components.Image(type="filepath", label="Input Image"), + "outputs": components.AnnotatedImage(label="Objects Detected"), + "preprocess": lambda image_path: (as_url(image_path),), + "postprocess": lambda result, image_path: ( + image_path, + [ + ( + ( + int(item["box"]["xmin"]), + int(item["box"]["ymin"]), + int(item["box"]["xmax"]), + int(item["box"]["ymax"]), + ), + f"{item['label']} ({item['score']})", + ) + for item in result + ], + ), + "postprocess_takes_inputs": True, + } + if pipeline.task == "image-feature-extraction": + return { + "inputs": components.Image(type="filepath", label="Input Image"), + "outputs": components.Dataframe(label="Output"), + "preprocess": lambda image_path: (as_url(image_path),), + "postprocess": lambda tensor: tensor.to_numpy(), + } + + ## Audio ## + if pipeline.task == "audio-classification": + return { + "inputs": components.Audio(type="filepath", label="Input"), + "outputs": components.Label(label="Class"), + "preprocess": lambda i: ( + read_audio( + i, pipeline.processor.feature_extractor.config["sampling_rate"] + ), + ), + "postprocess": lambda r: {i["label"]: i["score"] for i in r}, + } + if pipeline.task == "automatic-speech-recognition": + return { + "inputs": components.Audio(type="filepath", label="Input"), + "outputs": components.Textbox(label="Output"), + "preprocess": lambda i: ( + read_audio( + i, pipeline.processor.feature_extractor.config["sampling_rate"] + ), + ), + "postprocess": lambda r: r["text"], + } + if pipeline.task == "text-to-audio": + return { + "inputs": [ + components.Textbox(label="Input"), + components.Textbox(label="Speaker Embeddings"), + ], + "outputs": components.Audio(label="Output"), + "preprocess": lambda text, speaker_embeddings: ( + text, + {"speaker_embeddings": speaker_embeddings}, + ), + "postprocess": lambda r: (r["sampling_rate"], np.asarray(r["audio"])), + } + + ## Multimodal ## + if pipeline.task == "document-question-answering": + return { + "inputs": [ + components.Image(type="filepath", label="Input Document"), + components.Textbox(label="Question"), + ], + "outputs": components.Textbox(label="Label"), + "preprocess": lambda img, q: (as_url(img), q), + "postprocess": lambda r: r[0][ + "answer" + ], # This data structure is different from the original Transformers. + } + if pipeline.task == "image-to-text": + return { + "inputs": components.Image(type="filepath", label="Input Image"), + "outputs": components.Textbox(label="Output"), + "preprocess": lambda image_path: (as_url(image_path),), + "postprocess": lambda r: r[0]["generated_text"], + } + if pipeline.task == "zero-shot-audio-classification": + return { + "inputs": [ + components.Audio(type="filepath", label="Input"), + components.Textbox(label="Possible class names (comma-separated)"), + ], + "outputs": components.Label(label="Classification"), + "preprocess": lambda audio_path, classnames: ( + read_audio( + audio_path, + pipeline.processor.feature_extractor.config["sampling_rate"], + ), + [c.strip() for c in classnames.split(",")], + ), + "postprocess": lambda result: {i["label"]: i["score"] for i in result}, + } + if pipeline.task == "zero-shot-image-classification": + return { + "inputs": [ + components.Image(type="filepath", label="Input Image"), + components.Textbox(label="Possible class names (comma-separated)"), + ], + "outputs": components.Label(label="Classification"), + "preprocess": lambda image_path, classnames: ( + as_url(image_path), + [c.strip() for c in classnames.split(",")], + ), + "postprocess": lambda result: {i["label"]: i["score"] for i in result}, + } + if pipeline.task == "zero-shot-object-detection": + return { + "inputs": [ + components.Image(type="filepath", label="Input Image"), + components.Textbox(label="Possible class names (comma-separated)"), + ], + "outputs": components.AnnotatedImage(label="Objects Detected"), + "preprocess": lambda image_path, classnames: ( + as_url(image_path), + [c.strip() for c in classnames.split(",")], + ), + "postprocess": lambda result, image_path, _: ( + image_path, + [ + ( + ( + int(item["box"]["xmin"]), + int(item["box"]["ymin"]), + int(item["box"]["xmax"]), + int(item["box"]["ymax"]), + ), + f"{item['label']} ({item['score']})", + ) + for item in result + ], + ), + "postprocess_takes_inputs": True, + } + + raise ValueError(f"Unsupported transformers_js_py pipeline type: {pipeline.task}") diff --git a/gradio/processing_utils.py b/gradio/processing_utils.py new file mode 100644 index 0000000..86e5ada --- /dev/null +++ b/gradio/processing_utils.py @@ -0,0 +1,1144 @@ +from __future__ import annotations + +import asyncio +import base64 +import hashlib +import ipaddress +import json +import logging +import mimetypes +import os +import shutil +import subprocess +import tempfile +import warnings +from collections.abc import Awaitable, Callable, Coroutine +from functools import lru_cache, wraps +from io import BytesIO +from pathlib import Path +from typing import TYPE_CHECKING, Any, TypeVar +from urllib.parse import urljoin, urlparse + +import httpx +import numpy as np +import safehttpx as sh +from gradio_client import utils as client_utils +from PIL import Image, ImageOps, ImageSequence, PngImagePlugin + +from gradio import utils +from gradio._vendor import aiofiles +from gradio.context import LocalContext +from gradio.data_classes import FileData, GradioModel, GradioRootModel, JsonData +from gradio.exceptions import Error, InvalidPathError +from gradio.profiling import traced_sync +from gradio.route_utils import API_PREFIX +from gradio.utils import abspath, get_hash_seed, get_upload_folder, is_in_or_equal + +with warnings.catch_warnings(): + warnings.simplefilter("ignore") # Ignore pydub warning if ffmpeg is not installed + from pydub import AudioSegment + +sync_transport = None +async_transport = None + +sync_client = httpx.Client(transport=sync_transport) + +log = logging.getLogger(__name__) + +if TYPE_CHECKING: + from gradio.blocks import Block + + +######################### +# GENERAL +######################### + + +def to_binary(x: str | dict) -> bytes: + """Converts a base64 string or dictionary to a binary string that can be sent in a POST.""" + if isinstance(x, dict): + if x.get("data"): + base64str = x["data"] + else: + base64str = client_utils.encode_url_or_file_to_base64(x["path"]) + else: + base64str = x + return base64.b64decode(extract_base64_data(base64str)) # type: ignore + + +def extract_base64_data(x: str) -> str: + """Just extracts the base64 data from a general base64 string.""" + return x.rsplit(",", 1)[-1] + + +######################### +# IMAGE PRE-PROCESSING +######################### + + +def encode_plot_to_base64(plt, format: str = "png"): + fmt = format or "png" + with BytesIO() as output_bytes: + plt.savefig(output_bytes, format=fmt) + bytes_data = output_bytes.getvalue() + base64_str = str(base64.b64encode(bytes_data), "utf-8") + return f"data:image/{format or 'png'};base64,{base64_str}" + + +def get_pil_exif_bytes(pil_image): + if "exif" in pil_image.info: + return pil_image.info["exif"] + + +def get_pil_metadata(pil_image): + # Copy any text-only metadata + metadata = PngImagePlugin.PngInfo() + for key, value in pil_image.info.items(): + if isinstance(key, str) and isinstance(value, str): + metadata.add_text(key, value) + + return metadata + + +def encode_pil_to_bytes(pil_image, format="png"): + with BytesIO() as output_bytes: + if format.lower() == "gif": + frames = [frame.copy() for frame in ImageSequence.Iterator(pil_image)] + frames[0].save( + output_bytes, + format=format, + save_all=True, + append_images=frames[1:], + loop=0, + ) + else: + if format.lower() == "png": + params = {"pnginfo": get_pil_metadata(pil_image)} + else: + exif = get_pil_exif_bytes(pil_image) + params = {"exif": exif} if exif else {} + pil_image.save(output_bytes, format, **params) + return output_bytes.getvalue() + + +hash_seed = get_hash_seed().encode("utf-8") + + +def hash_file(file_path: str | Path, chunk_num_blocks: int = 128) -> str: + sha = hashlib.sha256() + sha.update(hash_seed) + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(chunk_num_blocks * sha.block_size), b""): + sha.update(chunk) + return sha.hexdigest() + + +def hash_url(url: str) -> str: + sha = hashlib.sha256() + sha.update(hash_seed) + sha.update(url.encode("utf-8")) + return sha.hexdigest() + + +def hash_bytes(bytes: bytes): + sha = hashlib.sha256() + sha.update(hash_seed) + sha.update(bytes) + return sha.hexdigest() + + +def hash_base64(base64_encoding: str, chunk_num_blocks: int = 128) -> str: + sha = hashlib.sha256() + sha.update(hash_seed) + for i in range(0, len(base64_encoding), chunk_num_blocks * sha.block_size): + data = base64_encoding[i : i + chunk_num_blocks * sha.block_size] + sha.update(data.encode("utf-8")) + return sha.hexdigest() + + +@traced_sync("postprocess_save_pil_to_cache") +def save_pil_to_cache( + img: Image.Image, + cache_dir: str, + name: str = "image", + format: str = "webp", +) -> str: + bytes_data = encode_pil_to_bytes(img, format) + temp_dir = Path(cache_dir) / hash_bytes(bytes_data) + temp_dir.mkdir(exist_ok=True, parents=True) + filename = str((temp_dir / f"{name}.{format}").resolve()) + (temp_dir / f"{name}.{format}").resolve().write_bytes(bytes_data) + return filename + + +@traced_sync("postprocess_save_img_array_to_cache") +def save_img_array_to_cache( + arr: np.ndarray, cache_dir: str, format: str = "webp" +) -> str: + pil_image = Image.fromarray(_convert(arr, np.uint8, force_copy=False)) + return save_pil_to_cache(pil_image, cache_dir, format=format) + + +@traced_sync("postprocess_save_audio_to_cache") +def save_audio_to_cache( + data: np.ndarray, sample_rate: int, format: str, cache_dir: str +) -> str: + audio_metadata = { + "cache_schema": "audio-cache-v1", + "dtype": str(data.dtype), + "format": format, + "sample_rate": int(sample_rate), + "shape": data.shape, + } + audio_hash = hashlib.sha256() + audio_hash.update(hash_seed) + audio_hash.update(json.dumps(audio_metadata, sort_keys=True).encode("utf-8")) + audio_hash.update(data.tobytes()) + temp_dir = Path(cache_dir) / audio_hash.hexdigest() + temp_dir.mkdir(exist_ok=True, parents=True) + filename = str((temp_dir / f"audio.{format}").resolve()) + audio_to_file(sample_rate, data, filename, format=format) + return filename + + +def detect_audio_format(data: bytes) -> str: + """Detect audio format from file header bytes. + + Args: + data: File content as bytes + + Returns: + Detected file extension with dot (e.g., ".wav", ".mp3") or empty string if not detected + """ + # Check WAV format (RIFF header) + if len(data) >= 12 and data[:4] == b"RIFF" and data[8:12] == b"WAVE": + return ".wav" + # Check MP3 format (ID3 tag) + elif len(data) >= 3 and data[:3] == b"ID3": # noqa: SIM114 + return ".mp3" + # Check MP3 format (sync frame) + elif len(data) >= 2 and data[:2] == b"\xff\xfb": + return ".mp3" + return "" + + +@traced_sync("postprocess_save_bytes_to_cache") +def save_bytes_to_cache(data: bytes, file_name: str, cache_dir: str) -> str: + path = Path(cache_dir) / hash_bytes(data) + path.mkdir(exist_ok=True, parents=True) + if not Path(file_name).suffix: + detected_extension = detect_audio_format(data) + file_name = file_name + detected_extension + path = path / Path(file_name).name + path.write_bytes(data) + return str(path.resolve()) + + +@traced_sync("save_file_to_cache") +def save_file_to_cache(file_path: str | Path, cache_dir: str) -> str: + """Returns a temporary file path for a copy of the given file path if it does + not already exist. Otherwise returns the path to the existing temp file.""" + temp_dir = hash_file(file_path) + temp_dir = Path(cache_dir) / temp_dir + temp_dir.mkdir(exist_ok=True, parents=True) + + name = client_utils.strip_invalid_filename_characters(Path(file_path).name) + full_temp_file_path = str(abspath(temp_dir / name)) + + if not Path(full_temp_file_path).exists(): + shutil.copy2(file_path, full_temp_file_path) + + return full_temp_file_path + + +# Always return these URLs as is, without checking to see if they resolve +# to an internal IP address. This is because Hugging Face uses DNS splitting, +# which means that requests from HF Spaces to HF Datasets or HF Models +# may resolve to internal IP addresses even if they are publicly accessible. +PUBLIC_HOSTNAME_WHITELIST = [ + "hf.co", + "huggingface.co", + "*.hf.co", + "*.huggingface.co", +] + + +def is_public_ip(ip: str) -> bool: + try: + ip_obj = ipaddress.ip_address(ip) + return not ( + ip_obj.is_private + or ip_obj.is_loopback + or ip_obj.is_link_local + or ip_obj.is_multicast + or ip_obj.is_reserved + ) + except ValueError: + return False + + +T = TypeVar("T") + + +def lru_cache_async(maxsize: int = 128): + def decorator( + async_func: Callable[..., Coroutine[Any, Any, T]], + ) -> Callable[..., Awaitable[T]]: + @lru_cache(maxsize=maxsize) + @wraps(async_func) + def wrapper(*args: Any, **kwargs: Any) -> Awaitable[T]: + return asyncio.create_task(async_func(*args, **kwargs)) + + return wrapper + + return decorator + + +MAX_REDIRECTS = 20 + + +async def async_ssrf_protected_get(url: str) -> httpx.Response: + """SSRF-protected GET: routes through `safehttpx` with the public hostname + allow-list and re-validates each redirect. Returns the `httpx.Response` + without raising on non-2xx status (callers decide how to handle that).""" + response = await sh.get( + url, domain_whitelist=PUBLIC_HOSTNAME_WHITELIST, _transport=async_transport + ) + redirects = 0 + while response.has_redirect_location: + if redirects >= MAX_REDIRECTS: + raise Exception(f"Exceeded maximum of {MAX_REDIRECTS} redirects.") + redirects += 1 + # Resolve the Location against the URL that produced this redirect, so + # relative/scheme-relative redirects and host changes across hops are + # handled per RFC 3986. safehttpx re-validates the resolved host. + url = urljoin(str(response.url), response.headers["Location"]) + response = await sh.get( + url, domain_whitelist=PUBLIC_HOSTNAME_WHITELIST, _transport=async_transport + ) + return response + + +async def async_ssrf_protected_download(url: str, cache_dir: str) -> str: + temp_dir = Path(cache_dir) / hash_url(url) + temp_dir.mkdir(exist_ok=True, parents=True) + + parsed_url = urlparse(url) + base_path = parsed_url.path.rstrip("/") + filename = ( + client_utils.strip_invalid_filename_characters(Path(base_path).name) or "file" + ) + + full_temp_file_path = str(abspath(temp_dir / filename)) + if Path(full_temp_file_path).exists(): + return full_temp_file_path + + response = await async_ssrf_protected_get(url) + if response.status_code != 200: + raise Exception(f"Failed to download file. Status code: {response.status_code}") + + async with aiofiles.open(full_temp_file_path, "wb") as f: + async for chunk in response.aiter_bytes(): + await f.write(chunk) + + return full_temp_file_path + + +def unsafe_download(url: str, cache_dir: str) -> str: + temp_dir = Path(cache_dir) / hash_url(url) + temp_dir.mkdir(exist_ok=True, parents=True) + filename = client_utils.strip_invalid_filename_characters(Path(url).name) + full_temp_file_path = str(abspath(temp_dir / filename)) + + with ( + sync_client.stream("GET", url, follow_redirects=True) as r, + open(full_temp_file_path, "wb") as f, + ): + for chunk in r.iter_raw(): + f.write(chunk) + + # print path and file size + print( + f"Downloaded {full_temp_file_path} ({os.path.getsize(full_temp_file_path)} bytes)" + ) + log.info( + f"Downloaded {full_temp_file_path} ({os.path.getsize(full_temp_file_path)} bytes)" + ) + + return full_temp_file_path + + +def ssrf_protected_download(url: str, cache_dir: str) -> str: + return client_utils.synchronize_async(async_ssrf_protected_download, url, cache_dir) + + +# Custom components created with versions of gradio < 5.0 may be using the processing_utils.save_url_to_cache method, so we alias to ssrf_protected_download to preserve backwards-compatibility +save_url_to_cache = ssrf_protected_download + + +def save_base64_to_cache( + base64_encoding: str, cache_dir: str, file_name: str | None = None +) -> str: + """Converts a base64 encoding to a file and returns the path to the file if + the file doesn't already exist. Otherwise returns the path to the existing file. + """ + temp_dir = hash_base64(base64_encoding) + temp_dir = Path(cache_dir) / temp_dir + temp_dir.mkdir(exist_ok=True, parents=True) + + guess_extension = client_utils.get_extension(base64_encoding) + if file_name: + file_name = client_utils.strip_invalid_filename_characters(file_name) + elif guess_extension: + file_name = f"file.{guess_extension}" + else: + file_name = "file" + + full_temp_file_path = str(abspath(temp_dir / file_name)) # type: ignore + + if not Path(full_temp_file_path).exists(): + data, _ = client_utils.decode_base64_to_binary(base64_encoding) + with open(full_temp_file_path, "wb") as fb: + fb.write(data) + + return full_temp_file_path + + +def move_resource_to_block_cache( + url_or_file_path: str | Path | None, block: Block +) -> str | None: + """This method has been replaced by Block.move_resource_to_block_cache(), but is + left here for backwards compatibility for any custom components created in Gradio 4.2.0 or earlier. + """ + return block.move_resource_to_block_cache(url_or_file_path) + + +def check_all_files_in_cache(data: JsonData): + def _in_cache(d: dict): + if ( + (path := d.get("path", "")) + and not client_utils.is_http_url_like(path) + and not is_in_or_equal(path, get_upload_folder()) + and not utils.is_static_file(path) + ): + raise Error( + f"File {path} is not in the cache folder and cannot be accessed." + ) + + client_utils.traverse(data, _in_cache, client_utils.is_file_obj) + + +def move_files_to_cache( + data: Any, + block: Block, + postprocess: bool = False, + check_in_upload_folder=False, + keep_in_cache=False, +): + """Move any files in `data` to cache and (optionally), adds URL prefixes (/file=...) needed to access the cached file. + Also handles the case where the file is on an external Gradio app (/proxy=...). + + Runs after .postprocess() and before .preprocess(). + + Args: + data: The input or output data for a component. Can be a dictionary or a dataclass + block: The component whose data is being processed + postprocess: Whether its running from postprocessing + check_in_upload_folder: If True, instead of moving the file to cache, checks if the file is in already in cache (exception if not). + keep_in_cache: If True, the file will not be deleted from cache when the server is shut down. + """ + + def _mark_svg_as_safe(payload: FileData): + # If the app has not launched, this path can be considered an "allowed path" + # This is mainly so that svg files can be displayed inline for button/chatbot icons + if ( + (blocks := LocalContext.blocks.get(None)) is None or not blocks.is_running + ) and (mimetypes.guess_type(payload.path)[0] == "image/svg+xml"): + utils.set_static_paths([payload.path]) + + def _move_to_cache(d: dict): + payload = FileData(**d) # type: ignore + # If the gradio app developer is returning a URL from + # postprocess, it means the component can display a URL + # without it being served from the gradio server + # This makes it so that the URL is not downloaded and speeds up event processing + if payload.url and postprocess and client_utils.is_http_url_like(payload.url): + payload.path = payload.url + elif utils.is_static_file(payload): + pass + elif not block.proxy_url: + # If the file is on a remote server, do not move it to cache. + if not client_utils.is_http_url_like(payload.path): + _check_allowed(payload.path, check_in_upload_folder) + if not payload.is_stream: + temp_file_path = block.move_resource_to_block_cache(payload.path) + if temp_file_path is None: + raise ValueError("Did not determine a file path for the resource.") + payload.path = temp_file_path + if keep_in_cache: + block.keep_in_cache.add(payload.path) + + url_prefix = ( + f"{API_PREFIX}/stream/" if payload.is_stream else f"{API_PREFIX}/file=" + ) + if block.proxy_url: + proxy_url = block.proxy_url.rstrip("/") + url = f"{API_PREFIX}/proxy={proxy_url}{url_prefix}{payload.path}" + elif client_utils.is_http_url_like(payload.path) or payload.path.startswith( + f"{url_prefix}" + ): + url = f"{payload.path}" + else: + url = f"{url_prefix}{payload.path}" + payload.url = url + _mark_svg_as_safe(payload) + return payload.model_dump() + + if isinstance(data, (GradioRootModel, GradioModel)): + data = data.model_dump() + + return client_utils.traverse( + data, _move_to_cache, client_utils.is_file_obj_with_meta + ) + + +def _check_allowed(path: str | Path, check_in_upload_folder: bool): + blocks = LocalContext.blocks.get(None) + if blocks is None or not blocks.has_launched: + return + + abs_path = utils.abspath(path) + + created_paths = [utils.get_upload_folder()] + # if check_in_upload_folder=True, we are running this during pre-process + # in which case only files in the upload_folder (cache_dir) are accepted + if check_in_upload_folder: + allowed_paths = [] + else: + allowed_paths = blocks.allowed_paths + [os.getcwd(), tempfile.gettempdir()] + allowed, reason = utils.is_allowed_file( + abs_path, + blocked_paths=blocks.blocked_paths, + allowed_paths=allowed_paths, + created_paths=created_paths, + ) + if not allowed: + msg = f"Cannot move {abs_path} to the gradio cache dir because " + if reason == "in_blocklist": + msg += f"it is located in one of the blocked_paths ({', '.join(blocks.blocked_paths)})." + elif check_in_upload_folder: + msg += "it was not uploaded by a user." + else: + msg += "it was not created by the application or it is not " + msg += "located in either the current working directory or your system's temp directory. " + msg += "To fix this error, please ensure your function returns files located in either " + msg += f"the current working directory ({os.getcwd()}), your system's temp directory ({tempfile.gettempdir()}) " + msg += f"or add {str(abs_path.parent)} to the allowed_paths parameter of launch()." + raise InvalidPathError(msg) + if ( + utils.is_in_or_equal(abs_path, os.getcwd()) + and abs_path.name.startswith(".") + and not any( + is_in_or_equal(path, allowed_path) for allowed_path in blocks.allowed_paths + ) + ): + raise InvalidPathError( + "Dotfiles located in the temporary directory cannot be moved to the cache for security reasons. " + "If you'd like to specifically allow this file to be served, you can add it to the allowed_paths parameter of launch()." + ) + + +async def async_move_files_to_cache( + data: Any, + block: Block, + postprocess: bool = False, + check_in_upload_folder=False, + keep_in_cache=False, +) -> dict: + """Move any files in `data` to cache and (optionally), adds URL prefixes (/file=...) needed to access the cached file. + Also handles the case where the file is on an external Gradio app (/proxy=...). + + Runs after .postprocess() and before .preprocess(). + + Args: + data: The input or output data for a component. Can be a dictionary or a dataclass + block: The component whose data is being processed + postprocess: Whether its running from postprocessing + check_in_upload_folder: If True, instead of moving the file to cache, checks if the file is in already in cache (exception if not). + keep_in_cache: If True, the file will not be deleted from cache when the server is shut down. + """ + + def _mark_svg_as_safe(payload: FileData): + # If the app has not launched, this path can be considered an "allowed path" + # This is mainly so that svg files can be displayed inline for button/chatbot icons + if ( + (blocks := LocalContext.blocks.get(None)) is None or not blocks.is_running + ) and (mimetypes.guess_type(payload.path)[0] == "image/svg+xml"): + utils.set_static_paths([payload.path]) + + async def _move_to_cache(d: dict): + payload = FileData(**d) # type: ignore + # If the gradio app developer is returning a URL from + # postprocess, it means the component can display a URL + # without it being served from the gradio server + # This makes it so that the URL is not downloaded and speeds up event processing + if payload.url and postprocess and client_utils.is_http_url_like(payload.url): + payload.path = payload.url + elif utils.is_static_file(payload): + pass + elif not block.proxy_url: + # If the file is on a remote server, do not move it to cache. + if not client_utils.is_http_url_like(payload.path): + _check_allowed(payload.path, check_in_upload_folder) + if not payload.is_stream: + temp_file_path = await block.async_move_resource_to_block_cache( + payload.path + ) + if temp_file_path is None: + raise ValueError("Did not determine a file path for the resource.") + payload.path = temp_file_path + if keep_in_cache: + block.keep_in_cache.add(payload.path) + + url_prefix = ( + f"{API_PREFIX}/stream/" if payload.is_stream else f"{API_PREFIX}/file=" + ) + if block.proxy_url: + proxy_url = block.proxy_url.rstrip("/") + url = f"{API_PREFIX}/proxy={proxy_url}{url_prefix}{payload.path}" + elif client_utils.is_http_url_like(payload.path) or payload.path.startswith( + f"{url_prefix}" + ): + url = payload.path + else: + url = f"{url_prefix}{payload.path}" + payload.url = url + _mark_svg_as_safe(payload) + return payload.model_dump() + + if isinstance(data, (GradioRootModel, GradioModel)): + data = data.model_dump() + return await client_utils.async_traverse( + data, _move_to_cache, client_utils.is_file_obj_with_meta + ) + + +def add_root_url(data: dict | list, root_url: str, previous_root_url: str | None): + def _add_root_url(file_dict: dict): + if previous_root_url and file_dict["url"].startswith(previous_root_url): + file_dict["url"] = file_dict["url"][len(previous_root_url) :] + elif client_utils.is_http_url_like(file_dict["url"]): + return file_dict + file_dict["url"] = f"{root_url}{file_dict['url']}" + return file_dict + + return client_utils.traverse(data, _add_root_url, client_utils.is_file_obj_with_url) + + +def resize_and_crop(img, size, crop_type="center"): + """ + Resize and crop an image to fit the specified size. + args: + size: `(width, height)` tuple. Pass `None` for either width or height + to only crop and resize the other. + crop_type: can be 'top', 'middle' or 'bottom', depending on this + value, the image will cropped getting the 'top/left', 'middle' or + 'bottom/right' of the image to fit the size. + raises: + ValueError: if an invalid `crop_type` is provided. + """ + if crop_type == "top": + center = (0, 0) + elif crop_type == "center": + center = (0.5, 0.5) + else: + raise ValueError + + resize = list(size) + if size[0] is None: + resize[0] = img.size[0] + if size[1] is None: + resize[1] = img.size[1] + return ImageOps.fit(img, resize, centering=center) # type: ignore + + +################## +# Audio +################## + + +def audio_from_file( + filename: str, crop_min: float = 0, crop_max: float = 100 +) -> tuple[int, np.ndarray]: + from gradio.profiling import trace_phase_sync + + with trace_phase_sync("preprocess_audio_from_file"): + try: + audio = AudioSegment.from_file(filename) + except FileNotFoundError as e: + isfile = Path(filename).is_file() + msg = ( + f"Cannot load audio from file: `{'ffprobe' if isfile else filename}` not found." + + " Please install `ffmpeg` in your system to use non-WAV audio file formats" + " and make sure `ffprobe` is in your PATH." + if isfile + else "" + ) + raise RuntimeError(msg) from e + except OSError as e: + raise e + if crop_min != 0 or crop_max != 100: + audio_start = len(audio) * crop_min / 100 + audio_end = len(audio) * crop_max / 100 + audio = audio[audio_start:audio_end] + data = np.array(audio.get_array_of_samples()) + if audio.channels > 1: + data = data.reshape(-1, audio.channels) + return audio.frame_rate, data + + +def audio_to_file(sample_rate, data, filename, format="wav"): + # pydub's `AudioSegment` raw constructor only supports integer PCM, and + # interprets `sample_width=4` as int32 rather than float32. Without an + # explicit conversion, non-WAV formats (mp3, flac, ogg, ...) end up + # feeding float32 bytes through ffmpeg as `pcm_s32le`, which decodes as + # noise. Run the same int16 conversion for every format so the encoded + # output matches the input waveform regardless of `format`. See #13364. + if data.dtype != np.int16: + data = convert_to_16_bit_audio(data) + + audio = AudioSegment( + data.tobytes(), + frame_rate=sample_rate, + sample_width=data.dtype.itemsize, + channels=(1 if len(data.shape) == 1 else data.shape[1]), + ) + file = audio.export(filename, format=format) + file.close() # type: ignore + + +def convert_to_16_bit_audio(data): + # Based on: https://docs.scipy.org/doc/scipy/reference/generated/scipy.io.wavfile.write.html + warning = "Trying to convert audio automatically from {} to 16-bit int format." + if data.dtype in [np.float64, np.float32, np.float16]: + warnings.warn(warning.format(data.dtype)) + peak = np.abs(data).max() + if peak == 0: + # Silence: avoid dividing by zero (which would produce NaNs that + # cast to nonzero int16 garbage and turn silence into noise). + data = np.zeros_like(data, dtype=np.int16) + else: + data = data / peak + data = data * 32767 + data = data.astype(np.int16) + elif data.dtype == np.int32: + warnings.warn(warning.format(data.dtype)) + data = data / 65536 + data = data.astype(np.int16) + elif data.dtype == np.int16: + pass + elif data.dtype == np.uint16: + warnings.warn(warning.format(data.dtype)) + data = data - 32768 + data = data.astype(np.int16) + elif data.dtype == np.uint8: + warnings.warn(warning.format(data.dtype)) + data = data * 257 - 32768 + data = data.astype(np.int16) + elif data.dtype == np.int8: + warnings.warn(warning.format(data.dtype)) + data = data * 256 + data = data.astype(np.int16) + else: + raise ValueError( + "Audio data cannot be converted automatically from " + f"{data.dtype} to 16-bit int format." + ) + return data + + +# Backwards-compatible alias: this function now handles non-WAV formats too, +# but it was previously named `convert_to_16_bit_wav` and may be imported by +# external code. +convert_to_16_bit_wav = convert_to_16_bit_audio + + +################## +# OUTPUT +################## + + +def _convert(image, dtype, force_copy=False, uniform=False): + """ + Adapted from: https://github.com/scikit-image/scikit-image/blob/main/skimage/util/dtype.py#L510-L531 + + Convert an image to the requested data-type. + Warnings are issued in case of precision loss, or when negative values + are clipped during conversion to unsigned integer types (sign loss). + Floating point values are expected to be normalized and will be clipped + to the range [0.0, 1.0] or [-1.0, 1.0] when converting to unsigned or + signed integers respectively. + Numbers are not shifted to the negative side when converting from + unsigned to signed integer types. Negative values will be clipped when + converting to unsigned integers. + Parameters + ---------- + image : ndarray + Input image. + dtype : dtype + Target data-type. + force_copy : bool, optional + Force a copy of the data, irrespective of its current dtype. + uniform : bool, optional + Uniformly quantize the floating point range to the integer range. + By default (uniform=False) floating point values are scaled and + rounded to the nearest integers, which minimizes back and forth + conversion errors. + .. versionchanged :: 0.15 + ``_convert`` no longer warns about possible precision or sign + information loss. See discussions on these warnings at: + https://github.com/scikit-image/scikit-image/issues/2602 + https://github.com/scikit-image/scikit-image/issues/543#issuecomment-208202228 + https://github.com/scikit-image/scikit-image/pull/3575 + References + ---------- + .. [1] DirectX data conversion rules. + https://msdn.microsoft.com/en-us/library/windows/desktop/dd607323%28v=vs.85%29.aspx + .. [2] Data Conversions. In "OpenGL ES 2.0 Specification v2.0.25", + pp 7-8. Khronos Group, 2010. + .. [3] Proper treatment of pixels as integers. A.W. Paeth. + In "Graphics Gems I", pp 249-256. Morgan Kaufmann, 1990. + .. [4] Dirty Pixels. J. Blinn. In "Jim Blinn's corner: Dirty Pixels", + pp 47-57. Morgan Kaufmann, 1998. + """ + dtype_range = { + bool: (False, True), + np.bool_: (False, True), + float: (-1, 1), + np.float16: (-1, 1), + np.float32: (-1, 1), + np.float64: (-1, 1), + } + + if hasattr(np, "float_"): + dtype_range[np.float_] = dtype_range[float] # type: ignore + if hasattr(np, "bool8"): + dtype_range[np.bool8] = dtype_range[np.bool_] # type: ignore + + def _dtype_itemsize(itemsize, *dtypes): + """Return first of `dtypes` with itemsize greater than `itemsize` + Parameters + ---------- + itemsize: int + The data type object element size. + Other Parameters + ---------------- + *dtypes: + Any Object accepted by `np.dtype` to be converted to a data + type object + Returns + ------- + dtype: data type object + First of `dtypes` with itemsize greater than `itemsize`. + """ + return next(dt for dt in dtypes if np.dtype(dt).itemsize >= itemsize) + + def _dtype_bits(kind, bits, itemsize=1): + """Return dtype of `kind` that can store a `bits` wide unsigned int + Parameters: + kind: str + Data type kind. + bits: int + Desired number of bits. + itemsize: int + The data type object element size. + Returns + ------- + dtype: data type object + Data type of `kind` that can store a `bits` wide unsigned int + """ + + s = next( + i + for i in (itemsize,) + (2, 4, 8) + if bits < (i * 8) or (bits == (i * 8) and kind == "u") + ) + + return np.dtype(kind + str(s)) + + def _scale(a, n, m, copy=True): + """Scale an array of unsigned/positive integers from `n` to `m` bits. + Numbers can be represented exactly only if `m` is a multiple of `n`. + Parameters + ---------- + a : ndarray + Input image array. + n : int + Number of bits currently used to encode the values in `a`. + m : int + Desired number of bits to encode the values in `out`. + copy : bool, optional + If True, allocates and returns new array. Otherwise, modifies + `a` in place. + Returns + ------- + out : array + Output image array. Has the same kind as `a`. + """ + kind = a.dtype.kind + if n > m and a.max() < 2**m: + return a.astype(_dtype_bits(kind, m)) + elif n == m: + return a.copy() if copy else a + elif n > m: + # downscale with precision loss + if copy: + b = np.empty(a.shape, _dtype_bits(kind, m)) + np.floor_divide(a, 2 ** (n - m), out=b, dtype=a.dtype, casting="unsafe") + return b + else: + a //= 2 ** (n - m) + return a + elif m % n == 0: + # exact upscale to a multiple of `n` bits + if copy: + b = np.empty(a.shape, _dtype_bits(kind, m)) + np.multiply(a, (2**m - 1) // (2**n - 1), out=b, dtype=b.dtype) + return b + else: + a = a.astype(_dtype_bits(kind, m, a.dtype.itemsize), copy=False) + a *= (2**m - 1) // (2**n - 1) + return a + else: + # upscale to a multiple of `n` bits, + # then downscale with precision loss + o = (m // n + 1) * n + if copy: + b = np.empty(a.shape, _dtype_bits(kind, o)) + np.multiply(a, (2**o - 1) // (2**n - 1), out=b, dtype=b.dtype) + b //= 2 ** (o - m) + return b + else: + a = a.astype(_dtype_bits(kind, o, a.dtype.itemsize), copy=False) + a *= (2**o - 1) // (2**n - 1) + a //= 2 ** (o - m) + return a + + image = np.asarray(image) + dtypeobj_in = image.dtype + dtypeobj_out = ( + dtypeobj_in + if dtype is np.floating + else np.dtype("float64") + if dtype is float + else np.dtype(dtype) + ) + dtype_in = dtypeobj_in.type + dtype_out = dtypeobj_out.type + kind_in = dtypeobj_in.kind + kind_out = dtypeobj_out.kind + itemsize_in = dtypeobj_in.itemsize + itemsize_out = dtypeobj_out.itemsize + + # Below, we do an `issubdtype` check. Its purpose is to find out + # whether we can get away without doing any image conversion. This happens + # when: + # + # - the output and input dtypes are the same or + # - when the output is specified as a type, and the input dtype + # is a subclass of that type (e.g. `np.floating` will allow + # `float32` and `float64` arrays through) + + if hasattr(np, "obj2sctype"): + is_subdtype = np.issubdtype(dtype_in, np.obj2sctype(dtype)) # type: ignore + else: + is_subdtype = np.issubdtype(dtype_in, dtypeobj_out.type) + + if is_subdtype: + if force_copy: + image = image.copy() + return image + + if kind_in in "ui": + imin_in = np.iinfo(dtype_in).min + imax_in = np.iinfo(dtype_in).max + if kind_out in "ui": + imin_out = np.iinfo(dtype_out).min # type: ignore + imax_out = np.iinfo(dtype_out).max # type: ignore + + # any -> binary + if kind_out == "b": + return image > dtype_in(dtype_range[dtype_in][1] / 2) + + # binary -> any + if kind_in == "b": + result = image.astype(dtype_out) + if kind_out != "f": + result *= dtype_out(dtype_range[dtype_out][1]) + return result + + # float -> any + if kind_in == "f": + if kind_out == "f": + # float -> float + return image.astype(dtype_out) + + if np.min(image) < -1.0 or np.max(image) > 1.0: + raise ValueError("Images of type float must be between -1 and 1.") + # floating point -> integer + # use float type that can represent output integer type + computation_type = _dtype_itemsize( + itemsize_out, dtype_in, np.float32, np.float64 + ) + + if not uniform: + if kind_out == "u": + image_out = np.multiply(image, imax_out, dtype=computation_type) # type: ignore + else: + image_out = np.multiply( + image, + (imax_out - imin_out) / 2, # type: ignore + dtype=computation_type, + ) + image_out -= 1.0 / 2.0 + np.rint(image_out, out=image_out) + np.clip(image_out, imin_out, imax_out, out=image_out) # type: ignore + elif kind_out == "u": + image_out = np.multiply(image, imax_out + 1, dtype=computation_type) # type: ignore + np.clip(image_out, 0, imax_out, out=image_out) # type: ignore + else: + image_out = np.multiply( + image, + (imax_out - imin_out + 1.0) / 2.0, # type: ignore + dtype=computation_type, + ) + np.floor(image_out, out=image_out) + np.clip(image_out, imin_out, imax_out, out=image_out) # type: ignore + return image_out.astype(dtype_out) + + # signed/unsigned int -> float + if kind_out == "f": + # use float type that can exactly represent input integers + computation_type = _dtype_itemsize( + itemsize_in, dtype_out, np.float32, np.float64 + ) + + if kind_in == "u": + # using np.divide or np.multiply doesn't copy the data + # until the computation time + image = np.multiply(image, 1.0 / imax_in, dtype=computation_type) # type: ignore + # DirectX uses this conversion also for signed ints + # if imin_in: + # np.maximum(image, -1.0, out=image) + else: + image = np.add(image, 0.5, dtype=computation_type) + image *= 2 / (imax_in - imin_in) # type: ignore + + return np.asarray(image, dtype_out) + + # unsigned int -> signed/unsigned int + if kind_in == "u": + if kind_out == "i": + # unsigned int -> signed int + image = _scale(image, 8 * itemsize_in, 8 * itemsize_out - 1) + return image.view(dtype_out) + else: + # unsigned int -> unsigned int + return _scale(image, 8 * itemsize_in, 8 * itemsize_out) + + # signed int -> unsigned int + if kind_out == "u": + image = _scale(image, 8 * itemsize_in - 1, 8 * itemsize_out) + result = np.empty(image.shape, dtype_out) + np.maximum(image, 0, out=result, dtype=image.dtype, casting="unsafe") + return result + + # signed int -> signed int + if itemsize_in > itemsize_out: + return _scale(image, 8 * itemsize_in - 1, 8 * itemsize_out - 1) + + image = image.astype(_dtype_bits("i", itemsize_out * 8)) + image -= imin_in # type: ignore + image = _scale(image, 8 * itemsize_in, 8 * itemsize_out, copy=False) + image += imin_out # type: ignore + return image.astype(dtype_out) + + +def ffmpeg_installed() -> bool: + return shutil.which("ffmpeg") is not None + + +def video_is_playable(video_filepath: str) -> bool: + """Determines if a video is playable in the browser. + + A video is playable if it has a playable container and codec. + .mp4 -> h264 + .webm -> vp9 + .ogg -> theora + """ + from gradio._vendor.ffmpy import FFprobe, FFRuntimeError + + try: + container = Path(video_filepath).suffix.lower() + probe = FFprobe( + global_options="-show_format -show_streams -select_streams v -print_format json", + inputs={video_filepath: None}, + ) + output = probe.run(stderr=subprocess.PIPE, stdout=subprocess.PIPE) + output = json.loads(output[0]) # type: ignore + video_codec = output["streams"][0]["codec_name"] + return (container, video_codec) in [ + (".mp4", "h264"), + (".mp4", "av1"), + (".ogg", "theora"), + (".webm", "vp9"), + (".webm", "vp8"), + (".webm", "av1"), + ] + # If anything goes wrong, assume the video can be played to not convert downstream + except (FFRuntimeError, IndexError, KeyError): + return True + + +def convert_video_to_playable_mp4(video_path: str) -> str: + """Convert the video to mp4. If something goes wrong return the original video.""" + from gradio._vendor.ffmpy import FFmpeg, FFRuntimeError + + try: + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + output_path = Path(video_path).with_suffix(".mp4") + shutil.copy2(video_path, tmp_file.name) + # ffmpeg will automatically use h264 codec (playable in browser) when converting to mp4 + ff = FFmpeg( + inputs={str(tmp_file.name): None}, + outputs={str(output_path): None}, + global_options="-y -loglevel quiet", + ) + ff.run() + except FFRuntimeError as e: + print(f"Error converting video to browser-playable format {str(e)}") + output_path = video_path + finally: + # Remove temp file + os.remove(tmp_file.name) # type: ignore + return str(output_path) + + +def get_video_length(video_path: str | Path): + duration = subprocess.check_output( + [ + "ffprobe", + "-i", + str(video_path), + "-show_entries", + "format=duration", + "-v", + "quiet", + "-of", + "csv={}".format("p=0"), + ] + ) + duration_str = duration.decode("utf-8").strip() + duration_float = float(duration_str) + + return duration_float diff --git a/gradio/profiling.py b/gradio/profiling.py new file mode 100644 index 0000000..2e777f0 --- /dev/null +++ b/gradio/profiling.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +import contextvars +import os +import time +from collections import deque +from contextlib import asynccontextmanager, contextmanager +from dataclasses import dataclass, field +from functools import wraps +from typing import Any + +PROFILING_ENABLED = os.environ.get("GRADIO_PROFILING", "").strip() in ("1", "true") + + +@dataclass +class RequestTrace: + event_id: str | None = None + fn_name: str | None = None + session_hash: str | None = None + timestamp: float = field(default_factory=time.time) + + queue_wait_ms: float = 0.0 + preprocess_ms: float = 0.0 + fn_call_ms: float = 0.0 + postprocess_ms: float = 0.0 + streaming_diff_ms: float = 0.0 + total_ms: float = 0.0 + n_iterations: int = 0 + upload_ms: float = 0.0 + preprocess_move_to_cache_ms: float = 0.0 + preprocess_format_image_ms: float = 0.0 + postprocess_save_img_array_to_cache_ms: float = 0.0 + preprocess_audio_from_file_ms: float = 0.0 + postprocess_save_audio_to_cache_ms: float = 0.0 + preprocess_video_ms: float = 0.0 + postprocess_video_convert_video_to_playable_mp4_ms: float = 0.0 + postprocess_update_state_in_config_ms: float = 0.0 + postprocess_move_to_cache_ms: float = 0.0 + postprocess_video_ms: float = 0.0 + postprocess_save_pil_to_cache_ms: float = 0.0 + postprocess_save_bytes_to_cache_ms: float = 0.0 + save_file_to_cache_ms: float = 0.0 + + def set_phase(self, name: str, duration_ms: float): + attr = f"{name}_ms" + if hasattr(self, attr): + # Accumulate across generator iterations + setattr(self, attr, getattr(self, attr) + duration_ms) + if name == "total": + self.n_iterations += 1 + + def to_dict(self) -> dict[str, Any]: + return { + "event_id": self.event_id, + "fn_name": self.fn_name, + "session_hash": self.session_hash, + "timestamp": self.timestamp, + "queue_wait_ms": self.queue_wait_ms, + "preprocess_ms": self.preprocess_ms, + "fn_call_ms": self.fn_call_ms, + "postprocess_ms": self.postprocess_ms, + "streaming_diff_ms": self.streaming_diff_ms, + "total_ms": self.total_ms, + "n_iterations": self.n_iterations, + "preprocess_move_to_cache_ms": self.preprocess_move_to_cache_ms, + "preprocess_format_image_ms": self.preprocess_format_image_ms, + "postprocess_save_img_array_to_cache_ms": self.postprocess_save_img_array_to_cache_ms, + "preprocess_audio_from_file_ms": self.preprocess_audio_from_file_ms, + "postprocess_save_audio_to_cache_ms": self.postprocess_save_audio_to_cache_ms, + "preprocess_video_ms": self.preprocess_video_ms, + "postprocess_video_convert_video_to_playable_mp4_ms": self.postprocess_video_convert_video_to_playable_mp4_ms, + "postprocess_update_state_in_config_ms": self.postprocess_update_state_in_config_ms, + "postprocess_move_to_cache_ms": self.postprocess_move_to_cache_ms, + "postprocess_video_ms": self.postprocess_video_ms, + "postprocess_save_pil_to_cache_ms": self.postprocess_save_pil_to_cache_ms, + "postprocess_save_bytes_to_cache_ms": self.postprocess_save_bytes_to_cache_ms, + "save_file_to_cache_ms": self.save_file_to_cache_ms, + } + + +_current_trace: contextvars.ContextVar[RequestTrace | None] = contextvars.ContextVar( + "_current_trace", default=None +) + + +def get_current_trace() -> RequestTrace | None: + return _current_trace.get() + + +def set_current_trace(trace: RequestTrace) -> contextvars.Token: + return _current_trace.set(trace) + + +@asynccontextmanager +async def trace_phase(name: str): + """Async context manager that records timing for a named phase into the current trace.""" + trace = _current_trace.get() + if trace is None: + yield + return + start = time.monotonic() + try: + yield + finally: + duration_ms = (time.monotonic() - start) * 1000 + trace.set_phase(name, duration_ms) + + +@contextmanager +def trace_phase_sync(name: str): + """Context manager that records timing for a named phase into the current trace.""" + trace = _current_trace.get() + if trace is None: + yield + return + start = time.monotonic() + try: + yield + finally: + duration_ms = (time.monotonic() - start) * 1000 + trace.set_phase(name, duration_ms) + + +def traced(phase): + if not PROFILING_ENABLED: + return lambda f: f + + def _factory(f): + @wraps(f) + async def wrapper(*args, **kwargs): + async with trace_phase(phase): + return await f(*args, **kwargs) + + return wrapper + + return _factory + + +def traced_sync(phase): + if not PROFILING_ENABLED: + return lambda f: f + + def _factory(f): + @wraps(f) + def wrapper(*args, **kwargs): + with trace_phase_sync(phase): + return f(*args, **kwargs) + + return wrapper + + return _factory + + +class TraceCollector: + def __init__(self, maxlen: int = 100_000): + self._traces: deque[RequestTrace] = deque(maxlen=maxlen) + + def add(self, trace: RequestTrace): + self._traces.append(trace) + + def get_all(self, last_n: int | None = None) -> list[dict[str, Any]]: + traces = list(self._traces) + if last_n is not None: + traces = traces[-last_n:] + return [t.to_dict() for t in traces] + + def get_summary(self) -> dict[str, Any]: + if not self._traces: + return {"count": 0, "phases": {}} + + import numpy as np + + prediction_traces = [ + t for t in self._traces if t.fn_name != "gradio_file_upload" + ] + upload_traces = [t for t in self._traces if t.fn_name == "gradio_file_upload"] + + phases = [ + "queue_wait", + "preprocess", + "fn_call", + "postprocess", + "streaming_diff", + "total", + ] + + def _percentiles(arr): + return { + "p50": float(np.percentile(arr, 50)), + "p90": float(np.percentile(arr, 90)), + "p95": float(np.percentile(arr, 95)), + "p99": float(np.percentile(arr, 99)), + "mean": float(np.mean(arr)), + "min": float(np.min(arr)), + "max": float(np.max(arr)), + } + + result: dict[str, Any] = { + "count": len(prediction_traces), + "phases": {}, + } + for phase in phases: + values = [getattr(t, f"{phase}_ms") for t in prediction_traces] + if values: + result["phases"][phase] = _percentiles(np.array(values)) + else: + result["phases"][phase] = { + "p50": 0.0, + "p90": 0.0, + "p95": 0.0, + "p99": 0.0, + "mean": 0.0, + "min": 0.0, + "max": 0.0, + } + + if upload_traces: + upload_values = [t.upload_ms for t in upload_traces] + result["upload"] = { + "count": len(upload_traces), + **_percentiles(np.array(upload_values)), + } + + return result + + def clear(self): + self._traces.clear() + + +# Global collector instance +collector = TraceCollector() + + +if not PROFILING_ENABLED: + # Replace with no-ops for zero overhead + + @asynccontextmanager + async def trace_phase(name: str): # noqa: ARG001 + yield + + @contextmanager + def trace_phase_sync(name: str): # noqa: ARG001 + yield diff --git a/gradio/py.typed b/gradio/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/gradio/queueing.py b/gradio/queueing.py new file mode 100644 index 0000000..3c47071 --- /dev/null +++ b/gradio/queueing.py @@ -0,0 +1,1134 @@ +from __future__ import annotations + +import asyncio +import copy +import inspect +import os +import platform +import random +import time +import traceback +import uuid +from asyncio import Queue as AsyncQueue +from collections import defaultdict +from typing import TYPE_CHECKING, Any, Literal, cast + +import fastapi +import numpy as np +from anyio.to_thread import run_sync + +from gradio import route_utils, routes +from gradio.caching import CacheMissError, ProbeCache +from gradio.data_classes import ( + PredictBodyInternal, +) +from gradio.exceptions import Error +from gradio.helpers import TrackedIterable +from gradio.server_messages import ( + EstimationMessage, + EventMessage, + LogMessage, + ProcessCompletedMessage, + ProcessGeneratingMessage, + ProcessStartsMessage, + ProgressMessage, + ProgressUnit, + ServerMessage, +) +from gradio.utils import ( + LRUCache, + error_payload, + run_coro_in_background, + safe_aclose_iterator, + safe_get_lock, + set_task_name, +) + +from .block_function import BlockFunction + +if TYPE_CHECKING: + from gradio.block_function import BlockFunction + from gradio.blocks import Blocks + + +class Event: + def __init__( + self, + session_hash: str | None, + fn: BlockFunction, + request: fastapi.Request, + username: str | None, + ): + self._id = uuid.uuid4().hex + self.session_hash: str = session_hash or self._id + self.fn = fn + self.request = request + self.username = username + self.concurrency_id = fn.concurrency_id + self.data: PredictBodyInternal | None = None + self.progress: ProgressMessage | None = None + self.progress_pending: bool = False + self.alive = True + self.closed = False + self.n_calls = 0 + self.run_time: float = 0 + self.enqueue_time: float = time.monotonic() + self.signal = asyncio.Event() + + @property + def streaming(self): + return self.fn.connection == "stream" + + @property + def is_finished(self): + if not self.streaming: + raise ValueError("Cannot access if_finished during a non-streaming event") + if self.closed: + return True + if self.fn.time_limit is None: + return False + return self.run_time >= self.fn.time_limit + + +class EventQueue: + def __init__(self, concurrency_id: str, concurrency_limit: int | None): + self.queue: list[Event] = [] + self.concurrency_id = concurrency_id + self.concurrency_limit = concurrency_limit + self.current_concurrency = 0 + self.start_times_per_fn: defaultdict[BlockFunction, set[float]] = defaultdict( + set + ) + + +class ProcessTime: + def __init__(self): + self.process_time = 0 + self.count = 0 + self.avg_time = 0 + + def add(self, time: float): + self.process_time += time + self.count += 1 + self.avg_time = self.process_time / self.count + + +class Queue: + def __init__( + self, + live_updates: bool, + concurrency_count: int, + update_intervals: float, + max_size: int | None, + blocks: Blocks, + default_concurrency_limit: int | None | Literal["not_set"] = "not_set", + ): + self.pending_messages_per_session: LRUCache[str, AsyncQueue[EventMessage]] = ( + LRUCache(2000) + ) + self.pending_event_ids_session: dict[str, set[str]] = {} + self.event_ids_to_events: dict[str, Event] = {} + self.pending_message_lock = safe_get_lock() + self.event_queue_per_concurrency_id: dict[str, EventQueue] = {} + self.stopped = False + self.max_thread_count = concurrency_count + self.update_intervals = update_intervals + self.active_jobs: list[None | list[Event]] = [] + self.delete_lock = safe_get_lock() + self.server_app = None + self.process_time_per_fn: defaultdict[BlockFunction, ProcessTime] = defaultdict( + ProcessTime + ) + self.live_updates = live_updates + self.sleep_when_free = 0.05 if platform.system() == "Windows" else 0.001 + self.progress_update_sleep_when_free = ( + 0.1 if platform.system() == "Windows" else 0.01 + ) + self.max_size = max_size + self.blocks = blocks + self._asyncio_tasks: list[asyncio.Task] = [] + self.default_concurrency_limit = self._resolve_concurrency_limit( + default_concurrency_limit + ) + self.event_analytics: dict[str, dict[str, float | str | None]] = {} + self.cached_event_analytics_summary = {"functions": {}} + self.event_count_at_last_cache = 0 + self.ANAYLTICS_CACHE_FREQUENCY = int( + os.getenv("GRADIO_ANALYTICS_CACHE_FREQUENCY", "1") + ) + + @staticmethod + def _get_df(event_analytics): + import pandas as pd + + try: + with pd.option_context("future.no_silent_downcasting", True): + return ( + pd.DataFrame(list(event_analytics.values())) + .fillna(value=np.nan) + .infer_objects(copy=False) # type: ignore + ) + except Exception as e: + if "No such keys(s)" in str(e): + return ( + pd.DataFrame(list(event_analytics.values())) + .fillna(value=np.nan) + .infer_objects(copy=False) # type: ignore + ) + raise e + + def compute_analytics_summary(self, event_analytics): + if ( + len(event_analytics) - self.event_count_at_last_cache + >= self.ANAYLTICS_CACHE_FREQUENCY + ): + df = self._get_df(event_analytics) + self.event_count_at_last_cache = len(event_analytics) + grouped = df.groupby("function") + metrics = {"functions": {}} + for fn_name, fn_df in grouped: + status = fn_df["status"].values + success = np.sum(status == "success") + failure = np.sum(status == "failed") + total = success + failure + success_rate = success / total if total > 0 else None + percentiles = np.percentile(fn_df["process_time"].values, [50, 90, 99]) # type: ignore + metrics["functions"][fn_name] = { + "success_rate": success_rate, + "process_time_percentiles": { + "50th": percentiles[0], # type: ignore + "90th": percentiles[1], # type: ignore + "99th": percentiles[2], # type: ignore + }, + "total_requests": fn_df.shape[0], + } + self.cached_event_analytics_summary = metrics + return self.cached_event_analytics_summary + + def start(self): + self.active_jobs = [None] * self.max_thread_count + + run_coro_in_background(self.start_processing) + run_coro_in_background(self.start_progress_updates) + if not self.live_updates: + run_coro_in_background(self.notify_clients) + + def create_event_queue_for_fn(self, block_fn: BlockFunction): + concurrency_id = block_fn.concurrency_id + concurrency_limit: int | None + if block_fn.concurrency_limit == "default": + concurrency_limit = self.default_concurrency_limit + else: + concurrency_limit = block_fn.concurrency_limit + if concurrency_id not in self.event_queue_per_concurrency_id: + self.event_queue_per_concurrency_id[concurrency_id] = EventQueue( + concurrency_id, concurrency_limit + ) + elif ( + concurrency_limit is not None + ): # Update concurrency limit if it is lower than existing limit + existing_event_queue = self.event_queue_per_concurrency_id[concurrency_id] + if ( + existing_event_queue.concurrency_limit is None + or concurrency_limit < existing_event_queue.concurrency_limit + ): + existing_event_queue.concurrency_limit = concurrency_limit + + def close(self): + self.stopped = True + + def send_message( + self, + event: Event, + event_message: EventMessage, + ): + if not event.alive: + return + event_message.event_id = event._id + messages = self.pending_messages_per_session[event.session_hash] + messages.put_nowait(event_message) + + def _resolve_concurrency_limit( + self, default_concurrency_limit: int | None | Literal["not_set"] + ) -> int | None: + """ + Handles the logic of resolving the default_concurrency_limit as this can be specified via a combination + of the `default_concurrency_limit` parameter of the `Blocks.queue()` or the `GRADIO_DEFAULT_CONCURRENCY_LIMIT` + environment variable. The parameter in `Blocks.queue()` takes precedence over the environment variable. + Parameters: + default_concurrency_limit: The default concurrency limit, as specified by a user in `Blocks.queue()`. + """ + if default_concurrency_limit != "not_set": + return default_concurrency_limit + if default_concurrency_limit_env := os.environ.get( + "GRADIO_DEFAULT_CONCURRENCY_LIMIT" + ): + if default_concurrency_limit_env.lower() == "none": + return None + else: + return int(default_concurrency_limit_env) + else: + return 1 + + def __len__(self): + total_len = 0 + for event_queue in self.event_queue_per_concurrency_id.values(): + total_len += len(event_queue.queue) + return total_len + + async def push( + self, body: PredictBodyInternal, request: fastapi.Request, username: str | None + ) -> tuple[ + bool, + str | list[dict[str, Any]], + Literal["success", "error", "queue_full", "validator_error"], + ]: + if body.fn_index is None: + return False, "No function index provided.", "error" + if self.max_size is not None and len(self) >= self.max_size: + return ( + False, + f"Queue is full. Max size is {self.max_size} and size is {len(self)}.", + "queue_full", + ) + + if body.session_hash: + session_state = self.blocks.state_holder[body.session_hash] + fn = session_state.blocks_config.fns[body.fn_index] + else: + fn = self.blocks.fns[body.fn_index] + + fn = route_utils.get_fn(self.blocks, None, body) + self.create_event_queue_for_fn(fn) + if fn.validator is not None: + gr_request = route_utils.compile_gr_request( + body=body, + fn=fn, + username=username, + request=None, + ) + assert body.request is not None # noqa: S101 + api_route_path = route_utils.get_api_call_path(request=body.request) + root_path = route_utils.get_root_url( + request=body.request, + route_path=api_route_path, + root_path=self.blocks.app.root_path, + ) + validator_fn = BlockFunction( + fn=fn.validator, + api_name=None, + api_visibility="undocumented", + batch=fn.batch, + concurrency_id=None, + concurrency_limit=None, + inputs=fn.inputs, + outputs=fn.inputs, + preprocess=fn.preprocess, + postprocess=False, + inputs_as_dict=fn.inputs_as_dict, + targets=[], + _id=-1, + max_batch_size=fn.max_batch_size, + tracks_progress=fn.tracks_progress, + js=None, + show_progress="hidden", + show_progress_on=fn.show_progress_on, + cancels=fn.cancels, + collects_event_data=fn.collects_event_data, + ) + + event = Event( + body.session_hash, + validator_fn, + request, + username, + ) + try: + response = await route_utils.call_process_api( + app=self.blocks.app, + body=body, + gr_request=gr_request, + fn=validator_fn, + root_path=root_path, + ) + + validation_response: list[dict[str, Any]] | dict[str, Any] | None = ( + response.get("data") + ) + + if validation_response is not None: + (is_valid, validation_data) = process_validation_response( + validation_response, fn + ) + if is_valid is False: + return ( + False, + validation_data, + "validator_error", + ) + + except Exception as e: + print(str(e)) + return False, str(e), "error" + event = Event( + body.session_hash, + fn, + request, + username, + ) + event.data = body + if body.session_hash is None: + body.session_hash = event.session_hash + async with self.pending_message_lock: + if body.session_hash not in self.pending_messages_per_session: + self.pending_messages_per_session[body.session_hash] = AsyncQueue() + if body.session_hash not in self.pending_event_ids_session: + self.pending_event_ids_session[body.session_hash] = set() + self.pending_event_ids_session[body.session_hash].add(event._id) + self.event_ids_to_events[event._id] = event + body.event_id = event._id if not fn.batch else None + + if hasattr(fn.fn, "cache"): + try: + cache_start = time.time() + gr_request = route_utils.compile_gr_request( + body=body, + fn=fn, + username=username, + request=None, + ) + assert body.request is not None # noqa: S101 + api_route_path = route_utils.get_api_call_path(request=body.request) + root_path = route_utils.get_root_url( + request=body.request, + route_path=api_route_path, + root_path=self.blocks.app.root_path, + ) + with ProbeCache(): + response = await route_utils.call_process_api( + app=self.blocks.app, + body=body, + gr_request=gr_request, + fn=fn, + root_path=root_path, + ) + while response and response.get("is_generating", False): + self.send_message( + event, + ProcessGeneratingMessage( + output=response, + success=True, + ), + ) + response = await route_utils.call_process_api( + app=self.blocks.app, + body=body, + gr_request=gr_request, + fn=fn, + root_path=root_path, + ) + cache_duration = time.time() - cache_start + avg_time = ( + self.process_time_per_fn[fn].avg_time + if fn in self.process_time_per_fn + else None + ) + self.send_message( + event, + ProcessCompletedMessage( + output=response, + success=True, + used_cache="full", + cache_duration=cache_duration, + avg_time=avg_time, + ), + ) + return True, event._id, "success" + except CacheMissError: + pass # Fall through to normal queue path + except Exception: + raise + + try: + event_queue = self.event_queue_per_concurrency_id[event.concurrency_id] + except KeyError as e: + raise KeyError( + "Event not found in queue. If you are deploying this Gradio app with multiple replicas, please enable stickiness to ensure that all requests from the same user are routed to the same instance." + ) from e + event_queue.queue.append(event) + self.event_analytics[event._id] = { + "time": time.time(), + "status": "queued", + "process_time": None, + "function": fn.api_name, + "session_hash": body.session_hash, + } + + self.broadcast_estimations(event.concurrency_id, len(event_queue.queue) - 1) + return True, event._id, "success" + + async def remove_from_queue(self, event_id: str): + event = self.event_ids_to_events.get(event_id) + if event: + async with self.delete_lock: + q = self.event_queue_per_concurrency_id[event.concurrency_id] + try: + q.queue.remove(event) + self.event_ids_to_events.pop(event_id, None) + except ValueError: + pass + + def _cancel_asyncio_tasks(self): + for task in self._asyncio_tasks: + task.cancel() + self._asyncio_tasks = [] + + def set_server_app(self, app: routes.App): + self.server_app = app + + def get_active_worker_count(self) -> int: + count = 0 + for worker in self.active_jobs: + if worker is not None: + count += 1 + return count + + def get_events(self) -> tuple[list[Event], bool, str] | None: + concurrency_ids = list(self.event_queue_per_concurrency_id.keys()) + random.shuffle(concurrency_ids) + for concurrency_id in concurrency_ids: + event_queue = self.event_queue_per_concurrency_id[concurrency_id] + if len(event_queue.queue) and ( + event_queue.concurrency_limit is None + or event_queue.current_concurrency < event_queue.concurrency_limit + ): + first_event = event_queue.queue[0] + block_fn = first_event.fn + events = [first_event] + batch = block_fn.batch + if batch: + events += [ + event + for event in event_queue.queue[1:] + if event.fn == first_event.fn + ][: block_fn.max_batch_size - 1] + + for event in events: + event_queue.queue.remove(event) + + return events, batch, concurrency_id + + async def start_processing(self) -> None: + try: + while not self.stopped: + if len(self) == 0: + await asyncio.sleep(self.sleep_when_free) + continue + + if None not in self.active_jobs: + await asyncio.sleep(self.sleep_when_free) + continue + + # Using mutex to avoid editing a list in use + async with self.delete_lock: + event_batch = self.get_events() + + if event_batch: + events, batch, concurrency_id = event_batch + self.active_jobs[self.active_jobs.index(None)] = events + event_queue = self.event_queue_per_concurrency_id[concurrency_id] + event_queue.current_concurrency += 1 + start_time = time.time() + event_queue.start_times_per_fn[events[0].fn].add(start_time) + for event in events: + self.event_analytics[event._id]["status"] = "processing" + process_event_task = run_coro_in_background( + self.process_events, events, batch, start_time + ) + set_task_name( + process_event_task, + events[0].session_hash, + events[0].fn._id, + events[0]._id, + batch, + ) + + self._asyncio_tasks.append(process_event_task) + if self.live_updates: + self.broadcast_estimations(concurrency_id) + else: + await asyncio.sleep(self.sleep_when_free) + finally: + self.stopped = True + self._cancel_asyncio_tasks() + + async def start_progress_updates(self) -> None: + """ + Because progress updates can be very frequent, we do not necessarily want to send a message per update. + Rather, we check for progress updates at regular intervals, and send a message if there is a pending update. + Consecutive progress updates between sends will overwrite each other so only the most recent update will be sent. + """ + while not self.stopped: + events = [evt for job in self.active_jobs if job is not None for evt in job] + + if len(events) == 0: + await asyncio.sleep(self.progress_update_sleep_when_free) + continue + + for event in events: + if event.progress_pending and event.progress: + event.progress_pending = False + self.send_message(event, event.progress) + + await asyncio.sleep(self.progress_update_sleep_when_free) + + def set_progress( + self, + event_id: str, + iterables: list[TrackedIterable] | None, + ): + if iterables is None: + return + for job in self.active_jobs: + if job is None: + continue + for evt in job: + if evt._id == event_id: + progress_data: list[ProgressUnit] = [] + for iterable in iterables: + progress_unit = ProgressUnit( + index=iterable.index, + length=iterable.length, + unit=iterable.unit, + progress=iterable.progress, + desc=iterable.desc, + ) + progress_data.append(progress_unit) + evt.progress = ProgressMessage(progress_data=progress_data) + evt.progress_pending = True + + def log_message( + self, + event_id: str, + log: str, + title: str, + level: Literal["info", "warning", "success"], + duration: float | None = 10, + visible: bool = True, + ): + events = [evt for job in self.active_jobs if job is not None for evt in job] + for event in events: + if event._id == event_id: + log_message = LogMessage( + log=log, + level=level, + duration=duration, + visible=visible, + title=title, + ) + self.send_message(event, log_message) + + async def clean_events( + self, *, session_hash: str | None = None, event_id: str | None = None + ) -> None: + for job_set in self.active_jobs: + if job_set: + for job in job_set: + if job.session_hash == session_hash or job._id == event_id: + job.alive = False + + async with self.delete_lock: + events_to_remove: list[Event] = [] + for event_queue in self.event_queue_per_concurrency_id.values(): + for event in event_queue.queue: + if event.session_hash == session_hash or event._id == event_id: + events_to_remove.append(event) + + for event in events_to_remove: + self.event_queue_per_concurrency_id[event.concurrency_id].queue.remove( + event + ) + self.event_ids_to_events.pop(event._id, None) + + if session_hash and session_hash in self.pending_event_ids_session: + removed_ids = {e._id for e in events_to_remove} + self.pending_event_ids_session[session_hash] -= removed_ids + if not self.pending_event_ids_session[session_hash]: + self.pending_event_ids_session.pop(session_hash, None) + + async def notify_clients(self) -> None: + """ + Notify clients about events statuses in the queue periodically. + """ + while not self.stopped: + await asyncio.sleep(self.update_intervals) + if len(self) > 0: + for concurrency_id in self.event_queue_per_concurrency_id: + self.broadcast_estimations(concurrency_id) + + def broadcast_estimations( + self, concurrency_id: str, after: int | None = None + ) -> None: + wait_so_far = 0 + event_queue = self.event_queue_per_concurrency_id[concurrency_id] + time_till_available_worker: int | None = 0 + + if event_queue.current_concurrency == event_queue.concurrency_limit: + expected_end_times = [] + for fn, start_times in event_queue.start_times_per_fn.items(): + if fn not in self.process_time_per_fn: + time_till_available_worker = None + break + if fn.connection == "stream": + process_time = fn.time_limit or 0 + else: + process_time = self.process_time_per_fn[fn].avg_time + expected_end_times += [ + start_time + process_time for start_time in start_times + ] + if time_till_available_worker is not None and len(expected_end_times) > 0: + time_of_first_completion = min(expected_end_times) + time_till_available_worker = max( + time_of_first_completion - time.time(), 0 + ) + + for rank, event in enumerate(event_queue.queue): + process_time_for_fn = ( + self.process_time_per_fn[event.fn].avg_time + if event.fn in self.process_time_per_fn + else None + ) + + # eta is the time remaining from now until the result will be returned + # process_time_for_fn = time to run fn once worker assigned to it + # wait_so_far = time till event gets to the head of the queue + # time_till_available_worker = time for a worker to be assigned to it once its at the head + # For streaming events, we modify this calculation slightly to be the time until the first + # chunk is processed. + rank_eta = ( + process_time_for_fn + wait_so_far + time_till_available_worker + if process_time_for_fn is not None + and wait_so_far is not None + and time_till_available_worker is not None + else None + ) + + if after is None or rank >= after: + self.send_message( + event, + EstimationMessage( + rank=rank, rank_eta=rank_eta, queue_size=len(event_queue.queue) + ), + ) + if event_queue.concurrency_limit is None: + wait_so_far = 0 + elif wait_so_far is not None and process_time_for_fn is not None: + delta = process_time_for_fn / event_queue.concurrency_limit + if event.streaming: + delta = ( + time_till_available_worker or 0 + ) / event_queue.concurrency_limit + wait_so_far += delta + else: + wait_so_far = None + + def get_status(self) -> EstimationMessage: + return EstimationMessage( + queue_size=len(self), + ) + + @staticmethod + async def wait_for_event(event: Event) -> str: + await event.signal.wait() + return "signal" + + @staticmethod + async def timeout(timeout: float) -> str: + await asyncio.sleep(timeout) + return "timeout" + + @staticmethod + async def wait_for_event_or_timeout( + event: Event, timeout: float + ) -> Literal["signal", "timeout"]: + t1 = asyncio.create_task(Queue.wait_for_event(event)) + t2 = asyncio.create_task(Queue.timeout(timeout)) + done, _ = await asyncio.wait( + [t1, t2], + return_when=asyncio.FIRST_COMPLETED, + ) + done = [d.result() for d in done] + event.signal.clear() + return cast(Literal["signal", "timeout"], done[0]) + + @staticmethod + async def wait_for_batch( + events: list[Event], timeouts: list[float] + ) -> tuple[list[Event], list[Event]]: + tasks = [] + for event, timeout in zip(events, timeouts, strict=False): + tasks.append( + asyncio.create_task(Queue.wait_for_event_or_timeout(event, timeout)) + ) + done, _ = await asyncio.wait( + tasks, + return_when=asyncio.ALL_COMPLETED, + ) + done = [d.result() for d in done] + awake_events = [] + closed_events = [] + for result, event in zip(done, events, strict=False): + if result == "signal": + awake_events.append(event) + else: + closed_events.append(event) + return awake_events, closed_events + + async def process_events( + self, events: list[Event], batch: bool, begin_time: float + ) -> None: + awake_events: list[Event] = [] + fn = events[0].fn + success = False + try: + for event in events: + if event.alive: + self.send_message( + event, + ProcessStartsMessage( + eta=self.process_time_per_fn[fn].avg_time + if fn in self.process_time_per_fn + else None + ), + ) + awake_events.append(event) + if not awake_events: + return + + events = awake_events + body = events[0].data + if body is None: + raise ValueError("No event data") + username = events[0].username + body.event_id = events[0]._id if not batch else None + try: + body.request = events[0].request + except ValueError: + pass + + if batch: + body.data = list( + zip( + *[event.data.data for event in events if event.data], + strict=False, + ) + ) + body.request = events[0].request + body.batched = True + + app = self.server_app + if app is None: + raise Exception("Server app has not been set.") + + gr_request = route_utils.compile_gr_request( + body=body, + fn=fn, + username=username, + request=None, + ) + assert body.request is not None # noqa: S101 + api_route_path = route_utils.get_api_call_path(request=body.request) + root_path = route_utils.get_root_url( + request=body.request, + route_path=api_route_path, + root_path=app.root_path, + ) + first_iteration = 0 + + from gradio.profiling import ( + PROFILING_ENABLED, + RequestTrace, + set_current_trace, + ) + + if PROFILING_ENABLED: + trace = RequestTrace( + event_id=events[0]._id, + fn_name=fn.api_name or str(fn.fn), + session_hash=events[0].session_hash, + ) + trace.queue_wait_ms = (time.monotonic() - events[0].enqueue_time) * 1000 + set_current_trace(trace) + else: + trace = None + + try: + start = time.monotonic() + response = await route_utils.call_process_api( + app=app, + body=body, + gr_request=gr_request, + fn=fn, + root_path=root_path, + ) + end = time.monotonic() + first_iteration = end - start + err = None + for event in awake_events: + event.run_time += end - start + if event.streaming: + response["is_generating"] = not event.is_finished + + except Exception as e: + if not isinstance(e, Error) or e.print_exception: + traceback.print_exc() + response = None + err = e + for event in awake_events: + content = error_payload(err, app.get_blocks().show_error) + self.send_message( + event, + ProcessCompletedMessage( + output=content, + title=content.get("title", "Error"), # type: ignore + success=False, + ), + ) + await run_sync(self.compute_analytics_summary, self.event_analytics) + if response and response.get("is_generating", False): + old_response = response + old_err = err + while response and response.get("is_generating", False): + start = time.monotonic() + old_response = response + old_err = err + for event in awake_events: + self.send_message( + event, + ProcessGeneratingMessage( + msg=ServerMessage.process_generating + if not event.streaming + else ServerMessage.process_streaming, + output=old_response, + success=old_response is not None, + time_limit=None + if not fn.time_limit + else cast(int, fn.time_limit) - first_iteration + if event.streaming + else None, + ), + ) + awake_events = [event for event in awake_events if event.alive] + if not awake_events: + return + try: + start = time.monotonic() + if awake_events[0].streaming: + awake_events, closed_events = await Queue.wait_for_batch( + awake_events, + # We need to wait for all of the events to have the latest input data + # the max time is the time limit of the function or 30 seconds (arbitrary) but should + # never really take that long to make a request from the client to the server unless + # the client disconnected. + [cast(float, fn.time_limit or 30) - first_iteration] + * len(awake_events), + ) + for closed_event in closed_events: + self.send_message( + closed_event, + ProcessCompletedMessage( + output=response, success=True + ), + ) + if not awake_events: + break + body = cast(PredictBodyInternal, awake_events[0].data) + if batch: + body.data = list( + zip( + *[ + event.data.data + for event in events + if event.data + ], + strict=False, + ) + ) + response = await route_utils.call_process_api( + app=app, + body=body, + gr_request=gr_request, + fn=fn, + root_path=root_path, + ) + end = time.monotonic() + for event in awake_events: + event.run_time += end - start + if event.streaming: + response["is_generating"] = not event.is_finished + except Exception as e: + if not isinstance(e, Error) or e.print_exception: + traceback.print_exc() + response = None + err = e + + if response: + success = True + output = response + else: + success = False + error = err or old_err + output = error_payload(error, app.get_blocks().show_error) + cache_source = output + if ( + success + and not output.get("used_cache") + and old_response + and old_response.get("used_cache") + ): + cache_source = old_response + used_cache = cache_source.get("used_cache") if success else None + used_cache = ( + cast(Literal["full", "partial"], used_cache) + if used_cache in ("full", "partial") + else None + ) + for event in awake_events: + self.send_message( + event, + ProcessCompletedMessage( + output=output, + success=success, + used_cache=used_cache, + cache_duration=cache_source.get("duration"), # type: ignore[arg-type] + avg_time=cache_source.get("average_duration"), # type: ignore[arg-type] + ), + ) + + elif response: + output = copy.deepcopy(response) + for e, event in enumerate(awake_events): + if batch and "data" in output: + output["data"] = list(zip(*response.get("data"), strict=False))[ + e + ] + success = response is not None + used_cache = output.get("used_cache") if success else None + used_cache = ( + cast(Literal["full", "partial"], used_cache) + if used_cache in ("full", "partial") + else None + ) + self.send_message( + event, + ProcessCompletedMessage( + output=output, + success=success, + used_cache=used_cache, + cache_duration=output.get("duration"), # type: ignore[arg-type] + avg_time=output.get("average_duration"), # type: ignore[arg-type] + ), + ) + end_time = time.time() + if response is not None: + duration = ( + end_time - begin_time + if not events[0].streaming + else first_iteration + ) + if not response.get("used_cache"): + self.process_time_per_fn[events[0].fn].add(duration) + for event in events: + self.event_analytics[event._id]["process_time"] = duration + except Exception as e: + if not isinstance(e, Error) or e.print_exception: + traceback.print_exc() + finally: + from gradio.profiling import PROFILING_ENABLED, collector, get_current_trace + + if PROFILING_ENABLED: + trace = get_current_trace() + if trace is not None: + collector.add(trace) + + event_queue = self.event_queue_per_concurrency_id[events[0].concurrency_id] + event_queue.current_concurrency -= 1 + start_times = event_queue.start_times_per_fn[fn] + if begin_time in start_times: + start_times.remove(begin_time) + try: + self.active_jobs[self.active_jobs.index(events)] = None + except ValueError: + # `events` can be absent from `self.active_jobs` + # when this coroutine is called from the `join_queue` endpoint handler in `routes.py` + # without putting the `events` into `self.active_jobs`. + # https://github.com/gradio-app/gradio/blob/f09aea34d6bd18c1e2fef80c86ab2476a6d1dd83/gradio/routes.py#L594-L596 + pass + for event in events: + # Always reset the state of the iterator + # If the job finished successfully, this has no effect + # If the job is cancelled, this will enable future runs + # to start "from scratch" + await self.reset_iterators(event._id) + + if event in awake_events: + self.event_analytics[event._id]["status"] = ( + "success" if success else "failed" + ) + else: + self.event_analytics[event._id]["status"] = "cancelled" + await run_sync(self.compute_analytics_summary, self.event_analytics) + + async def reset_iterators(self, event_id: str): + # Do the same thing as the /reset route + app = self.server_app + if app is None: + raise Exception("Server app has not been set.") + if event_id not in app.iterators: + # Failure, but don't raise an error + return + async with app.lock: + try: + await safe_aclose_iterator(app.iterators[event_id]) + except Exception: + pass + del app.iterators[event_id] + app.iterators_to_reset.add(event_id) + return + + +def process_validation_response( + validation_response: list[dict[str, Any]] | dict[str, Any], + fn: BlockFunction | None = None, +) -> tuple[bool, list[dict[str, Any]]]: + validation_data: list[dict[str, Any]] = [] + + param_names = [] + if fn and fn.fn: + sig = inspect.signature(fn.fn) + param_names = list(sig.parameters.keys()) + + if isinstance(validation_response, list): + for i, data in enumerate(validation_response): + if isinstance(data, dict) and data.get("__type__", None) == "validate": + param_name = ( + param_names[i] if i < len(param_names) else f"parameter_{i}" + ) + data_with_name = {**data, "parameter_name": param_name} + validation_data.append(data_with_name) + else: + validation_data.append({"is_valid": True, "message": ""}) + + elif ( + isinstance(validation_response, dict) + and validation_response.get("is_valid", None) is False + ): + validation_data.append( + validation_response, + ) + else: + validation_data.append({"is_valid": True, "message": ""}) + + return all( + x.get("is_valid", None) is True for x in validation_data + ), validation_data diff --git a/gradio/ranged_response.py b/gradio/ranged_response.py new file mode 100644 index 0000000..5df52a0 --- /dev/null +++ b/gradio/ranged_response.py @@ -0,0 +1,190 @@ +# Taken from https://gist.github.com/kevinastone/a6a62db57577b3f24e8a6865ed311463 +# Context: https://github.com/encode/starlette/pull/1090 +from __future__ import annotations + +import os +import re +import stat +from typing import NamedTuple +from urllib.parse import quote + +from starlette.datastructures import Headers +from starlette.exceptions import HTTPException +from starlette.responses import Response, guess_type # type: ignore +from starlette.staticfiles import StaticFiles +from starlette.types import Receive, Scope, Send + +from gradio._vendor import aiofiles +from gradio._vendor.aiofiles.os import stat as aio_stat + +RANGE_REGEX = re.compile(r"^bytes=(?P\d+)-(?P\d*)$") + + +class ClosedRange(NamedTuple): + start: int + end: int + + def __len__(self) -> int: # type: ignore[override] + return self.end - self.start + 1 + + def __bool__(self) -> bool: + return len(self) > 0 + + +class OpenRange(NamedTuple): + start: int + end: int | None = None + + def clamp(self, start: int, end: int) -> ClosedRange: + begin = max(self.start, start) + end = min(x for x in (self.end, end) if x) + + begin = min(begin, end) + end = max(begin, end) + + return ClosedRange(begin, end) + + +class RangedFileResponse(Response): + chunk_size = 4096 + + def __init__( + self, + path: str | os.PathLike, + range: OpenRange, + headers: dict[str, str] | None = None, + media_type: str | None = None, + filename: str | None = None, + stat_result: os.stat_result | None = None, + method: str | None = None, + ) -> None: + if aiofiles is None: + raise ModuleNotFoundError( + "'aiofiles' must be installed to use FileResponse" + ) + self.path = path + self.range = range + self.filename = filename + self.background = None + self.send_header_only = method is not None and method.upper() == "HEAD" + if media_type is None: + media_type = guess_type(filename or path)[0] or "text/plain" + self.media_type = media_type + self.init_headers(headers or {}) + if self.filename is not None: + content_disposition_filename = quote(self.filename) + if content_disposition_filename != self.filename: + content_disposition = ( + f"attachment; filename*=utf-8''{content_disposition_filename}" + ) + else: + content_disposition = f'attachment; filename="{self.filename}"' + self.headers.setdefault("content-disposition", content_disposition) + self.stat_result = stat_result + + def set_range_headers(self, range: ClosedRange) -> None: + if not self.stat_result: + raise ValueError("No stat result to set range headers with") + total_length = self.stat_result.st_size + content_length = len(range) + self.headers["content-range"] = ( + f"bytes {range.start}-{range.end}/{total_length}" + ) + self.headers["content-length"] = str(content_length) + pass + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # noqa: ARG002 + if self.stat_result is None: + try: + stat_result = await aio_stat(self.path) + self.stat_result = stat_result + except FileNotFoundError as fnfe: + raise RuntimeError( + f"File at path {self.path} does not exist." + ) from fnfe + else: + mode = stat_result.st_mode + if not stat.S_ISREG(mode): + raise RuntimeError(f"File at path {self.path} is not a file.") + + byte_range = self.range.clamp(0, self.stat_result.st_size) + self.set_range_headers(byte_range) + + async with aiofiles.open(self.path, mode="rb") as file: + await file.seek(byte_range.start) + await send( + { + "type": "http.response.start", + "status": 206, + "headers": self.raw_headers, + } + ) + if self.send_header_only: + await send( + {"type": "http.response.body", "body": b"", "more_body": False} + ) + else: + remaining_bytes = len(byte_range) + + if not byte_range: + await send( + {"type": "http.response.body", "body": b"", "more_body": False} + ) + return + + while remaining_bytes > 0: + chunk_size = min(self.chunk_size, remaining_bytes) + chunk = await file.read(chunk_size) + remaining_bytes -= len(chunk) + await send( + { + "type": "http.response.body", + "body": chunk, + "more_body": remaining_bytes > 0, + } + ) + + +class RangedStaticFiles(StaticFiles): + def file_response( + self, + full_path: str | os.PathLike, + stat_result: os.stat_result, + scope: Scope, + status_code: int = 200, + ) -> Response: + request_headers = Headers(scope=scope) + + if request_headers.get("range"): + response = self.ranged_file_response( + full_path, stat_result=stat_result, scope=scope + ) + else: + response = super().file_response( + full_path, stat_result=stat_result, scope=scope, status_code=status_code + ) + response.headers["accept-ranges"] = "bytes" + return response + + def ranged_file_response( + self, + full_path: str | os.PathLike, + stat_result: os.stat_result, + scope: Scope, + ) -> Response: + method = scope["method"] + request_headers = Headers(scope=scope) + + range_header = request_headers["range"] + + match = RANGE_REGEX.search(range_header) + if not match: + raise HTTPException(400) + + start, end = match.group("start"), match.group("end") + + range = OpenRange(int(start), int(end) if end else None) + + return RangedFileResponse( + full_path, range, stat_result=stat_result, method=method + ) diff --git a/gradio/renderable.py b/gradio/renderable.py new file mode 100644 index 0000000..ef8d18c --- /dev/null +++ b/gradio/renderable.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +from collections.abc import Callable, Sequence +from typing import TYPE_CHECKING, Literal, Union, cast + +from gradio_client.documentation import document + +from gradio.blocks import Block +from gradio.components import Component +from gradio.context import Context, LocalContext +from gradio.events import EventListener, EventListenerMethod +from gradio.layouts import Column, Row + +if TYPE_CHECKING: + from gradio.blocks import BlockFunction + from gradio.events import Trigger + + +class Renderable: + def __init__( + self, + fn: Callable, + inputs: Sequence[Component], + triggers: list[tuple[Block | None, str]], + concurrency_limit: int | None | Literal["default"], + concurrency_id: str | None, + trigger_mode: Literal["once", "multiple", "always_last"] | None, + queue: bool, + show_progress: Literal["full", "minimal", "hidden"], + ): + if Context.root_block is None: + raise ValueError("Reactive render must be inside a Blocks context.") + + self._id = len(Context.root_block.renderables) + Context.root_block.renderables.append(self) + self.ContainerClass = Row if isinstance(Context.block, Row) else Column + self.container = self.ContainerClass(show_progress=True) + self.container_id = self.container._id + + self.fn = fn + self.inputs = inputs + self.triggers: list[EventListenerMethod] = [] + self.page = Context.root_block.current_page + self.key_to_id_map: dict[int | str | tuple[str | int, ...], int] = {} + self.render_iteration = 0 + + self.triggers = [EventListenerMethod(*t) for t in triggers] + Context.root_block.default_config.set_event_trigger( + self.triggers, + self.apply, + self.inputs, + self.container, + api_visibility="undocumented", + concurrency_limit=concurrency_limit, + concurrency_id=concurrency_id, + renderable=self, + trigger_mode=trigger_mode, + postprocess=False, + queue=queue, + show_progress=show_progress, + ) + + def apply(self, *args, **kwargs): + blocks_config = LocalContext.blocks_config.get(None) + if blocks_config is None: + raise ValueError("Reactive render must be inside a LocalContext.") + + fns_from_last_render: list[BlockFunction] = [] + for fn in blocks_config.fns.values(): + if fn.rendered_in is self: + fns_from_last_render.append(fn) + + container_copy = self.ContainerClass(render=False, show_progress=True) + container_copy._id = self.container_id + container_copy.page = self.page + LocalContext.renderable.set(self) + LocalContext.key_to_id_map.set(self.key_to_id_map) + + try: + with container_copy: + self.fn(*args, **kwargs) + blocks_config.blocks[self.container_id] = container_copy + blocks_config.attach_load_events(self) + finally: + LocalContext.renderable.set(None) + LocalContext.key_to_id_map.set(None) + + for fn in fns_from_last_render: + # The block-function may already have been removed from `blocks_config.fns` + # during the inner render -- e.g. `gr.Examples` adds and then pops a + # transient "fake_event" function (gradio/helpers.py) for cache-on-launch + # processing. If the inner render never re-registered ``fn``, indexing + # would raise `KeyError`. The cleanup intent is "drop stale fns from the + # previous iteration", so a missing entry is already in the desired state. + current_fn = blocks_config.fns.get(fn._id) + if current_fn is None: + continue + if current_fn.render_iteration != self.render_iteration: + del blocks_config.fns[fn._id] + + self.render_iteration += 1 + + +@document() +def render( + inputs: Sequence[Component] | Component | None = None, + triggers: Sequence[Trigger] | Trigger | None = None, + *, + queue: bool = True, + trigger_mode: Literal["once", "multiple", "always_last"] | None = "always_last", + concurrency_limit: int | None | Literal["default"] = None, + concurrency_id: str | None = None, + show_progress: Literal["full", "minimal", "hidden"] = "full", +): + """ + The render decorator allows Gradio Blocks apps to have dynamic layouts, so that the components and event listeners in your app can change depending on custom logic. + Attaching a @gr.render decorator to a function will cause the function to be re-run whenever the inputs are changed (or specified triggers are activated). The function contains the components and event listeners that will update based on the inputs. + With render, you can:\n + - Show or hide components\n + - Change text or layout\n + - Create components based on what users enter\n + The basic usage of @gr.render is as follows:\n + 1. Create a function and attach the @gr.render decorator to it.\n + 2. Add the input components to the `inputs=` argument of @gr.render, and create a corresponding argument in your function for each component.\n + 3. Add all components inside the function that you want to update based on the inputs. Any event listeners that use these components should also be inside this function.\n + Parameters: + inputs: List of gradio.components to use as inputs. If the function takes no inputs, this should be an empty list. + triggers: List of triggers to listen to, e.g. [btn.click, number.change]. If None, will listen to changes to any inputs. + queue: If True, will place the request on the queue, if the queue has been enabled. If False, will not put this event on the queue, even if the queue has been enabled. If None, will use the queue setting of the gradio app. + trigger_mode: If "once" (default for all events except `.change()`) would not allow any submissions while an event is pending. If set to "multiple", unlimited submissions are allowed while pending, and "always_last" (default for `.change()` and `.key_up()` events) would allow a second submission after the pending event is complete. + concurrency_limit: If set, this is the maximum number of this event that can be running simultaneously. Can be set to None to mean no concurrency_limit (any number of this event can be running simultaneously). Set to "default" to use the default concurrency limit (defined by the `default_concurrency_limit` parameter in `Blocks.queue()`, which itself is 1 by default). + concurrency_id: If set, this is the id of the concurrency group. Events with the same concurrency_id will be limited by the lowest set concurrency_limit. + Example: + import gradio as gr + + with gr.Blocks() as demo: + input_text = gr.Textbox() + + @gr.render(inputs=input_text) + def show_split(text): + if len(text) == 0: + gr.Markdown("## No Input Provided") + else: + for letter in text: + with gr.Row(): + text = gr.Textbox(letter) + btn = gr.Button("Clear") + btn.click(lambda: gr.Textbox(value=""), None, text) + """ + new_triggers = cast(Union[list[EventListener], EventListener, None], triggers) + + if Context.root_block is None: + raise ValueError("Reactive render must be inside a Blocks context.") + + inputs = ( + [inputs] if isinstance(inputs, Component) else [] if inputs is None else inputs + ) + _triggers: list[tuple[Block | None, str]] = [] + if new_triggers is None: + _triggers = [(Context.root_block, "load")] + for input in inputs: + if hasattr(input, "change"): + _triggers.append((input, "change")) + else: + new_triggers = ( + [new_triggers] if isinstance(new_triggers, EventListener) else new_triggers + ) + _triggers = [ + (getattr(t, "__self__", None) if t.has_trigger else None, t.event_name) + for t in new_triggers + ] + + if new_triggers: + for trigger in new_triggers: + if trigger.callback: + trigger.callback(trigger.__self__) # type: ignore + + def wrapper_function(fn): + Renderable( + fn, + inputs, # type: ignore + _triggers, + concurrency_limit, + concurrency_id, + trigger_mode, + queue, + show_progress, + ) + return fn + + return wrapper_function diff --git a/gradio/route_utils.py b/gradio/route_utils.py new file mode 100644 index 0000000..e0ee087 --- /dev/null +++ b/gradio/route_utils.py @@ -0,0 +1,1422 @@ +from __future__ import annotations + +import asyncio +import functools +import hashlib +import hmac +import importlib.resources +import json +import mimetypes +import os +import pickle +import re +import secrets +import shutil +import tempfile +import threading +import unicodedata +import uuid +from collections import defaultdict, deque +from collections.abc import AsyncGenerator, Callable +from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager +from dataclasses import dataclass as python_dataclass +from datetime import datetime +from pathlib import Path +from tempfile import NamedTemporaryFile, _TemporaryFileWrapper +from typing import ( + TYPE_CHECKING, + Any, + BinaryIO, + Union, + cast, +) +from urllib.parse import urlparse + +import anyio +import fastapi +import gradio_client.utils as client_utils +import httpx +import safehttpx +from gradio_client.documentation import document +from python_multipart.multipart import MultipartParser, parse_options_header +from starlette.background import BackgroundTask +from starlette.datastructures import ( + FormData, + Headers, + MutableHeaders, + State, + UploadFile, +) +from starlette.exceptions import HTTPException +from starlette.formparsers import MultiPartException, MultipartPart +from starlette.requests import Request as StarletteRequest +from starlette.responses import ( + FileResponse, + PlainTextResponse, + Response, + StreamingResponse, +) +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +from gradio import processing_utils, utils +from gradio.data_classes import ( + BlocksConfigDict, + DeveloperPath, + MediaStreamChunk, + PredictBody, + PredictBodyInternal, + UserProvidedPath, +) +from gradio.exceptions import Error, InvalidPathError +from gradio.state_holder import SessionState +from gradio.utils import get_package_version + +if TYPE_CHECKING: + from gradio.blocks import BlockFunction, Blocks, BlocksConfig + from gradio.helpers import EventData + from gradio.routes import App + + +config_lock = threading.Lock() +API_PREFIX = "/gradio_api" + + +mimetypes.init() + + +class Obj: + """ + Using a class to convert dictionaries into objects. Used by the `Request` class. + Credit: https://www.geeksforgeeks.org/convert-nested-python-dictionary-to-object/ + """ + + def __init__(self, dict_): + self.__dict__.update(dict_) + for key, value in dict_.items(): + if isinstance(value, (dict, list)): + value = Obj(value) + setattr(self, key, value) + + def __getitem__(self, item): + return self.__dict__[item] + + def __setitem__(self, item, value): + self.__dict__[item] = value + + def __iter__(self): + for key, value in self.__dict__.items(): + if isinstance(value, Obj): + yield (key, dict(value)) + else: + yield (key, value) + + def __contains__(self, item) -> bool: + if item in self.__dict__: + return True + for value in self.__dict__.values(): + if isinstance(value, Obj) and item in value: + return True + return False + + def get(self, item, default=None): + if item in self: + return self.__dict__[item] + return default + + def keys(self): + return self.__dict__.keys() + + def values(self): + return self.__dict__.values() + + def items(self): + return self.__dict__.items() + + def __str__(self) -> str: + return str(self.__dict__) + + def __repr__(self) -> str: + return str(self.__dict__) + + def pop(self, item, default=None): + if item in self: + return self.__dict__.pop(item) + return default + + +@document() +class Request: + """ + A Gradio request object that can be used to access the request headers, cookies, + query parameters and other information about the request from within the prediction + function. The class is a thin wrapper around the fastapi.Request class. Attributes + of this class include: `headers`, `client`, `query_params`, `session_hash`, and `path_params`. If + auth is enabled, the `username` attribute can be used to get the logged in user. In some environments, + the dict-like attributes (e.g. `requests.headers`, `requests.query_params`) of this class are automatically + converted to dictionaries, so we recommend converting them to dictionaries before accessing + attributes for consistent behavior in different environments. + Example: + import gradio as gr + def echo(text, request: gr.Request): + if request: + print("Request headers dictionary:", dict(request.headers)) + print("Query parameters:", dict(request.query_params)) + print("IP address:", request.client.host) + print("Gradio session hash:", request.session_hash) + return text + io = gr.Interface(echo, "textbox", "textbox").launch() + Demos: request_ip_headers + """ + + def __init__( + self, + request: fastapi.Request | None = None, + username: str | None = None, + session_hash: str | None = None, + **kwargs, + ): + """ + Can be instantiated with either a fastapi.Request or by manually passing in + attributes (needed for queueing). + Parameters: + request: A fastapi.Request + username: The username of the logged in user (if auth is enabled) + session_hash: The session hash of the current session. It is unique for each page load. + """ + + self.request = request + self.username = username + self.session_hash: str | None = session_hash + self.kwargs: dict[str, Any] = kwargs + + def dict_to_obj(self, d): + if isinstance(d, dict): + return json.loads(json.dumps(d), object_hook=Obj) + else: + return d + + def __getattr__(self, name: str): + if self.request: + return self.dict_to_obj(getattr(self.request, name)) + else: + try: + obj = self.kwargs[name] + except KeyError as ke: + raise AttributeError( + f"'Request' object has no attribute '{name}'" + ) from ke + return self.dict_to_obj(obj) + + def __getstate__(self) -> dict[str, Any]: + self.kwargs.update( + { + "headers": dict(getattr(self, "headers", {})), + "query_params": dict(getattr(self, "query_params", {})), + "cookies": dict(getattr(self, "cookies", {})), + "path_params": dict(getattr(self, "path_params", {})), + "client": { + "host": getattr(self, "client", {}) and self.client.host, + "port": getattr(self, "client", {}) and self.client.port, + }, + "url": getattr(self, "url", ""), + } + ) + state_obj = getattr(self, "state", None) + if state_obj is not None: + request_state = dict(getattr(state_obj, "_state", {})) + if request_state: + try: + pickle.dumps(request_state) + self.kwargs["request_state"] = request_state + except pickle.PicklingError: + pass + self.request = None + return self.__dict__ + + def __setstate__(self, state: dict[str, Any]): + request_state = state.pop("request_state", None) + self.__dict__ = state + if request_state is not None: + self.state = State(request_state) + + +@document() +class Header(str): + """A string that represents a header value in an incoming HTTP request to the Gradio app. + + When you type a function argument of type `Header`, Gradio will automatically extract that header from the request and pass it to the function. + Note that it's common for header values to use hyphens, e.g. `x-forwarded-host`, and these will automatically be converted to underscores. + So make sure you use underscores in your function arguments. + + Example: + import gradio as gr + + def make_api_request_on_behalf_of_user(prompt: str, x_api_token: gr.Header): + return "Hello from the API" if not x_api_token else "Hello from the API with token!" + + demo = gr.Interface( + make_api_request_on_behalf_of_user, + [ + gr.Textbox(label="Prompt"), + ], + gr.Textbox(label="Response"), + ) + + demo.launch(mcp_server=True) + """ + + pass + + +class FnIndexInferError(Exception): + pass + + +def get_fn(blocks: Blocks, api_name: str | None, body: PredictBody) -> BlockFunction: + if body.session_hash: + session_state = blocks.state_holder[body.session_hash] + fns = session_state.blocks_config.fns + else: + fns = blocks.fns + + if body.fn_index is None: + if api_name is not None: + for fn in fns.values(): + if fn.api_name == api_name: + return fn + raise FnIndexInferError( + f"Could not infer function index for API name: {api_name}" + ) + else: + return fns[body.fn_index] + + +def compile_gr_request( + body: PredictBodyInternal, + fn: BlockFunction, + username: str | None, + request: fastapi.Request | None, +): + # If this fn_index cancels jobs, then the only input we need is the + # current session hash + if fn.cancels: + body.data = [body.session_hash] + if body.request: + if body.batched: + gr_request = [ + Request( + username=username, + request=body.request, + session_hash=body.session_hash, + ) + ] + else: + gr_request = Request( + username=username, request=body.request, session_hash=body.session_hash + ) + else: + if request is None: + raise ValueError("request must be provided if body.request is None") + gr_request = Request( + username=username, request=request, session_hash=body.session_hash + ) + + return gr_request + + +def restore_session_state(app: App, body: PredictBodyInternal): + event_id = body.event_id + session_hash = getattr(body, "session_hash", None) + if session_hash is not None: + session_state = app.state_holder[session_hash] + # The should_reset set keeps track of the fn_indices + # that have been cancelled. When a job is cancelled, + # the /reset route will mark the jobs as having been reset. + # That way if the cancel job finishes BEFORE the job being cancelled + # the job being cancelled will not overwrite the state of the iterator. + if event_id is None: + iterator = None + elif event_id in app.iterators_to_reset: + iterator = None + app.iterators_to_reset.remove(event_id) + else: + iterator = app.iterators.get(event_id) + else: + session_state = SessionState(app.get_blocks()) + iterator = None + + return session_state, iterator + + +def prepare_event_data( + blocks_config: BlocksConfig, + body: PredictBodyInternal, +) -> EventData: + from gradio.helpers import EventData + + target = body.trigger_id + event_data = EventData( + blocks_config.blocks.get(target) if target else None, + body.event_data, + ) + # Set parent to None to avoid pickle issues in ZeroGPU + # See https://github.com/gradio-app/gradio/issues/11551 + if hasattr(event_data.target, "parent"): + event_data.target.parent = None # type: ignore + return event_data + + +async def call_process_api( + app: App, + body: PredictBodyInternal, + gr_request: Union[Request, list[Request]], + fn: BlockFunction, + root_path: str, +): + session_state, iterator = restore_session_state(app=app, body=body) + + event_data = prepare_event_data(session_state.blocks_config, body) + event_id = body.event_id + + session_hash = getattr(body, "session_hash", None) + inputs = body.data + + batch_in_single_out = not body.batched and fn.batch + if batch_in_single_out: + inputs = [inputs] + + try: + from gradio.profiling import trace_phase + + with utils.MatplotlibBackendMananger(): + async with trace_phase("total"): + output = await app.get_blocks().process_api( + block_fn=fn, + inputs=inputs, + request=gr_request, + state=session_state, + iterator=iterator, + session_hash=session_hash, + event_id=event_id, + event_data=event_data, + in_event_listener=True, + simple_format=body.simple_format, + root_path=root_path, + ) + iterator = output.pop("iterator", None) + if event_id is not None: + app.iterators[event_id] = iterator # type: ignore + if isinstance(output, Error): + raise output + except BaseException: + iterator = app.iterators.get(event_id) if event_id is not None else None + if iterator is not None: # close off any streams that are still open + run_id = id(iterator) + pending_streams: dict[int, MediaStream] = ( + app.get_blocks().pending_streams[session_hash].get(run_id, {}) + ) + for stream in pending_streams.values(): + stream.end_stream() + raise + + if batch_in_single_out: + output["data"] = output["data"][0] + return output + + +def get_first_header_value(request: fastapi.Request, header_name: str): + header_value = request.headers.get(header_name) + if header_value: + return header_value.split(",")[0].strip() + return None + + +def get_request_origin(request: fastapi.Request, route_path: str) -> httpx.URL: + """ + Examines the request headers to determine the origin of the request. + If the request includes the x-forwarded-host header, it is used directly to determine the origin. + Otherwise, the request url is used and the route path is stripped off. + + The returned URL is a httpx.URL object without a trailing slash, e.g. "https://example.com" + """ + + x_forwarded_host = get_first_header_value(request, "x-forwarded-host") + x_gradio_server = get_first_header_value(request, "x-gradio-server") + root_url = ( + f"http://{x_forwarded_host}" + if x_forwarded_host + else str(x_gradio_server or request.url) + ) + root_url = httpx.URL(root_url) + root_url = root_url.copy_with(query=None) + root_url = str(root_url).rstrip("/") + + if get_first_header_value(request, "x-forwarded-proto") == "https": + root_url = root_url.replace("http://", "https://") + + route_path = route_path.rstrip("/") + + if len(route_path) > 0 and not x_forwarded_host and root_url.endswith(route_path): + root_url = root_url[: -len(route_path)] + + root_url = root_url.rstrip("/") + root_url = httpx.URL(root_url) + + return root_url + + +def get_api_call_path(request: fastapi.Request) -> str: + """ + Extracts the API call path from the request URL. + + If the URL (without query parameters) ends with "{API_PREFIX}/queue/join", that exact path is returned. + Otherwise, if the URL contains "{API_PREFIX}/call", the substring starting from "{API_PREFIX}/call" is returned. + This allows for dynamic API calls to methods other than "predict". + + Raises: + ValueError: If the request URL does not match any recognized API call pattern. + """ + queue_api_url = f"{API_PREFIX}/queue/join" + generic_api_url = f"{API_PREFIX}/call" + request_path = request.url.path.rstrip("/") + + if request_path.endswith(queue_api_url): + return queue_api_url + + start_index = request_path.rfind(generic_api_url) + if start_index >= 0: + return request_path[start_index : len(request_path)] + + raise ValueError( + f"Request url '{str(request.url)}' has an unknown api call pattern." + ) + + +def get_root_url( + request: fastapi.Request, route_path: str, root_path: str | None +) -> str: + """ + Gets the root url of the Gradio app (i.e. the public url of the app) without a trailing slash. + + This is how the root_url is resolved: + 1. If a user provides a `root_path` manually that is a full URL, it is returned directly. + 2. If the request has an x-forwarded-host header (e.g. because it is behind a proxy), the root url is + constructed from the x-forwarded-host header. In this case, `route_path` is not used to construct the root url. + 3. Otherwise, the root url is constructed from the request url. The query parameters and `route_path` are stripped off. + And if a relative `root_path` is provided, and it is not already the subpath of the URL, it is appended to the root url. + + In cases (2) and (3), We also check to see if the x-forwarded-proto header is present, and if so, convert the root url to https. + And if there are multiple hosts in the x-forwarded-host or multiple protocols in the x-forwarded-proto, the first one is used. + """ + + if root_path and client_utils.is_http_url_like(root_path): + return root_path.rstrip("/") + + root_url = get_request_origin(request, route_path) + + if root_path and root_url.path != root_path: + root_url = root_url.copy_with(path=root_path) + + return str(root_url).rstrip("/") + + +def _user_safe_decode(src: bytes, codec: str) -> str: + try: + return src.decode(codec) + except (UnicodeDecodeError, LookupError): + return src.decode("latin-1") + + +class GradioUploadFile(UploadFile): + """UploadFile with a sha attribute.""" + + def __init__( + self, + file: BinaryIO, + *, + size: int | None = None, + filename: str | None = None, + headers: Headers | None = None, + ) -> None: + super().__init__(file, size=size, filename=filename, headers=headers) + self.sha = hashlib.sha256() + self.sha.update(processing_utils.hash_seed) + + +@python_dataclass(frozen=True) +class FileUploadProgressUnit: + filename: str + chunk_size: int + + +@python_dataclass +class FileUploadProgressTracker: + deque: deque[FileUploadProgressUnit] + is_done: bool + + +class FileUploadProgressNotTrackedError(Exception): + pass + + +class FileUploadProgressNotQueuedError(Exception): + pass + + +class FileUploadProgress: + def __init__(self) -> None: + self._statuses: dict[str, FileUploadProgressTracker] = {} + self._signals = defaultdict(asyncio.Event) + + def track(self, upload_id: str): + if upload_id not in self._statuses: + self._statuses[upload_id] = FileUploadProgressTracker(deque(), False) + self._signals[upload_id].set() + + async def is_tracked(self, upload_id: str) -> bool: + return await self._signals[upload_id].wait() + + def append(self, upload_id: str, filename: str, message_bytes: bytes): + if upload_id not in self._statuses: + self.track(upload_id) + queue = self._statuses[upload_id].deque + + if len(queue) == 0: + queue.append(FileUploadProgressUnit(filename, len(message_bytes))) + else: + last_unit = queue.popleft() + if last_unit.filename != filename: + queue.append(FileUploadProgressUnit(filename, len(message_bytes))) + else: + queue.append( + FileUploadProgressUnit( + filename, + last_unit.chunk_size + len(message_bytes), + ) + ) + + def set_done(self, upload_id: str): + if upload_id not in self._statuses: + self.track(upload_id) + self._statuses[upload_id].is_done = True + + def is_done(self, upload_id: str): + if upload_id not in self._statuses: + raise FileUploadProgressNotTrackedError() + return self._statuses[upload_id].is_done + + def stop_tracking(self, upload_id: str): + if upload_id in self._statuses: + del self._statuses[upload_id] + + def pop(self, upload_id: str) -> FileUploadProgressUnit: + if upload_id not in self._statuses: + raise FileUploadProgressNotTrackedError() + try: + return self._statuses[upload_id].deque.pop() + except IndexError as e: + raise FileUploadProgressNotQueuedError() from e + + +class GradioMultiPartParser: + """Vendored from starlette.MultipartParser. + + Thanks starlette! + + Made the following modifications + - Use GradioUploadFile instead of UploadFile + - Use NamedTemporaryFile instead of SpooledTemporaryFile + - Compute hash of data as the request is streamed + + """ + + max_header_size = 1024 * 8 + + def __init__( + self, + headers: Headers, + stream: AsyncGenerator[bytes, None], + *, + max_files: Union[int, float] = 1000, + max_fields: Union[int, float] = 1000, + upload_id: str | None = None, + upload_progress: FileUploadProgress | None = None, + max_file_size: int | float, + max_header_size: int = max_header_size, + ) -> None: + self.headers = headers + self.stream = stream + self.max_files = max_files + self.max_fields = max_fields + self.items: list[tuple[str, Union[str, UploadFile]]] = [] + self.upload_id = upload_id + self.upload_progress = upload_progress + self._current_files = 0 + self._current_fields = 0 + self.max_file_size = max_file_size + self.max_header_size = max_header_size + self._current_partial_header_name: bytes = b"" + self._current_partial_header_value: bytes = b"" + self._current_header_size: int = 0 + self._current_part = MultipartPart() + self._charset = "" + self._file_parts_to_write: list[tuple[MultipartPart, bytes]] = [] + self._file_parts_to_finish: list[MultipartPart] = [] + self._files_to_close_on_error: list[_TemporaryFileWrapper] = [] + + def on_part_begin(self) -> None: + self._current_part = MultipartPart() + self._current_header_size = 0 + + def on_part_data(self, data: bytes, start: int, end: int) -> None: + message_bytes = data[start:end] + if self.upload_progress is not None: + self.upload_progress.append( + self.upload_id, # type: ignore + self._current_part.file.filename, # type: ignore + message_bytes, + ) + if self._current_part.file is None: + self._current_part.data += message_bytes + else: + self._file_parts_to_write.append((self._current_part, message_bytes)) + + def on_part_end(self) -> None: + if self._current_part.file is None: + data = self._current_part.data + data_bytes = bytes(data) if isinstance(data, bytearray) else data + self.items.append( + ( + self._current_part.field_name, + _user_safe_decode(data_bytes, str(self._charset)), + ) + ) + else: + self._file_parts_to_finish.append(self._current_part) + # The file can be added to the items right now even though it's not + # finished yet, because it will be finished in the `parse()` method, before + # self.items is used in the return value. + self.items.append((self._current_part.field_name, self._current_part.file)) + + def _check_header_size(self, additional_bytes: int): + if self._current_header_size + additional_bytes > self.max_header_size: + raise MultiPartException( + f"Headers exceeded maximum allowed size of {self.max_header_size} bytes." + ) + + def on_header_field(self, data: bytes, start: int, end: int) -> None: + additional_header_bytes = end - start + self._check_header_size(additional_header_bytes) + self._current_partial_header_name += data[start:end] + self._current_header_size += additional_header_bytes + + def on_header_value(self, data: bytes, start: int, end: int) -> None: + additional_header_bytes = end - start + self._check_header_size(additional_header_bytes) + self._current_partial_header_value += data[start:end] + self._current_header_size += additional_header_bytes + + def on_header_end(self) -> None: + field = self._current_partial_header_name.lower() + if field == b"content-disposition": + self._current_part.content_disposition = self._current_partial_header_value + self._current_part.item_headers.append( + (field, self._current_partial_header_value) + ) + self._current_partial_header_name = b"" + self._current_partial_header_value = b"" + + def on_headers_finished(self) -> None: + _, options = parse_options_header(self._current_part.content_disposition or b"") + try: + self._current_part.field_name = _user_safe_decode( + options[b"name"], str(self._charset) + ) + except KeyError as e: + raise MultiPartException( + 'The Content-Disposition header field "name" must be provided.' + ) from e + if b"filename" in options: + self._current_files += 1 + if self._current_files > self.max_files: + raise MultiPartException( + f"Too many files. Maximum number of files is {self.max_files}." + ) + filename = _user_safe_decode(options[b"filename"], str(self._charset)) + tempfile = NamedTemporaryFile(delete=False) + self._files_to_close_on_error.append(tempfile) + self._current_part.file = GradioUploadFile( + file=tempfile, # type: ignore[arg-type] + size=0, + filename=filename, + headers=Headers(raw=self._current_part.item_headers), + ) + else: + self._current_fields += 1 + if self._current_fields > self.max_fields: + raise MultiPartException( + f"Too many fields. Maximum number of fields is {self.max_fields}." + ) + self._current_part.file = None + + def on_end(self) -> None: + pass + + async def parse(self) -> FormData: + # Parse the Content-Type header to get the multipart boundary. + _, params = parse_options_header(self.headers["Content-Type"]) + charset = params.get(b"charset", "utf-8") + if isinstance(charset, bytes): + charset = charset.decode("latin-1") + self._charset = charset + try: + boundary = params[b"boundary"] + except KeyError as e: + raise MultiPartException("Missing boundary in multipart.") from e + + # Callbacks dictionary. + callbacks = { + "on_part_begin": self.on_part_begin, + "on_part_data": self.on_part_data, + "on_part_end": self.on_part_end, + "on_header_field": self.on_header_field, + "on_header_value": self.on_header_value, + "on_header_end": self.on_header_end, + "on_headers_finished": self.on_headers_finished, + "on_end": self.on_end, + } + + # Create the parser. + parser = MultipartParser(boundary, callbacks) # type: ignore + try: + # Feed the parser with data from the request. + async for chunk in self.stream: + parser.write(chunk) + # Write file data, it needs to use await with the UploadFile methods + # that call the corresponding file methods *in a threadpool*, + # otherwise, if they were called directly in the callback methods above + # (regular, non-async functions), that would block the event loop in + # the main thread. + for part, data in self._file_parts_to_write: + assert part.file # for type checkers # noqa: S101 + if (part.file.size or 0) + len(data) > self.max_file_size: + if self.upload_progress is not None: + self.upload_progress.set_done(self.upload_id) # type: ignore + raise MultiPartException( + f"File size exceeded maximum allowed size of {self.max_file_size} bytes." + ) + await part.file.write(data) + part.file.sha.update(data) # type: ignore + for part in self._file_parts_to_finish: + assert part.file # for type checkers # noqa: S101 + await part.file.seek(0) + self._file_parts_to_write.clear() + self._file_parts_to_finish.clear() + except MultiPartException as exc: + # Close all the files if there was an error. + for file in self._files_to_close_on_error: + file.close() + Path(file.name).unlink() + raise exc + + parser.finalize() + if self.upload_progress is not None: + self.upload_progress.set_done(self.upload_id) # type: ignore + return FormData(self.items) + + +def move_uploaded_files_to_cache(files: list[str], destinations: list[str]) -> None: + for file, dest in zip(files, destinations, strict=False): + shutil.move(file, dest) + + +def update_root_in_config(config: BlocksConfigDict, root: str) -> BlocksConfigDict: + """ + Updates the root "key" in the config dictionary to the new root url. If the + root url has changed, all of the urls in the config that correspond to component + file urls are updated to use the new root url. + """ + previous_root = config.get("root") + if previous_root is None or previous_root != root: + config["root"] = root + config = processing_utils.add_root_url(config, root, previous_root) # type: ignore + return config + + +def update_example_values_to_use_public_url(api_info: dict[str, Any]) -> dict[str, Any]: + """ + Updates the example values in the api_info dictionary to use a public url + """ + + def _add_root_url(file_dict: dict): + default_value = file_dict.get("parameter_default") + if default_value is not None and client_utils.is_file_obj_with_url( + default_value + ): + if client_utils.is_http_url_like(default_value["url"]): + return file_dict + # If the default value's url is not already a full public url, + # we use the example_input url. This makes it so that the example + # value for images, audio, and video components pass SSRF checks. + default_value["url"] = file_dict["example_input"]["url"] + return file_dict + + return client_utils.traverse( + api_info, + _add_root_url, + lambda d: isinstance(d, dict) and "parameter_default" in d, + ) + + +def compare_passwords_securely(input_password: str, correct_password: str) -> bool: + return hmac.compare_digest(input_password.encode(), correct_password.encode()) + + +def starts_with_protocol(string: str) -> bool: + """This regex matches strings that start with a scheme (one or more characters not including colon, slash, or space) + followed by ://, or start with just //, \\/, /\\, or \\ as they are interpreted as SMB paths on Windows. + """ + pattern = r"^(?:[a-zA-Z][a-zA-Z0-9+\-.]*://|//|\\\\|\\/|/\\)" + return re.match(pattern, string) is not None + + +def get_hostname(url: str) -> str: + """ + Returns the hostname of a given url, or an empty string if the url cannot be parsed. + Examples: + get_hostname("https://www.gradio.app") -> "www.gradio.app" + get_hostname("localhost:7860") -> "localhost" + get_hostname("127.0.0.1") -> "127.0.0.1" + """ + if not url: + return "" + if "://" not in url: + url = "http://" + url + try: + return urlparse(url).hostname or "" + except Exception: + return "" + + +class CustomCORSMiddleware: + # This is a modified version of the Starlette CORSMiddleware that restricts the allowed origins when the host is localhost. + # Adapted from: https://github.com/encode/starlette/blob/89fae174a1ea10f59ae248fe030d9b7e83d0b8a0/starlette/middleware/cors.py + + def __init__( + self, + app: ASGIApp, + strict_cors: bool = True, + ) -> None: + self.app = app + self.all_methods = ("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT") + self.preflight_headers = { + "Access-Control-Allow-Methods": ", ".join(self.all_methods), + "Access-Control-Max-Age": str(600), + "Access-Control-Allow-Credentials": "true", + } + self.simple_headers = {"Access-Control-Allow-Credentials": "true"} + # Any of these hosts suggests that the Gradio app is running locally. + self.localhost_aliases = ["localhost", "127.0.0.1", "0.0.0.0"] + if not strict_cors or os.getenv("GRADIO_LOCAL_DEV_MODE") is not None: # type: ignore + # Note: "null" is a special case that happens if a Gradio app is running + # as an embedded web component in a local static webpage. However, it can + # also be used maliciously for CSRF attacks, so it is not allowed by default. + self.localhost_aliases.append("null") + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + headers = Headers(scope=scope) + origin = headers.get("origin") + if origin is None: + await self.app(scope, receive, send) + return + if scope["method"] == "OPTIONS" and "access-control-request-method" in headers: + response = self.preflight_response(request_headers=headers) + await response(scope, receive, send) + return + await self.simple_response(scope, receive, send, request_headers=headers) + + def preflight_response(self, request_headers: Headers) -> Response: + headers = dict(self.preflight_headers) + origin = request_headers["Origin"] + if self.is_valid_origin(request_headers): + headers["Access-Control-Allow-Origin"] = origin + requested_headers = request_headers.get("access-control-request-headers") + if requested_headers is not None: + headers["Access-Control-Allow-Headers"] = requested_headers + return PlainTextResponse("OK", status_code=200, headers=headers) + + async def simple_response( + self, scope: Scope, receive: Receive, send: Send, request_headers: Headers + ) -> None: + send = functools.partial(self._send, send=send, request_headers=request_headers) + await self.app(scope, receive, send) + + async def _send( + self, message: Message, send: Send, request_headers: Headers + ) -> None: + if message["type"] != "http.response.start": + await send(message) + return + message.setdefault("headers", []) + headers = MutableHeaders(scope=message) + headers.update(self.simple_headers) + origin = request_headers["Origin"] + if self.is_valid_origin(request_headers): + self.allow_explicit_origin(headers, origin) + await send(message) + + def is_valid_origin(self, request_headers: Headers) -> bool: + origin = request_headers["Origin"] + host = request_headers["Host"] + host_name = get_hostname(host) + origin_name = get_hostname(origin) + + return ( + host_name not in self.localhost_aliases + or origin_name in self.localhost_aliases + ) + + @staticmethod + def allow_explicit_origin(headers: MutableHeaders, origin: str) -> None: + headers["Access-Control-Allow-Origin"] = origin + headers.add_vary_header("Origin") + + +def delete_files_created_by_app(blocks: Blocks, age: int | None) -> None: + """Delete files that are older than age. If age is None, delete all files.""" + dont_delete = set() + + for component in blocks.blocks.values(): + dont_delete.update(getattr(component, "keep_in_cache", set())) + for temp_set in blocks.temp_file_sets: + # We use a copy of the set to avoid modifying the set while iterating over it + # otherwise we would get an exception: Set changed size during iteration + to_remove = set() + for file in temp_set: + if file in dont_delete: + continue + try: + file_path = Path(file) + modified_time = datetime.fromtimestamp(file_path.lstat().st_ctime) + if ( + age is None + or (datetime.now() - modified_time).total_seconds() > age + ): + os.remove(file) + to_remove.add(file) + except FileNotFoundError: + continue + temp_set -= to_remove + + +async def delete_files_on_schedule(app: App, frequency: int, age: int) -> None: + """Startup task to delete files created by the app based on time since last modification.""" + while True: + await asyncio.sleep(frequency) + await anyio.to_thread.run_sync( + delete_files_created_by_app, app.get_blocks(), age + ) + + +@asynccontextmanager +async def _lifespan_handler( + app: App, frequency: int = 1, age: int = 1 +) -> AsyncGenerator: + """A context manager that triggers the startup and shutdown events of the app.""" + asyncio.create_task(delete_files_on_schedule(app, frequency, age)) + yield + delete_files_created_by_app(app.get_blocks(), age=None) + + +async def _delete_state(app: App): + """Delete all expired state every second.""" + while True: + app.state_holder.delete_all_expired_state() + await asyncio.sleep(1) + + +@asynccontextmanager +async def _delete_state_handler(app: App): + """When the server launches, regularly delete expired state.""" + asyncio.create_task(_delete_state(app)) + yield + + +def create_lifespan_handler( + user_lifespan: Callable[[App], AbstractAsyncContextManager] | None, + frequency: int | None = 1, + age: int | None = 1, +) -> Callable[[App], AbstractAsyncContextManager]: + """Return a context manager that applies _lifespan_handler and user_lifespan if it exists.""" + + @asynccontextmanager + async def _handler(app: App): + state = None + async with AsyncExitStack() as stack: + await stack.enter_async_context(_delete_state_handler(app)) + if frequency and age: + await stack.enter_async_context(_lifespan_handler(app, frequency, age)) + if user_lifespan is not None: + state = await stack.enter_async_context(user_lifespan(app)) + yield state + + return _handler + + +class MediaStream: + def __init__(self, desired_output_format: str | None = None): + self.segments: list[MediaStreamChunk] = [] + self.combined_file: str | None = None + self.ended = False + self.segment_index = 0 + self.playlist = "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT\n#EXT-X-TARGETDURATION:10\n#EXT-X-VERSION:4\n#EXT-X-MEDIA-SEQUENCE:0\n" + self.max_duration = 5 + self.desired_output_format = desired_output_format + + async def add_segment(self, data: MediaStreamChunk | None): + if not data: + return + + segment_id = str(uuid.uuid4()) + self.segments.append({"id": segment_id, **data}) # type: ignore + self.max_duration = max(self.max_duration, data["duration"]) + 1 + + def end_stream(self): + self.ended = True + + +def create_url_safe_hash(data: bytes, digest_size=8): + """Create a URL-safe short hash of the data. Used to generate unique short deep links.""" + import base64 + + hash_obj = hashlib.blake2b(data, digest_size=digest_size, usedforsecurity=False) + url_safe_hash = base64.urlsafe_b64encode(hash_obj.digest()).decode().rstrip("=") + + return url_safe_hash + + +def slugify(value): + """ + Convert to ASCII. Convert spaces or repeated dashes to single dashes. + Remove characters that aren't alphanumerics, underscores, or hyphens. + Convert to lowercase. Also strip leading and trailing whitespace, + dashes, and underscores. + """ + value = str(value) + value = ( + unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii") + ) + value = re.sub(r"[^\w\s-]", "", value.lower()) + return re.sub(r"[-\s]+", "-", value).strip("-_") + + +# Prefixes that identify "dumb" routes safe to proxy to static workers. +# These routes serve files, static assets, or handle uploads — no queue/state needed. +STATIC_ROUTE_PREFIXES = ( + "/svelte/", + "/static/", + "/assets/", + "/favicon.ico", + "/file=", + "/file/", + "/upload", + "/custom_component/", +) + + +def routes_safe_join(directory: DeveloperPath, path: UserProvidedPath) -> str: + """Safely join the user path to the directory while performing some additional http-related checks, + e.g. ensuring that the full path exists on the local file system and is not a directory + """ + if path == "": + raise fastapi.HTTPException(400) + if starts_with_protocol(path): + raise fastapi.HTTPException(403) + try: + fullpath = Path(utils.safe_join(directory, path)) + except InvalidPathError as e: + raise fastapi.HTTPException(403) from e + if fullpath.is_dir(): + raise fastapi.HTTPException(403) + if not fullpath.exists(): + raise fastapi.HTTPException(404) + return str(fullpath) + + +STATIC_TEMPLATE_LIB = cast( + DeveloperPath, + importlib.resources.files("gradio").joinpath("templates").as_posix(), # type: ignore +) +STATIC_PATH_LIB = cast( + DeveloperPath, + importlib.resources.files("gradio") + .joinpath("templates/frontend/static") + .as_posix(), # type: ignore +) +BUILD_PATH_LIB = cast( + DeveloperPath, + importlib.resources.files("gradio") + .joinpath("templates/frontend/assets") + .as_posix(), # type: ignore +) +VERSION = get_package_version() +XSS_SAFE_MIMETYPES = { + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "audio/mpeg", + "audio/wav", + "audio/ogg", + "video/mp4", + "video/webm", + "video/ogg", + "text/plain", + "application/json", +} + +DEFAULT_TEMP_DIR = os.environ.get("GRADIO_TEMP_DIR") or str( + Path(tempfile.gettempdir()) / "gradio" +) + + +def file_response(developer_path: DeveloperPath, user_path: UserProvidedPath): + return FileResponse(routes_safe_join(developer_path, user_path)) + + +def favicon(favicon_path: str | Path | None = None): + if favicon_path is None: + return file_response(STATIC_PATH_LIB, UserProvidedPath("img/logo.svg")) + else: + return FileResponse(favicon_path) + + +_FILE_STREAM_MAX_REDIRECTS = 20 +_FILE_STREAM_PASSTHROUGH_HEADERS = ( + "content-length", + "content-range", + "accept-ranges", + "last-modified", + "etag", +) + + +async def secure_url_stream_response(url: str, request: StarletteRequest): + """ + SSRF-safe way to serve an http(s) URL from the `/gradio_api/file=` + endpoint. Instead of redirecting the caller to `url` (an open redirect, and + a client-side SSRF vector because `gradio_client` follows redirects), the + server fetches `url` itself through `safehttpx` — which resolves the host, + confirms it maps to a public IP, and pins that IP for the connection so a DNS + rebind cannot swap in an internal address — and streams the bytes back, + re-validating every redirect hop. Hosts that are unresolvable or resolve to a + private / loopback / link-local / reserved address are rejected. See #13593. + """ + forward_headers = {} + range_header = request.headers.get("Range") + if range_header: + forward_headers["Range"] = range_header + + current_url = url + redirects = 0 + while True: + try: + parsed = httpx.URL(current_url) + except Exception as e: + raise HTTPException(403, f"File not allowed: {url}.") from e + if parsed.scheme not in ("http", "https") or not parsed.host: + raise HTTPException(403, f"File not allowed: {url}.") + try: + verified_ip = await safehttpx.async_validate_url(parsed.host) + except Exception as e: + raise HTTPException(403, f"File not allowed: {url}.") from e + + transport = safehttpx.AsyncSecureTransport(verified_ip) + client = httpx.AsyncClient( + transport=transport, timeout=httpx.Timeout(None, connect=10.0) + ) + try: + req = client.build_request( + request.method, current_url, headers=forward_headers + ) + upstream = await client.send(req, stream=True) + except Exception as e: + await client.aclose() + raise HTTPException(502, f"Could not fetch file: {url}.") from e + + if upstream.has_redirect_location: + location = upstream.headers.get("location", "") + await upstream.aclose() + await client.aclose() + redirects += 1 + if redirects > _FILE_STREAM_MAX_REDIRECTS or not location: + raise HTTPException(502, f"Could not fetch file: {url}.") + current_url = str(httpx.URL(current_url).join(location)) + continue + + upstream_mime = upstream.headers.get("content-type", "").split(";")[0].strip() + guessed_mime, _ = mimetypes.guess_type(current_url) + if upstream_mime in XSS_SAFE_MIMETYPES or guessed_mime in XSS_SAFE_MIMETYPES: + content_type = upstream_mime or guessed_mime or "application/octet-stream" + content_disposition = "inline" + else: + content_type = "application/octet-stream" + content_disposition = "attachment" + + response_headers = { + "Content-Type": content_type, + "Content-Disposition": content_disposition, + "X-Content-Type-Options": "nosniff", + } + for header in _FILE_STREAM_PASSTHROUGH_HEADERS: + if header in upstream.headers: + response_headers[header] = upstream.headers[header] + + async def _close(upstream=upstream, client=client) -> None: + await upstream.aclose() + await client.aclose() + + return StreamingResponse( + upstream.aiter_raw(), + status_code=upstream.status_code, + headers=response_headers, + background=BackgroundTask(_close), + ) + + +def file_fetch( + path_or_url, + request, + blocks_or_config, + upload_dir, +): + # NOTE: http(s) URLs are intentionally NOT handled here. Previously this + # returned a 302 redirect to the URL, which was an open redirect and — since + # `gradio_client` follows redirects — a client-side SSRF vector (#13593). + # They are now served by `secure_url_stream_response()` from the async route, + # which fetches through `safehttpx` (public-IP validated, IP-pinned) and + # streams the bytes back. Any http(s) URL reaching this sync path falls + # through to the `starts_with_protocol` guard below and is rejected. + if starts_with_protocol(path_or_url): + raise HTTPException(403, f"File not allowed: {path_or_url}.") + + abs_path = utils.abspath(path_or_url) + try: + if abs_path.is_dir() or not abs_path.exists(): + raise HTTPException(403, f"File not allowed: {path_or_url}.") + except Exception as e: + raise HTTPException(403, f"File not allowed: {path_or_url}.") from e + + from gradio.data_classes import _StaticFiles + + allowed, reason = utils.is_allowed_file( + abs_path, + blocked_paths=blocks_or_config.blocked_paths, + allowed_paths=blocks_or_config.allowed_paths + _StaticFiles.all_paths, + created_paths=[upload_dir, str(utils.get_cache_folder())], + ) + if not allowed: + raise HTTPException(403, f"File not allowed: {path_or_url}.") + + mime_type, _ = mimetypes.guess_type(abs_path) + if mime_type in XSS_SAFE_MIMETYPES or reason == "allowed": + media_type = mime_type or "application/octet-stream" + content_disposition_type = "inline" + else: + media_type = "application/octet-stream" + content_disposition_type = "attachment" + + range_val = request.headers.get("Range", "").strip() + if range_val.startswith("bytes=") and "-" in range_val: + range_val = range_val[6:] + start, end = range_val.split("-") + if start.isnumeric() and end.isnumeric(): + from gradio import ranged_response # type: ignore + + start = int(start) + end = int(end) + headers = dict(request.headers) + headers["Content-Disposition"] = content_disposition_type + headers["Content-Type"] = media_type + return ranged_response.RangedFileResponse( + abs_path, + ranged_response.OpenRange(start, end), + headers, + stat_result=os.stat(abs_path), + ) + + return FileResponse( + abs_path, + headers={"Accept-Ranges": "bytes"}, + content_disposition_type=content_disposition_type, + media_type=media_type, + filename=abs_path.name, + ) + + +async def upload_fn( + request: StarletteRequest, + upload_dir, + max_file_size, + upload_id, + force_move: bool = True, + upload_progress: FileUploadProgress | None = None, +): + content_type_header = request.headers.get("Content-Type") + content_type: bytes + content_type, _ = parse_options_header(content_type_header or "") + if content_type != b"multipart/form-data": + raise HTTPException(status_code=400, detail="Invalid content type.") + + if upload_id and upload_progress: + upload_progress.track(upload_id) + + multipart_parser = GradioMultiPartParser( + request.headers, + request.stream(), + max_files=1000, + max_fields=1000, + max_file_size=max_file_size, + upload_id=upload_id, + upload_progress=upload_progress, + ) + form = await multipart_parser.parse() + + output_files = [] + files_to_copy = [] + locations = [] + for temp_file in form.getlist("files"): + if not isinstance(temp_file, GradioUploadFile): + raise TypeError("File is not an instance of GradioUploadFile") + if temp_file.filename: + file_name = Path(temp_file.filename).name + name = client_utils.strip_invalid_filename_characters(file_name) + else: + name = f"tmp{secrets.token_hex(5)}" + directory = Path(upload_dir) / temp_file.sha.hexdigest() + directory.mkdir(exist_ok=True, parents=True) + try: + dest = utils.safe_join( + DeveloperPath(str(directory)), UserProvidedPath(name) + ) + except InvalidPathError as err: + raise HTTPException( + status_code=400, detail=f"Invalid file name: {name}" + ) from err + temp_file.file.close() + try: + os.rename(temp_file.file.name, dest) + except OSError: + if force_move: + import shutil + + shutil.move(temp_file.file.name, dest) + else: + files_to_copy.append(temp_file.file.name) + locations.append(dest) + output_files.append(dest) + + return output_files, files_to_copy, locations diff --git a/gradio/routes.py b/gradio/routes.py new file mode 100644 index 0000000..a3d901b --- /dev/null +++ b/gradio/routes.py @@ -0,0 +1,2639 @@ +"""Implements a FastAPI server to run the gradio interface. Note that some types in this +module use the Optional/Union notation so that they work correctly with pydantic.""" + +from __future__ import annotations + +import asyncio +import contextlib +import hashlib +import inspect +import io +import json +import math +import mimetypes +import os +import secrets +import sys +import time +import traceback +import warnings +from collections.abc import AsyncIterator, Callable, Sequence +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Union, + cast, +) + +import fastapi +import httpx +import markupsafe +import orjson +from fastapi import ( + APIRouter, + BackgroundTasks, + Body, + Depends, + FastAPI, + HTTPException, + status, +) +from fastapi.responses import ( + FileResponse, + HTMLResponse, + JSONResponse, + PlainTextResponse, + Response, + StreamingResponse, +) +from fastapi.security import OAuth2PasswordRequestForm +from fastapi.templating import Jinja2Templates +from gradio_client import utils as client_utils +from gradio_client.documentation import document +from gradio_client.snippet import generate_code_snippets +from gradio_client.utils import ServerMessage +from hf_gradio.cli import _condense_info, generate_cli_snippet +from jinja2.exceptions import TemplateNotFound +from python_multipart.multipart import parse_options_header +from starlette.background import BackgroundTask +from starlette.datastructures import UploadFile as StarletteUploadFile +from starlette.formparsers import MultiPartException +from starlette.responses import RedirectResponse + +import gradio +from gradio import ( + caching, + route_utils, + themes, + utils, +) +from gradio.brotli_middleware import BrotliMiddleware +from gradio.context import Context +from gradio.data_classes import ( + CancelBody, + ComponentServerBlobBody, + ComponentServerJSONBody, + DataWithFiles, + DeveloperPath, + JsonData, + PredictBody, + PredictBodyInternal, + ResetBody, + SimplePredictBody, + UserProvidedPath, + VibeCodeBody, + VibeEditBody, +) +from gradio.exceptions import Error, InvalidPathError +from gradio.helpers import special_args +from gradio.i18n import I18n +from gradio.node_server import ( + start_node_server, +) +from gradio.oauth import attach_oauth +from gradio.route_utils import ( # noqa: F401 + API_PREFIX, + BUILD_PATH_LIB, + DEFAULT_TEMP_DIR, + STATIC_PATH_LIB, + STATIC_TEMPLATE_LIB, + VERSION, + XSS_SAFE_MIMETYPES, + CustomCORSMiddleware, + FileUploadProgress, + FileUploadProgressNotQueuedError, + FileUploadProgressNotTrackedError, + GradioMultiPartParser, + GradioUploadFile, + Request, + compare_passwords_securely, + create_lifespan_handler, + favicon, + file_fetch, + file_response, + move_uploaded_files_to_cache, + routes_safe_join, + secure_url_stream_response, + upload_fn, +) +from gradio.screen_recording_utils import process_video_with_ffmpeg +from gradio.server_messages import ( + CloseStreamMessage, + EstimationMessage, + EventMessage, + HeartbeatMessage, + ProcessCompletedMessage, + ProcessGeneratingMessage, + UnexpectedErrorMessage, +) +from gradio.state_holder import StateHolder +from gradio.themes import ThemeClass as Theme +from gradio.utils import ( + cancel_tasks, + get_node_path, + get_upload_folder, + safe_aclose_iterator, +) + +if TYPE_CHECKING: + from gradio.blocks import Block + +import difflib +import re +import shutil +import tempfile + +mimetypes.init() + +BUILT_IN_THEMES: dict[str, Theme] = { + t.name: t # type: ignore + for t in [ + themes.Base(), + themes.Default(), + themes.Monochrome(), + themes.Soft(), + themes.Glass(), + themes.Origin(), + themes.Citrus(), + themes.Ocean(), + themes.Mario(), + ] + if t.name is not None +} + + +class ORJSONResponse(JSONResponse): + media_type = "application/json" + + @staticmethod + def default(content: Any) -> str: + if isinstance(content, JsonData): + return content.model_dump() + return str(content) + + @staticmethod + def _render(content: Any) -> bytes: + return orjson.dumps( + content, + option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_PASSTHROUGH_DATETIME, + default=ORJSONResponse.default, + ) + + def render(self, content: Any) -> bytes: + return ORJSONResponse._render(content) + + @staticmethod + def _render_str(content: Any) -> str: + return ORJSONResponse._render(content).decode("utf-8") + + +def toorjson(value): + return markupsafe.Markup( + ORJSONResponse._render_str(value) + .replace("<", "\\u003c") + .replace(">", "\\u003e") + .replace("&", "\\u0026") + .replace("'", "\\u0027") + ) + + +templates = Jinja2Templates(directory=STATIC_TEMPLATE_LIB) +templates.env.filters["toorjson"] = toorjson + +# Shared transport keeps the connection pool warm without sharing an +# `httpx.AsyncClient` (and therefore a cookie jar) across `/proxy=` requests. +# A single shared `AsyncClient` would persist `Set-Cookie` headers from one +# proxied response and replay them on subsequent requests to any sibling +# `*.hf.space` URL — see GHSA-2mr9-9r47-px2g. +_proxy_transport = httpx.AsyncHTTPTransport( + limits=httpx.Limits( + max_connections=100, + max_keepalive_connections=20, + ), +) + + +file_upload_statuses = FileUploadProgress() + + +class App(FastAPI): + """ + FastAPI App Wrapper + """ + + app_port = None + + def __init__( + self, + auth_dependency: Callable[[fastapi.Request], str | None] | None = None, + **kwargs, + ): + self.tokens = {} + self.auth = None + self.analytics_key = secrets.token_urlsafe(16) + self.monitoring_enabled = False + self.blocks: gradio.Blocks | None = None + self.state_holder = StateHolder() + self.iterators: dict[str, AsyncIterator] = {} + self.iterators_to_reset: set[str] = set() + self.lock = utils.safe_get_lock() + self.stop_event = utils.safe_get_stop_event() + self.cookie_id = secrets.token_urlsafe(32) + self.queue_token = secrets.token_urlsafe(32) + self.startup_events_triggered = False + self.uploaded_file_dir = get_upload_folder() + self.change_count: int = 0 + self.change_type: Literal["reload", "error"] | None = None + self.reload_error_message: str | None = None + self._asyncio_tasks: list[asyncio.Task] = [] + self.auth_dependency = auth_dependency + self.api_info = None + self.static_worker_pool = None # Set by launch() when num_workers > 0 + self.all_app_info = None + self._static_prefixes: tuple[ + str, ... + ] = () # Populated by enable_static_workers + + # Allow user to manually set `docs_url` and `redoc_url` + # when instantiating an App; when they're not set, disable docs and redoc. + kwargs.setdefault("docs_url", None) + kwargs.setdefault("redoc_url", None) + self.custom_component_hashes: dict[str, str] = {} + super().__init__(**kwargs) + + def configure_app(self, blocks: gradio.Blocks) -> None: + auth = blocks.auth + if auth is not None: + if not callable(auth): + self.auth = {account[0]: account[1] for account in auth} # type: ignore + else: + self.auth = auth + else: + self.auth = None + self.blocks = blocks + self.cwd = os.getcwd() + self.favicon_path = blocks.favicon_path + self.tokens = {} + self.root_path = blocks.root_path or "" + self.state_holder.set_blocks(blocks) + + def get_blocks(self) -> gradio.Blocks: + if self.blocks is None: + raise ValueError("No Blocks has been configured for this app.") + return self.blocks + + def build_proxy_request(self, url_path): + url = httpx.URL(url_path) + assert self.blocks # noqa: S101 + # Don't proxy a URL unless it's a URL specifically loaded by the user using + # gr.load() to prevent SSRF or harvesting of HF tokens by malicious Spaces. + is_safe_url = any( + url.host == httpx.URL(root).host for root in self.blocks.proxy_urls + ) + if not is_safe_url: + raise PermissionError("This URL cannot be proxied.") + # Only allow proxying to Hugging Face Space URLs to prevent SSRF + # via malicious proxy_url values in untrusted configs. + if not url.host.endswith(".hf.space"): + raise PermissionError("This URL cannot be proxied.") + headers = {} + if Context.token is not None: + headers["Authorization"] = f"Bearer {Context.token}" + # Build a plain request rather than `client.build_request` so that + # the proxy does not share an `httpx.AsyncClient` (or cookie jar) + # across calls (see GHSA-2mr9-9r47-px2g). + return url, headers + + def _cancel_asyncio_tasks(self): + for task in self._asyncio_tasks: + task.cancel() + self._asyncio_tasks = [] + + @staticmethod + def setup_mcp_server( + blocks: gradio.Blocks, + app_kwargs: dict[str, Any], + mcp_server: bool | None = None, + ): + mcp_subpath = API_PREFIX + "/mcp" + if mcp_server is None: + mcp_server = os.environ.get("GRADIO_MCP_SERVER", "False").lower() == "true" + if mcp_server: + try: + import gradio.mcp + except ImportError as e: + raise ImportError( + 'In order to use `mcp_server=True`, you must install gradio with the `mcp` extra. Please install it with `pip install "gradio[mcp]"`' + ) from e + try: + blocks.mcp_server_obj = gradio.mcp.GradioMCPServer(blocks) + blocks.mcp_server = True + user_lifespan = None + if "lifespan" in app_kwargs: + user_lifespan = app_kwargs["lifespan"] + + @contextlib.asynccontextmanager + async def _lifespan(app: App): + async with contextlib.AsyncExitStack() as stack: + if blocks.mcp_server_obj: + await stack.enter_async_context( + blocks.mcp_server_obj.lifespan(app) + ) + if user_lifespan is not None: + await stack.enter_async_context(user_lifespan(app)) + yield + + app_kwargs["lifespan"] = _lifespan + except Exception as e: + blocks.mcp_server = False + blocks.mcp_error = f"Error launching MCP server: {e}" + + blocks.config = ( + blocks.get_config_file() + ) # Because the config should include the fact that the MCP server is enabled + return mcp_subpath + + @staticmethod + def create_app( + blocks: gradio.Blocks, + app: App | None = None, + app_kwargs: dict[str, Any] | None = None, + auth_dependency: Callable[[fastapi.Request], str | None] | None = None, + strict_cors: bool = True, + mcp_server: bool | None = None, + debug: bool = False, + ) -> App: + app_kwargs = app_kwargs or {} + app_kwargs.setdefault("default_response_class", ORJSONResponse) + mcp_subpath = App.setup_mcp_server(blocks, app_kwargs, mcp_server) + + delete_cache = blocks.delete_cache or (None, None) + if app is None: + app_kwargs["lifespan"] = create_lifespan_handler( + app_kwargs.get("lifespan", None), *delete_cache + ) + app = App(auth_dependency=auth_dependency, **app_kwargs, debug=debug) + else: + app.router.lifespan_context = create_lifespan_handler( + app_kwargs.get("lifespan", None), *delete_cache + ) + if blocks.mcp_server_obj: + blocks.mcp_server_obj.launch_mcp_on_sse(app, mcp_subpath, blocks.root_path) + router = APIRouter(prefix=API_PREFIX) + + app.configure_app(blocks) + + app.add_middleware(CustomCORSMiddleware, strict_cors=strict_cors) # type: ignore + app.add_middleware( + BrotliMiddleware, # type: ignore + quality=4, + excluded_handlers=[mcp_subpath], + ) + + # Note: In the current architecture, Node is the front proxy and + # routes requests to Python. The old conditional_routing_middleware + # that proxied Python -> Node is no longer needed. + + @router.get("/user") + @router.get("/user/") + def get_current_user(request: fastapi.Request) -> str | None: + if app.auth_dependency is not None: + return app.auth_dependency(request) + token = request.cookies.get( + f"access-token-{app.cookie_id}" + ) or request.cookies.get(f"access-token-unsecure-{app.cookie_id}") + return app.tokens.get(token) + + @router.get("/login_check") + @router.get("/login_check/") + def login_check(user: str = Depends(get_current_user)): + if (app.auth is None and app.auth_dependency is None) or user is not None: + return + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={ + "error": "Not authenticated", + "auth_message": blocks.auth_message, + }, + ) + + @router.get("/token") + @router.get("/token/") + def get_token(request: fastapi.Request) -> dict: + token = request.cookies.get(f"access-token-{app.cookie_id}") + return {"token": token, "user": app.tokens.get(token)} + + @router.get("/app_id") + @router.get("/app_id/") + def app_id(request: fastapi.Request) -> dict: # noqa: ARG001 + return {"app_id": app.get_blocks().app_id} + + @router.get("/dev/reload", dependencies=[Depends(login_check)]) + async def notify_changes( + request: fastapi.Request, + ): + async def reload_checker(request: fastapi.Request): + heartbeat_rate = 15 + check_rate = 0.05 + last_heartbeat = time.perf_counter() + current_count = app.change_count + + while True: + if await request.is_disconnected(): + return + + if app.change_count != current_count: + current_count = app.change_count + msg = ( + json.dumps(f"{app.reload_error_message}") + if app.change_type == "error" + else "{}" + ) + yield f"""event: {app.change_type}\ndata: {msg}\n\n""" + + await asyncio.sleep(check_rate) + if time.perf_counter() - last_heartbeat > heartbeat_rate: + yield """event: heartbeat\ndata: {}\n\n""" + last_heartbeat = time.perf_counter() + + return StreamingResponse( + reload_checker(request), + media_type="text/event-stream", + ) + + @app.post("/login") + @app.post("/login/") + async def login( + request: fastapi.Request, form_data: OAuth2PasswordRequestForm = Depends() + ): + username, password = form_data.username.strip(), form_data.password + if app.auth is None: + root = route_utils.get_root_url( + request=request, + route_path="/login", + root_path=app.root_path, + ) + return RedirectResponse(url=root, status_code=status.HTTP_302_FOUND) + if ( + not callable(app.auth) + and username in app.auth # type: ignore + and compare_passwords_securely(password, app.auth[username]) # type: ignore + ) or ( + callable(app.auth) + and ( + await app.auth(username, password) + if inspect.iscoroutinefunction(app.auth) + else app.auth(username, password) + ) + ): # type: ignore + token = secrets.token_urlsafe(16) + app.tokens[token] = username + response = JSONResponse(content={"success": True}) + response.set_cookie( + key=f"access-token-{app.cookie_id}", + value=token, + httponly=True, + samesite="none", + secure=True, + ) + response.set_cookie( + key=f"access-token-unsecure-{app.cookie_id}", + value=token, + httponly=True, + ) + return response + else: + raise HTTPException(status_code=400, detail="Incorrect credentials.") + + ############### + # OAuth Routes + ############### + + # Define OAuth routes if the app expects it (i.e. a LoginButton is defined). + # It allows users to "Sign in with HuggingFace". Otherwise, add the default + # logout route. + if app.blocks is not None and app.blocks.expects_oauth: + attach_oauth(app) + else: + + @app.get("/logout") + def logout( + request: fastapi.Request, + user: str = Depends(get_current_user), + all_session: bool = True, + ): + root = route_utils.get_root_url( + request=request, + route_path="/logout", + root_path=app.root_path, + ) + response = RedirectResponse(url=root, status_code=status.HTTP_302_FOUND) + response.delete_cookie(key=f"access-token-{app.cookie_id}", path="/") + response.delete_cookie( + key=f"access-token-unsecure-{app.cookie_id}", path="/" + ) + if all_session: + # Delete the tokens of all sessions associated with the current user. + for token in list(app.tokens.keys()): + if app.tokens[token] == user: + del app.tokens[token] + # Delete only the token associated with the current session. + elif request.cookies.get(f"access-token-{app.cookie_id}") in app.tokens: + del app.tokens[request.cookies.get(f"access-token-{app.cookie_id}")] + return response + + ############### + # Main Routes + ############### + + @app.get("/svelte/{path:path}") + async def _(path: str): + svelte_path = DeveloperPath(str(Path(BUILD_PATH_LIB) / "svelte")) + return file_response(svelte_path, UserProvidedPath(path)) + + def attach_page(page): + @app.get(f"/{page}", response_class=HTMLResponse) + def page_route( + request: fastapi.Request, + user: str = Depends(get_current_user), + deep_link: str = "", + ): + return main(request, user, page, deep_link) + + @app.get(f"/{page}/") + def page_redirect(): + return RedirectResponse( + url=f"/{page}", status_code=status.HTTP_301_MOVED_PERMANENTLY + ) + + for pageset in blocks.pages: + page = pageset[0] + if page != "": + attach_page(page) + + def load_deep_link( + deep_link: str, config: dict[str, Any], page: str | None = None + ): + components = config["components"] + try: + user_path = Path("deep_links") / deep_link / "state.json" + path = Path( + routes_safe_join( + DeveloperPath(app.uploaded_file_dir), + UserProvidedPath(str(user_path)), + ) + ) + if path.exists(): + components = orjson.loads(path.read_bytes()) + deep_link_state = "valid" + else: + deep_link_state = "invalid" + except (FileNotFoundError, OSError, orjson.JSONDecodeError): + deep_link_state = "invalid" + components = [] + if page: + components = [ + component + for component in components + if component["id"] in config["page"][page]["components"] + ] + return components, deep_link_state + + @app.head("/", response_class=HTMLResponse) + @app.get("/", response_class=HTMLResponse) + def main( + request: fastapi.Request, + user: str = Depends(get_current_user), + page: str = "", + deep_link: str = "", + ): + mimetypes.add_type("application/javascript", ".js") + blocks = app.get_blocks() + root = route_utils.get_root_url( + request=request, + route_path=f"/{page}", + root_path=app.root_path + or request.scope.get("root_path") + or blocks.custom_mount_path, + ) + if (app.auth is None and app.auth_dependency is None) or user is not None: + config = utils.safe_deepcopy(blocks.config) + deep_link_state = "none" + components = [ + component + for component in config["components"] + if component["id"] in config["page"][page]["components"] + ] + if deep_link: + components, deep_link_state = load_deep_link( + deep_link, + config, # type: ignore + page, + ) + config["username"] = user + config["deep_link_state"] = deep_link_state + config["components"] = components # type: ignore + config["dependencies"] = [ + dependency + for dependency in config.get("dependencies", []) + if dependency["id"] in config["page"][page]["dependencies"] + ] + config["layout"] = config["page"][page]["layout"] + config["current_page"] = page + # Update root after loading the deep link state (if applicable) + # so that static files are served from the correct root + config = route_utils.update_root_in_config(config, root) + elif app.auth_dependency: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={ + "error": "Not authenticated", + "auth_message": blocks.auth_message, + }, + ) + else: + config = { + "auth_required": True, + "auth_message": blocks.auth_message, + "space_id": blocks.space_id, + "root": root, + "page": {"": {"layout": {}}}, + "pages": [""], + "components": [], + "dependencies": [], + "current_page": "", + } + + try: + template = ( + "frontend/share.html" if blocks.share else "frontend/index.html" + ) + gradio_api_info = api_info(request) + resp = templates.TemplateResponse( + request=request, + name=template, + context={ + "config": config, + "gradio_api_info": gradio_api_info, + }, + ) + return resp + except TemplateNotFound as err: + if blocks.share: + raise ValueError( + "Did you install Gradio from source files? Share mode only " + "works when Gradio is installed through the pip package." + ) from err + else: + raise ValueError( + "Did you install Gradio from source files? You need to build " + "the frontend by running /scripts/build_frontend.sh" + ) from err + + @app.get("/gradio_api/deep_link") + def deep_link(session_hash: str): + if session_hash in app.state_holder: + components = [ + utils.safe_deepcopy(c) + for c in app.state_holder[session_hash].components + ] + components_json = orjson.dumps( + components, + option=orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_PASSTHROUGH_DATETIME, + default=str, + ) + deep_link = route_utils.create_url_safe_hash(components_json) + directory = Path(app.uploaded_file_dir) / "deep_links" / deep_link + directory.mkdir(parents=True, exist_ok=True) + with open(directory / "state.json", "wb") as f: + f.write(components_json) + return deep_link + else: + return "" + + @router.get("/info/", dependencies=[Depends(login_check)]) + @router.get("/info", dependencies=[Depends(login_check)]) + def api_info(request: fastapi.Request): + all_endpoints = request.query_params.get("all_endpoints", False) + if all_endpoints: + if not app.all_app_info: + app.all_app_info = app.get_blocks().get_api_info(all_endpoints=True) + return app.all_app_info + if not app.api_info: + api_info = utils.safe_deepcopy(app.get_blocks().get_api_info()) + api_info = cast(dict[str, Any], api_info) + api_info = route_utils.update_example_values_to_use_public_url(api_info) + root = route_utils.get_root_url( + request=request, + route_path=f"{API_PREFIX}/info", + root_path=app.root_path, + ) + space_id = app.get_blocks().space_id + cli_snippets = generate_cli_snippet(api_info["named_endpoints"]) + for k, v in cli_snippets.items(): + cli_snippets[k] = v.replace("{space_id}", space_id or str(root)) + api_prefix = API_PREFIX + "/" + for ep_name, ep_info in api_info.get("named_endpoints", {}).items(): + ep_info["code_snippets"] = generate_code_snippets( + ep_name, + ep_info, + str(root), + space_id=space_id, + api_prefix=api_prefix, + ) + ep_info["code_snippets"]["cli"] = cli_snippets[ep_name] + app.api_info = api_info + return app.api_info + + @router.get("/openapi.json", dependencies=[Depends(login_check)]) + def openapi_schema(request: fastapi.Request): + """Generate an OpenAPI schema from the Gradio app's API info.""" + info = api_info(request) + info_simple = _condense_info(info, url_only=True) + schema = { + "openapi": "3.0.2", + "info": { + "title": getattr(app.get_blocks(), "title", "Gradio App"), + "description": getattr(app.get_blocks(), "description", ""), + "version": VERSION, + }, + "paths": { + "/gradio_api/upload": { + "post": { + "summary": "Upload File", + "operationId": "upload_file_upload_post", + "parameters": [ + { + "name": "upload_id", + "in": "query", + "required": False, + "schema": { + "type": "string", + "nullable": True, + "default": None, + }, + "description": "Optional ID to track upload progress", + } + ], + "requestBody": { + "required": True, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "string", + "format": "binary", + }, + "description": "One or more files to upload", + } + }, + "required": ["files"], + } + } + }, + }, + "responses": { + "200": { + "description": "List of file paths where the uploaded files were saved", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": {"type": "string"}, + } + } + }, + }, + "400": { + "description": "Invalid content type or invalid file name", + "content": { + "text/plain": {"schema": {"type": "string"}} + }, + }, + "413": { + "description": "File exceeds maximum allowed size", + "content": { + "text/plain": {"schema": {"type": "string"}} + }, + }, + }, + "security": [{"login_check": []}], + } + } + }, + "components": {"schemas": {}}, + } + + for endpoint_path, endpoint_info in info.get("named_endpoints", {}).items(): # type: ignore + if endpoint_info.get("api_visibility", "public") == "private": + continue + endpoint_name = endpoint_path.strip("/").replace("/", "_") + has_file_params = any( + p.get("type", {}).get("type") == "filepath" + for p in info_simple[endpoint_path].get("parameters", []) + ) + summary = ( + endpoint_info.get("description", "") or f"Endpoint {endpoint_path}" + ) + if has_file_params: + summary += '. File inputs must first be uploaded via POST /gradio_api/upload (multipart/form-data with a "files" field). Use the returned path in the request body as {"path": "", "meta": {"_type": "gradio.FileData"}}.' + path_item = { + "post": { + "summary": summary, + "description": endpoint_info.get("description", ""), + "operationId": endpoint_name, + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"type": "object", "properties": {}} + }, + }, + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "event_id": {"type": "string"} + }, + } + } + }, + } + }, + } + } + + request_properties = path_item["post"]["requestBody"]["content"][ # type: ignore + "application/json" + ]["schema"]["properties"] # type: ignore + for param in info_simple[endpoint_path].get("parameters", []): + param_name = param["name"] + param_type = param.get("type", {}) + + if "additional_description" in param_type: + param_type = dict(param_type) + param_type.pop("additional_description", None) + + if "properties" in param_type and "type" not in param_type: + param_type = dict(param_type) + param_type["type"] = "object" + + if param_type.get("type") == "filepath": + param_type = dict(param_type) + param_type["type"] = "string" + param_type["format"] = "filepath" + + request_properties[param_name] = param_type # type: ignore + + if "example_input" in param: + if ( + "examples" + not in path_item["post"]["requestBody"]["content"][ # type: ignore + "application/json" + ] + ): + path_item["post"]["requestBody"]["content"][ # type: ignore + "application/json" + ]["examples"] = {"example1": {"value": {}}} + path_item["post"]["requestBody"]["content"]["application/json"][ # type: ignore + "examples" + ]["example1"]["value"][param_name] = param["example_input"] # type: ignore + + returns_info = [] + for i, ret in enumerate(info_simple[endpoint_path].get("returns", [])): + ret_name = f"output_{i}" if i > 0 else "output" + ret_type = ret.get("type", {}) + desc = "" + returns_info.append( + f"{ret_name} ({ret_type})" + "" if not desc else f"desc: {desc}" + ) # type: ignore + path_item["post"]["description"] += ( # type: ignore + f"Output must be fetched from GET /gradio_api/call{endpoint_path}/{{event_id}}. Returns an array of {len(returns_info)} elements of the following format: {returns_info}" + ) + + schema["paths"][f"/gradio_api/call/v2{endpoint_path}"] = path_item # type: ignore + + get_path = f"/gradio_api/call{endpoint_path}/{{event_id}}" + schema["paths"][get_path] = { # type: ignore + "get": { + "summary": f"Fetch results for {endpoint_path}", + "description": "Returns a stream of server-sent events (SSE). The final event has `event: complete` with `data` containing a JSON array of outputs.", + "operationId": f"{endpoint_name}_get", + "parameters": [ + { + "name": "event_id", + "in": "path", + "required": True, + "schema": {"type": "string"}, + "description": "The event_id returned by the POST request", + } + ], + "responses": { + "200": { + "description": "SSE stream with event: complete containing a JSON array of outputs", + "content": { + "text/event-stream": {"schema": {"type": "string"}} + }, + } + }, + } + } + + return schema + + @app.get("/config/", dependencies=[Depends(login_check)]) + @app.get("/config", dependencies=[Depends(login_check)]) + def get_config(request: fastapi.Request, deep_link: str = ""): + config = utils.safe_deepcopy(app.get_blocks().config) + root = route_utils.get_root_url( + request=request, + route_path="/config", + root_path=app.root_path + or request.scope.get("root_path") + or blocks.custom_mount_path, + ) + config["username"] = get_current_user(request) + if deep_link: + components, deep_link_state = load_deep_link(deep_link, config, page="") # type: ignore + config["components"] = components # type: ignore + config["deep_link_state"] = deep_link_state + if hasattr(blocks, "i18n_instance") and blocks.i18n_instance: + config["i18n_translations"] = blocks.i18n_instance.translations_dict + else: + config["i18n_translations"] = None + config = route_utils.update_root_in_config(config, root) + return ORJSONResponse(content=config) + + @app.get("/static/{path:path}") + async def static_resource(path: str): + return file_response(STATIC_PATH_LIB, UserProvidedPath(path)) + + @router.get("/custom_component/{id}/{environment}/{type}/{file_name}") + def custom_component_path( + id: str, + environment: Literal["client", "server"], + type: str, + file_name: str, + req: fastapi.Request, + ): + print( + f"id={id}, environment={environment}, type={type}, file_name={file_name}" + ) + if environment not in ["client", "server"]: + raise HTTPException( + status_code=404, detail="Environment not supported." + ) + components = utils.get_all_components() + location = next( + (item for item in components if item.get_component_class_id() == id), + None, + ) + if location is None: + raise HTTPException(status_code=404, detail="Component not found.") + + module_name = location.__module__ + module_path = sys.modules[module_name].__file__ + + if module_path is None: + raise HTTPException(status_code=404, detail="Component not found.") + + try: + requested_path = utils.safe_join( + location.TEMPLATE_DIR, + UserProvidedPath(f"{type}/{file_name}"), + ) + except InvalidPathError: + raise HTTPException( + status_code=404, detail="Component not found." + ) from None + + path = routes_safe_join( + DeveloperPath(str(Path(module_path).parent)), + UserProvidedPath(requested_path), + ) + + # Uncomment when we support custom component SSR + # if environment == "server": + # return PlainTextResponse(path) + + key = f"{id}-{type}-{file_name}" + + if key not in app.custom_component_hashes: + app.custom_component_hashes[key] = hashlib.sha256( + Path(path).read_text(encoding="utf-8").encode() + ).hexdigest() + + version = app.custom_component_hashes.get(key) + headers = {"Cache-Control": "max-age=0, must-revalidate"} + if version: + headers["ETag"] = version + + if version and req.headers.get("if-none-match") == version: + return PlainTextResponse(status_code=304, headers=headers) + + return FileResponse(path, headers=headers) + + @app.get("/assets/{path:path}") + async def build_resource(path: str): + return file_response(BUILD_PATH_LIB, UserProvidedPath(path)) + + @app.get("/favicon.ico") + async def _(): + favicon_path = app.get_blocks().favicon_path + return favicon(favicon_path) + + @router.head("/proxy={url_path:path}", dependencies=[Depends(login_check)]) + @router.get("/proxy={url_path:path}", dependencies=[Depends(login_check)]) + async def reverse_proxy(url_path: str): + # Adapted from: https://github.com/tiangolo/fastapi/issues/1788 + try: + proxy_client = httpx.AsyncClient( + transport=_proxy_transport, + timeout=httpx.Timeout(10.0), + ) + url, headers = app.build_proxy_request(url_path) + rp_req = proxy_client.build_request("GET", url, headers=headers) + except PermissionError as err: + raise HTTPException(status_code=400, detail=str(err)) from err + + rp_resp = await proxy_client.send(rp_req, stream=True) + + mime_type, _ = mimetypes.guess_type(url_path) + if mime_type not in XSS_SAFE_MIMETYPES: + rp_resp.headers.update({"Content-Disposition": "attachment"}) + rp_resp.headers.update({"Content-Type": "application/octet-stream"}) + return StreamingResponse( + rp_resp.aiter_raw(), + status_code=rp_resp.status_code, + headers=rp_resp.headers, # type: ignore + background=BackgroundTask(rp_resp.aclose), + ) + + @router.head("/file={path_or_url:path}", dependencies=[Depends(login_check)]) + @router.get("/file={path_or_url:path}", dependencies=[Depends(login_check)]) + async def file(path_or_url: str, request: fastapi.Request): + if client_utils.is_http_url_like(path_or_url): + return await secure_url_stream_response(path_or_url, request) + blocks = app.get_blocks() + return file_fetch(path_or_url, request, blocks, app.uploaded_file_dir) + + @router.post("/stream/{event_id}") + async def _(event_id: str, body: PredictBody, request: fastapi.Request): + event = app.get_blocks()._queue.event_ids_to_events[event_id] + body = PredictBodyInternal(**body.model_dump(), request=request) # type: ignore + event.data = body + event.signal.set() + return {"msg": "success"} + + @router.post("/stream/{event_id}/close") + async def _(event_id: str): + event = app.get_blocks()._queue.event_ids_to_events[event_id] + event.run_time = math.inf + event.closed = True + event.signal.set() + return {"msg": "success"} + + @router.get("/stream/{session_hash}/{run}/{component_id}/playlist.m3u8") + async def _(session_hash: str, run: int, component_id: int): + stream: route_utils.MediaStream | None = ( + app.get_blocks() + .pending_streams[session_hash] + .get(run, {}) + .get(component_id, None) + ) + + if not stream: + return Response(status_code=404) + + playlist = f"#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT\n#EXT-X-TARGETDURATION:{stream.max_duration}\n#EXT-X-VERSION:4\n#EXT-X-MEDIA-SEQUENCE:0\n" + + for segment in stream.segments: + playlist += f"#EXTINF:{segment['duration']:.3f},\n" + playlist += f"{segment['id']}{segment['extension']}\n" # type: ignore + # HLS expects the start time of the video segments to be continuous + # Instead of re-encoding the user video chunks, we add a discontinuity tag + if segment["extension"] == ".ts": + playlist += "#EXT-X-DISCONTINUITY\n" + + if stream.ended: + playlist += "#EXT-X-ENDLIST\n" + + return Response( + content=playlist, media_type="application/vnd.apple.mpegurl" + ) + + @router.get("/stream/{session_hash}/{run}/{component_id}/{segment_id}.{ext}") + async def _( + session_hash: str, run: int, component_id: int, segment_id: str, ext: str + ): + if ext not in ["aac", "ts"]: + return Response(status_code=400, content="Unsupported file extension") + stream: route_utils.MediaStream | None = ( + app.get_blocks() + .pending_streams[session_hash] + .get(run, {}) + .get(component_id, None) + ) + + if not stream: + return Response(status_code=404, content="Stream not found") + + segment = next((s for s in stream.segments if s["id"] == segment_id), None) # type: ignore + + if segment is None: + return Response(status_code=404, content="Segment not found") + + if ext == "aac": + return Response(content=segment["data"], media_type="audio/aac") + else: + return Response(content=segment["data"], media_type="video/MP2T") + + @router.get("/stream/{session_hash}/{run}/{component_id}/playlist-file") + async def _(session_hash: str, run: int, component_id: int): + stream: route_utils.MediaStream | None = ( + app.get_blocks() + .pending_streams[session_hash] + .get(run, {}) + .get(component_id, None) + ) + + if not stream: + return Response(status_code=404) + + if not stream.combined_file: + stream_data = [s["data"] for s in stream.segments] + combined_file = ( + await app.get_blocks() + .get_component(component_id) + .combine_stream( # type: ignore + stream_data, + only_file=True, + desired_output_format=stream.desired_output_format, + ) + ) + stream.combined_file = combined_file.path + return FileResponse(stream.combined_file) + + @router.get("/file/{path:path}", dependencies=[Depends(login_check)]) + async def file_deprecated(path: str, request: fastapi.Request): + return await file(path, request) + + @router.post("/reset/") + @router.post("/reset") + async def reset_iterator(body: ResetBody): # noqa: ARG001 + # No-op, all the cancelling/reset logic handled by /cancel + return {"success": True} + + @router.get("/heartbeat/{session_hash}") + def heartbeat( + session_hash: str, + request: fastapi.Request, + background_tasks: BackgroundTasks, + username: str = Depends(get_current_user), + ): + """Clients make a persistent connection to this endpoint to keep the session alive. + When the client disconnects, the session state is deleted. + """ + heartbeat_rate = utils.get_heartbeat_rate() + + async def iterator(): + stop_stream_task = asyncio.create_task(app.stop_event.wait()) + while True: + try: + yield "data: ALIVE\n\n" + # We need to close the heartbeat connections as soon as the server stops + # otherwise the server can take forever to close + wait_task = asyncio.create_task(asyncio.sleep(heartbeat_rate)) + done, _ = await asyncio.wait( + [wait_task, stop_stream_task], + return_when=asyncio.FIRST_COMPLETED, + ) + if stop_stream_task in done: + raise asyncio.CancelledError() + except asyncio.CancelledError: + if not stop_stream_task.done(): + stop_stream_task.cancel() + + req = Request(request, username, session_hash=session_hash) + root_path = route_utils.get_root_url( + request=request, + route_path=f"{API_PREFIX}/heartbeat/{session_hash}", + root_path=app.root_path, + ) + body = PredictBodyInternal( + session_hash=session_hash, data=[], request=request + ) + unload_fn_indices = [ + i + for i, dep in app.get_blocks().fns.items() + if any(t for t in dep.targets if t[1] == "unload") + ] + for fn_index in unload_fn_indices: + # The task running this loop has been cancelled + # so we add tasks in the background + background_tasks.add_task( + route_utils.call_process_api, + app=app, + body=body, + gr_request=req, + fn=app.get_blocks().fns[fn_index], + root_path=root_path, + ) + # This will mark the state to be deleted in an hour + if session_hash in app.state_holder.session_data: + app.state_holder.session_data[session_hash].is_closed = True + caching.clear_session_caches(session_hash) + for ( + event_id + ) in app.get_blocks()._queue.pending_event_ids_session.get( + session_hash, [] + ): + event = app.get_blocks()._queue.event_ids_to_events[ + event_id + ] + event.run_time = math.inf + event.signal.set() + return + + return StreamingResponse(iterator(), media_type="text/event-stream") + + # had to use '/run' endpoint for Colab compatibility, '/api' supported for backwards compatibility + @router.post("/run/{api_name}", dependencies=[Depends(login_check)]) + @router.post("/run/{api_name}/", dependencies=[Depends(login_check)]) + @router.post("/api/{api_name}", dependencies=[Depends(login_check)]) + @router.post("/api/{api_name}/", dependencies=[Depends(login_check)]) + async def predict( + api_name: str, + body: PredictBody, + request: fastapi.Request, + username: str = Depends(get_current_user), + ): + body = PredictBodyInternal(**body.model_dump(), request=request) # type: ignore + fn = route_utils.get_fn( + blocks=app.get_blocks(), api_name=api_name, body=body + ) + + if not app.get_blocks().api_open and fn.queue: + raise HTTPException( + detail="This API endpoint does not accept direct HTTP POST requests. Please join the queue to use this API.", + status_code=status.HTTP_404_NOT_FOUND, + ) + gr_request = route_utils.compile_gr_request( + body, + fn=fn, + username=username, + request=request, + ) + root_path = route_utils.get_root_url( + request=request, + route_path=request.url.path, + root_path=app.root_path, + ) + try: + output = await route_utils.call_process_api( + app=app, + body=body, + gr_request=gr_request, + fn=fn, + root_path=root_path, + ) + except BaseException as error: + content = utils.error_payload(error, app.get_blocks().show_error) + if not isinstance(error, Error) or error.print_exception: + traceback.print_exc() + return JSONResponse( + content=content, + status_code=500, + ) + return ORJSONResponse(output) + + def prepare_simple_api_data(body: PredictBody, fn: Any) -> None: + if len(body.data) == len(fn.inputs): + return + api_inputs = [block for block in fn.inputs if not block.skip_api] + if len(body.data) != len(api_inputs): + return + data = iter(body.data) + body.data = [None if block.skip_api else next(data) for block in fn.inputs] + + @router.post("/call/v2/{api_name}", dependencies=[Depends(login_check)]) + @router.post("/call/v2/{api_name}/", dependencies=[Depends(login_check)]) + async def _( + api_name: str, + body: dict[str, Any], + request: fastapi.Request, + username: str = Depends(get_current_user), + ): + parameters_info = app.api_info["named_endpoints"]["/" + api_name][ # type: ignore + "parameters" + ] + processed_args = client_utils.construct_args( + parameters_info, + (), + body, + ) + simple_body = SimplePredictBody(data=processed_args) + full_body = PredictBody(**simple_body.model_dump(), simple_format=True) # type: ignore + fn = route_utils.get_fn( + blocks=app.get_blocks(), api_name=api_name, body=full_body + ) + prepare_simple_api_data(full_body, fn) + full_body.fn_index = fn._id + return await queue_join_helper(full_body, request, username) + + @router.post("/call/{api_name}", dependencies=[Depends(login_check)]) + @router.post("/call/{api_name}/", dependencies=[Depends(login_check)]) + async def simple_predict_post( + api_name: str, + body: SimplePredictBody, + request: fastapi.Request, + username: str = Depends(get_current_user), + ): + full_body = PredictBody(**body.model_dump(), simple_format=True) # type: ignore + fn = route_utils.get_fn( + blocks=app.get_blocks(), api_name=api_name, body=full_body + ) + prepare_simple_api_data(full_body, fn) + full_body.fn_index = fn._id + return await queue_join_helper(full_body, request, username) + + @router.post("/queue/join", dependencies=[Depends(login_check)]) + async def queue_join( + body: PredictBody, + request: fastapi.Request, + username: str = Depends(get_current_user), + ): + if body.session_hash is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Session hash not found.", + ) + return await queue_join_helper(body, request, username) + + async def queue_join_helper( + body: PredictBody, + request: fastapi.Request, + username: str, + ): + blocks = app.get_blocks() + + if blocks._queue.server_app is None: + blocks._queue.set_server_app(app) + + if blocks._queue.stopped: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Queue is stopped.", + ) + body = PredictBodyInternal(**body.model_dump(), request=request) # type: ignore + success, event_id, state = await blocks._queue.push( + body=body, request=request, username=username + ) + error_map = { + "queue_full": status.HTTP_503_SERVICE_UNAVAILABLE, + "validator_error": status.HTTP_422_UNPROCESSABLE_CONTENT, + "error": status.HTTP_400_BAD_REQUEST, + "success": status.HTTP_200_OK, + } + + if not success: + status_code = error_map[state] + raise HTTPException(status_code=status_code, detail=event_id) + return {"event_id": event_id} + + @router.post("/cancel") + async def cancel_event(body: CancelBody): + await cancel_tasks({f"{body.session_hash}_{body.fn_index}"}) + blocks = app.get_blocks() + # Need to complete the job so that the client disconnects + session_open = ( + body.session_hash in blocks._queue.pending_messages_per_session + ) + event_running = ( + body.event_id + in blocks._queue.pending_event_ids_session.get(body.session_hash, {}) + ) + await blocks._queue.remove_from_queue(body.event_id) + if session_open and event_running: + message = ProcessCompletedMessage( + output={}, success=True, event_id=body.event_id + ) + blocks._queue.pending_messages_per_session[ + body.session_hash + ].put_nowait(message) + if body.event_id in app.iterators: + async with app.lock: + try: + await safe_aclose_iterator(app.iterators[body.event_id]) + except Exception: + pass + del app.iterators[body.event_id] + app.iterators_to_reset.add(body.event_id) + return {"success": True} + + @router.get( + "/call/v2/{api_name}/{event_id}", dependencies=[Depends(login_check)] + ) + @router.get("/call/{api_name}/{event_id}", dependencies=[Depends(login_check)]) + async def simple_predict_get( + request: fastapi.Request, + event_id: str, + ): + def process_msg(message: EventMessage) -> str | None: + msg = message.model_dump() + if isinstance(message, ProcessCompletedMessage): + event = "complete" if message.success else "error" + data = ( + msg["output"].get("data") if message.success else msg["output"] + ) + elif isinstance(message, ProcessGeneratingMessage): + event = "generating" if message.success else "error" + data = ( + msg["output"].get("data") if message.success else msg["output"] + ) + elif isinstance(message, HeartbeatMessage): + event = "heartbeat" + data = None + elif isinstance(message, UnexpectedErrorMessage): + event = "error" + data = message.message + else: + return None + return f"event: {event}\ndata: {json.dumps(data)}\n\n" + + event = app.get_blocks()._queue.event_ids_to_events.get(event_id) + session_hash = event.session_hash if event else event_id + return await queue_data_helper(request, session_hash, process_msg) + + @router.get("/queue/data", dependencies=[Depends(login_check)]) + async def queue_data( + request: fastapi.Request, + session_hash: str, + ): + def process_msg(message: EventMessage) -> str: + return f"data: {orjson.dumps(message.model_dump(), default=str).decode('utf-8')}\n\n" + + return await queue_data_helper(request, session_hash, process_msg) + + async def queue_data_helper( + request: fastapi.Request, + session_hash: str, + process_msg: Callable[[EventMessage], str | None], + ): + blocks = app.get_blocks() + heartbeat_rate = utils.get_heartbeat_rate() + + async def heartbeat(): + while blocks.is_running: + await asyncio.sleep(heartbeat_rate) + # It's possible the event has finished by the time + # the heartbeat wakes up + queue = blocks._queue.pending_messages_per_session.get(session_hash) + if queue: + await queue.put(HeartbeatMessage()) + + async def sse_stream(request: fastapi.Request): + heartbeat_task = asyncio.create_task(heartbeat()) + try: + while True: + if await request.is_disconnected(): + await blocks._queue.clean_events(session_hash=session_hash) + heartbeat_task.cancel() + return + + if ( + session_hash + not in blocks._queue.pending_messages_per_session + ): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + ) + + message = None + try: + messages = blocks._queue.pending_messages_per_session[ + session_hash + ] + message = await asyncio.wait_for(messages.get(), timeout=10) + except (TimeoutError, asyncio.TimeoutError): + pass + + if blocks._queue.stopped: + message = UnexpectedErrorMessage( + message="Server stopped unexpectedly.", + success=False, + ) + if message: + response = process_msg(message) + if response is not None: + yield response + if ( + isinstance(message, ProcessCompletedMessage) + and message.event_id + ): + # It's possible that the event_id has already been removed + # for example, the user sent two duplicate `/cancel` requests. + # The first one would have removed the event_id from pending_event_ids_session + if ( + message.event_id + in ( + blocks._queue.pending_event_ids_session[ + session_hash + ] + ) + ): + blocks._queue.pending_event_ids_session[ + session_hash + ].remove(message.event_id) + if message.msg == ServerMessage.server_stopped or ( + message.msg == ServerMessage.process_completed + and ( + len( + blocks._queue.pending_event_ids_session[ + session_hash + ] + ) + == 0 + ) + ): + message = CloseStreamMessage() + response = process_msg(message) + if response is not None: + yield response + heartbeat_task.cancel() + return + except BaseException as e: + message = UnexpectedErrorMessage( + message=str(e), + session_not_found=isinstance(e, HTTPException), + ) + response = process_msg(message) + if isinstance(e, asyncio.CancelledError): + del blocks._queue.pending_messages_per_session[session_hash] + await blocks._queue.clean_events(session_hash=session_hash) + if response is not None: + yield response + heartbeat_task.cancel() + raise e + + return StreamingResponse( + sse_stream(request), + media_type="text/event-stream", + ) + + async def get_item_or_file( + request: fastapi.Request, + ) -> Union[ComponentServerJSONBody, ComponentServerBlobBody]: + content_type = request.headers.get("Content-Type") + + if isinstance(content_type, str) and content_type.startswith( + "multipart/form-data" + ): + files = [] + data = {} + max_file_size = ( + blocks.max_file_size + if blocks.max_file_size is not None + else math.inf + ) + multipart_parser = GradioMultiPartParser( + request.headers, + request.stream(), + max_files=1000, + max_fields=1000, + max_file_size=max_file_size, + ) + try: + form = await multipart_parser.parse() + except MultiPartException as exc: + code = 413 if "File size exceeded" in exc.message else 400 + raise HTTPException(status_code=code, detail=exc.message) from None + try: + for key, value in form.multi_items(): + if isinstance(value, StarletteUploadFile): + contents = await value.read() + files.append((value.filename, contents)) + else: + data[key] = value + finally: + for _, value in form.multi_items(): + if isinstance(value, StarletteUploadFile): + await value.close() + with contextlib.suppress(OSError, AttributeError): + os.unlink(value.file.name) + + return ComponentServerBlobBody( + data=DataWithFiles(data=data, files=files), + component_id=data["component_id"], + session_hash=data["session_hash"], + fn_name=data["fn_name"], + ) + else: + try: + data = await request.json() + return ComponentServerJSONBody( + data=data["data"], + component_id=data["component_id"], + session_hash=data["session_hash"], + fn_name=data["fn_name"], + ) + + except Exception: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid JSON body.", + ) from None + + @router.post( + "/component_server", + dependencies=[Depends(login_check)], + ) + @router.post( + "/component_server/", + dependencies=[Depends(login_check)], + ) + async def component_server( + request: fastapi.Request, + ): + body = await get_item_or_file(request) + state = app.state_holder[body.session_hash] + component_id = body.component_id + block: Block + if component_id in state: + block = state[component_id] + else: + block = app.get_blocks().blocks[component_id] + fn = getattr(block, body.fn_name, None) + if fn is None or not getattr(fn, "_is_server_fn", False): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Function not found.", + ) + processed_input, *_ = special_args( + fn, + [body.data], + request, # type: ignore + None, + ) + if inspect.iscoroutinefunction(fn): + return await fn(*processed_input) + else: + return fn(*processed_input) + + @router.get( + "/queue/status", + dependencies=[Depends(login_check)], + response_model=EstimationMessage, + ) + async def get_queue_status(): + return app.get_blocks()._queue.get_status() + + @router.get("/upload_progress") + async def get_upload_progress(upload_id: str, request: fastapi.Request): + async def sse_stream(request: fastapi.Request): + last_heartbeat = time.perf_counter() + is_done = False + while True: + if await request.is_disconnected(): + file_upload_statuses.stop_tracking(upload_id) + return + if is_done: + file_upload_statuses.stop_tracking(upload_id) + return + + heartbeat_rate = 15 + check_rate = 0.05 + try: + if file_upload_statuses.is_done(upload_id): + message = {"msg": "done"} + is_done = True + else: + update = file_upload_statuses.pop(upload_id) + message = { + "msg": "update", + "orig_name": update.filename, + "chunk_size": update.chunk_size, + } + yield f"data: {json.dumps(message)}\n\n" + except FileUploadProgressNotTrackedError: + return + except FileUploadProgressNotQueuedError: + await asyncio.sleep(check_rate) + if time.perf_counter() - last_heartbeat > heartbeat_rate: + message = {"msg": "heartbeat"} + yield f"data: {json.dumps(message)}\n\n" + last_heartbeat = time.perf_counter() + + try: + await asyncio.wait_for( + file_upload_statuses.is_tracked(upload_id), timeout=3 + ) + except (asyncio.TimeoutError, TimeoutError): + return PlainTextResponse("Upload not found", status_code=404) + + return StreamingResponse( + sse_stream(request), + media_type="text/event-stream", + ) + + def set_upload_trace(session_hash: str, start: float): + import uuid + + from gradio.profiling import PROFILING_ENABLED, RequestTrace, collector + + if PROFILING_ENABLED: + trace = RequestTrace( + event_id=str(uuid.uuid4()), + fn_name="gradio_file_upload", + session_hash=session_hash, + ) + trace.upload_ms = (time.monotonic() - start) * 1000 + collector.add(trace) + + @router.post("/upload", dependencies=[Depends(login_check)]) + async def upload_file( + request: fastapi.Request, + bg_tasks: BackgroundTasks, + upload_id: str | None = None, + ): + start = None + if PROFILING_ENABLED: + start = time.monotonic() + try: + output_files, files_to_copy, locations = await upload_fn( + request, + app.uploaded_file_dir, + blocks.max_file_size + if blocks.max_file_size is not None + else math.inf, + upload_id, + force_move=False, + upload_progress=file_upload_statuses if upload_id else None, + ) + except MultiPartException as exc: + code = 413 if "maximum allowed size" in exc.message else 400 + return PlainTextResponse(exc.message, status_code=code) + + if files_to_copy: + bg_tasks.add_task( + move_uploaded_files_to_cache, files_to_copy, locations + ) + if PROFILING_ENABLED: + bg_tasks.add_task( + set_upload_trace, request.headers.get("session_hash", ""), start + ) + blocks.upload_file_set.update(output_files) + + return output_files + + @router.get("/startup-events") + async def startup_events(): + if not app.startup_events_triggered: + app.get_blocks().run_startup_events() + await app.get_blocks().run_extra_startup_events() + app.startup_events_triggered = True + return True + return False + + @router.get("/theme.css", response_class=PlainTextResponse) + @app.get("/theme.css", response_class=PlainTextResponse) + def theme_css(): + return PlainTextResponse(app.get_blocks().theme_css, media_type="text/css") + + @app.get("/robots.txt", response_class=PlainTextResponse) + def robots_txt(): + if app.get_blocks().share: + return "User-agent: *\nDisallow: /" + else: + return "User-agent: *\nDisallow: " + + @app.get("/pwa_icon") + @app.get("/pwa_icon/{size}") + async def pwa_icon(size: int | None = None): + blocks = app.get_blocks() + favicon_path = blocks.favicon_path + if favicon_path is None: + raise HTTPException(status_code=404) + + if size is None: + return FileResponse(favicon_path) + + import PIL.Image + + img = PIL.Image.open(favicon_path) + img = img.resize((size, size)) + + img_byte_array = io.BytesIO() + img.save(img_byte_array, format="PNG") + img_byte_array.seek(0) + + return StreamingResponse( + io.BytesIO(img_byte_array.read()), media_type="image/png" + ) + + @app.get("/manifest.json") + def manifest_json(): + favicon_path = blocks.favicon_path + if isinstance(favicon_path, Path): + favicon_path = str(favicon_path) + if favicon_path is None: + icons = [ + { + "src": "static/img/logo_nosize.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any", + }, + ] + elif favicon_path.endswith(".svg"): + icons = [ + { + "src": app.url_path_for("pwa_icon"), + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any", + }, + ] + else: + icons = [ + { + "src": app.url_path_for("pwa_icon", size=192), + "sizes": "192x192", + "type": "image/png", + "purpose": "any", + }, + { + "src": app.url_path_for("pwa_icon", size=512), + "sizes": "512x512", + "type": "image/png", + "purpose": "any", + }, + ] + + return ORJSONResponse( + content={ + # NOTE: Required members: https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable#required_manifest_members + "name": app.get_blocks().title or "Gradio", + "icons": icons, + "start_url": "./", + "display": "standalone", + }, + media_type="application/manifest+json", + ) + + @app.get("/monitoring", dependencies=[Depends(login_check)]) + async def analytics_login(request: fastapi.Request): + if not blocks.enable_monitoring: + raise HTTPException( + status_code=403, detail="Monitoring is not enabled." + ) + root_url = route_utils.get_root_url( + request=request, + route_path=f"{API_PREFIX}/monitoring", + root_path=app.root_path, + ) + monitoring_url = f"{root_url}/monitoring/{app.analytics_key}" + print(f"* Monitoring URL: {monitoring_url} *") + return HTMLResponse("See console for monitoring URL.") + + @app.get("/monitoring/summary") + async def _(): + return app.get_blocks()._queue.cached_event_analytics_summary + + @app.get("/monitoring/{key}") + async def analytics_dashboard(key: str): + if not blocks.enable_monitoring: + raise HTTPException( + status_code=403, detail="Monitoring is not enabled." + ) + if compare_passwords_securely(key, app.analytics_key): + analytics_url = f"/monitoring/{app.analytics_key}/dashboard" + if not app.monitoring_enabled: + from gradio.monitoring_dashboard import data + from gradio.monitoring_dashboard import demo as dashboard + + mount_gradio_app( + app, dashboard, path=analytics_url, mcp_server=False + ) + dashboard._queue.start() + analytics = app.get_blocks()._queue.event_analytics + data["data"] = analytics + app.monitoring_enabled = True + return RedirectResponse( + url=analytics_url, status_code=status.HTTP_302_FOUND + ) + else: + raise HTTPException(status_code=403, detail="Invalid key.") + + @router.post("/process_recording", dependencies=[Depends(login_check)]) + async def process_recording( + request: fastapi.Request, + ): + try: + content_type_header = request.headers.get("Content-Type") + content_type: bytes + content_type, _ = parse_options_header(content_type_header or "") + if content_type != b"multipart/form-data": + raise HTTPException(status_code=400, detail="Invalid content type.") + + app = request.app + max_file_size = ( + app.get_blocks().max_file_size + if hasattr(app, "get_blocks") + else None + ) + max_file_size = max_file_size if max_file_size is not None else math.inf + + multipart_parser = GradioMultiPartParser( + request.headers, + request.stream(), + max_files=1, + max_fields=10, + max_file_size=max_file_size, + ) + form = await multipart_parser.parse() + except MultiPartException as exc: + code = 413 if "maximum allowed size" in exc.message else 400 + return PlainTextResponse(exc.message, status_code=code) + + video_files = form.getlist("video") + if not video_files or not isinstance(video_files[0], GradioUploadFile): + raise HTTPException(status_code=400, detail="No video file provided") + + video_file = video_files[0] + + params = {} + if ( + form.get("remove_segment_start") is not None + and form.get("remove_segment_end") is not None + ): + params["remove_segment_start"] = form.get("remove_segment_start") + params["remove_segment_end"] = form.get("remove_segment_end") + + zoom_effects_json = form.get("zoom_effects") + if zoom_effects_json: + try: + params["zoom_effects"] = json.loads(str(zoom_effects_json)) + except json.JSONDecodeError: + params["zoom_effects"] = [] + + with tempfile.NamedTemporaryFile( + delete=False, suffix=".mp4", dir=DEFAULT_TEMP_DIR + ) as input_file: + video_file.file.seek(0) + shutil.copyfileobj(video_file.file, input_file) + input_path = input_file.name + + if shutil.which("ffmpeg") is None: + return FileResponse( + input_path, + media_type="video/mp4", + filename="gradio-screen-recording.mp4", + background=BackgroundTask(lambda: cleanup_files([input_path])), + ) + + output_path = tempfile.mkstemp( + suffix="_processed.mp4", dir=DEFAULT_TEMP_DIR + )[1] + + try: + processed_path, temp_files = await process_video_with_ffmpeg( + input_path, output_path, params + ) + + return FileResponse( + processed_path, + media_type="video/mp4", + filename="gradio-screen-recording.mp4", + background=BackgroundTask(lambda: cleanup_files(temp_files)), + ) + except Exception: + return FileResponse( + input_path, + media_type="video/mp4", + filename="gradio-screen-recording.mp4", + background=BackgroundTask(lambda: cleanup_files([input_path])), + ) + + vibe_edit_history_dir = Path(DEFAULT_TEMP_DIR) / "vibe_edit_history" + vibe_edit_history_dir.mkdir(exist_ok=True, parents=True) + chat_history = {"history": ""} + hash_to_chat_history = {} + + def limit_chat_history(history: str, max_pairs: int = 5) -> str: + """Limit chat history in the prompt to the last max_pairs user-assistant pairs.""" + if not history.strip(): + return "" + + user_messages = history.split("\nUser: ") + if len(user_messages) <= max_pairs: + return history + + recent_messages = user_messages[-max_pairs:] + + if len(recent_messages) > 0: + if recent_messages[0].startswith("User: "): + result = recent_messages[0] + else: + result = "User: " + recent_messages[0] + + for msg in recent_messages[1:]: + result += "\nUser: " + msg + + return result + + return "" + + control_token_re = re.compile(r"<\|[^>]*\|>") + final_start_re = re.compile( + r"<\|start\|>assistant<\|channel\|>final<\|message\|>", re.IGNORECASE + ) + end_re = re.compile(r"<\|end\|>", re.IGNORECASE) + reasoning_block_re = re.compile( + r"<\s*reasoning\s*>\s*(?P.*?)\s*<\s*/\s*reasoning\s*>", + re.IGNORECASE | re.DOTALL, + ) + think_block_re = re.compile( + r"<\s*think\s*>.*?<\s*/\s*think\s*>", re.IGNORECASE | re.DOTALL + ) + + # Remove analysis and weird markers from gpt-oss + def clean_out_markers(raw: str) -> str: + if not raw: + return raw + + m = final_start_re.search(raw) + if m: + text = raw[m.end() :] + m_end = end_re.search(text) + if m_end: + text = text[: m_end.start()] + return text.strip() + + text = control_token_re.sub("", raw) + return text.strip() + + def strip_think_blocks(text: str) -> str: + """Remove any ... blocks entirely""" + return think_block_re.sub("", text) + + def split_reasoning_code(text: str) -> tuple[str, str]: + """ + Extract all ... and code. If multiple, concatenate with blank lines, de-duping exact copies. + """ + reasoning_chunks = [] + seen = set() + for m in reasoning_block_re.finditer(text): + body = m.group("body").strip() + if body and body not in seen: + reasoning_chunks.append(body) + seen.add(body) + + reasoning_text = "\n\n".join(reasoning_chunks).strip() + code_text = reasoning_block_re.sub("", text).strip() + + return reasoning_text, code_text + + if blocks.vibe_mode: + from huggingface_hub import InferenceClient + + inference_client = InferenceClient() + + @router.post("/vibe-edit/") + @router.post("/vibe-edit") + async def vibe_edit(body: VibeEditBody): + if not blocks.vibe_mode: + raise HTTPException( + status_code=403, + detail="Vibe editor is not enabled. Use --vibe flag to enable.", + ) + + from gradio.http_server import GRADIO_WATCH_DEMO_PATH + + with open(GRADIO_WATCH_DEMO_PATH) as f: + original_code = f.read() + + snapshot_hash = secrets.token_hex(16) + snapshot_file = vibe_edit_history_dir / f"{snapshot_hash}.py" + + with open(snapshot_file, "w") as f: + f.write(original_code) + + hash_to_chat_history[snapshot_hash] = chat_history["history"] + + content = "" + limited_history = limit_chat_history(chat_history["history"]) + + prompt = f""" +You are a code generator for Gradio apps. Given the following existing code and prompt, return the full new code. +Existing code: +```python +{original_code} +``` + +Prompt: +{body.prompt} + +History: +{limited_history if limited_history else "No chat history."} +""" + + system_prompt = load_system_prompt() + content = ( + inference_client.chat_completion( + model="openai/gpt-oss-120b", + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt}, + ], + max_tokens=10000, + ) + .choices[0] + .message.content + ) + + if content is None: + raise HTTPException(status_code=500, detail="Error generating code") + + content = clean_out_markers(content) + content = strip_think_blocks(content) + + chat_history["history"] += f"\nUser: {body.prompt}\nAssistant: {content}\n" + + reasoning, content = split_reasoning_code(content) + + if "```python\n" in content: + start = content.index("```python\n") + len("```python\n") + end = content.find("\n```", start) + content = content[start:end] if end != -1 else content[start:] + + # Calculate diff stats + original_lines = original_code.splitlines(keepends=True) + new_lines = content.splitlines(keepends=True) + diff = list(difflib.unified_diff(original_lines, new_lines, n=0)) + + lines_added = 0 + lines_removed = 0 + for line in diff: + if line.startswith("+") and not line.startswith("+++"): + lines_added += 1 + elif line.startswith("-") and not line.startswith("---"): + lines_removed += 1 + + with open(GRADIO_WATCH_DEMO_PATH, "w") as f: + f.write(content) + + return { + "hash": snapshot_hash, + "diff_stats": { + "lines_added": lines_added, + "lines_removed": lines_removed, + }, + "reasoning": reasoning, + } + + @router.post("/undo-vibe-edit/") + @router.post("/undo-vibe-edit") + async def undo_vibe_edit(hash: str = Body(..., embed=True)): + if not blocks.vibe_mode: + raise HTTPException( + status_code=403, + detail="Vibe editor is not enabled. Use --vibe flag to enable.", + ) + + from gradio.http_server import GRADIO_WATCH_DEMO_PATH + + snapshot_file = vibe_edit_history_dir / f"{hash}.py" + + if not snapshot_file.exists(): + raise HTTPException(status_code=404, detail="Snapshot not found") + + # Restore the file from the snapshot + with open(snapshot_file) as f: + saved_content = f.read() + + with open(GRADIO_WATCH_DEMO_PATH, "w") as f: + f.write(saved_content) + + chat_history["history"] = hash_to_chat_history.get(hash, "") + + return {"success": True} + + @router.get("/vibe-code/") + @router.get("/vibe-code") + async def get_vibe_code(): + if not blocks.vibe_mode: + raise HTTPException( + status_code=403, + detail="Vibe editor is not enabled. Use --vibe flag to enable.", + ) + + from gradio.http_server import GRADIO_WATCH_DEMO_PATH + + try: + with open(GRADIO_WATCH_DEMO_PATH) as f: + code = f.read() + return {"code": code} + except FileNotFoundError: + raise HTTPException( + status_code=404, detail="Demo file not found" + ) from None + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error reading file: {str(e)}" + ) from e + + @router.post("/vibe-code/") + @router.post("/vibe-code") + async def update_vibe_code(body: VibeCodeBody): + if not blocks.vibe_mode: + raise HTTPException( + status_code=403, + detail="Vibe editor is not enabled. Use --vibe flag to enable.", + ) + + from gradio.http_server import GRADIO_WATCH_DEMO_PATH + + try: + with open(GRADIO_WATCH_DEMO_PATH, "w") as f: + f.write(body.code) + return {"success": True} + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error writing file: {str(e)}" + ) from e + + @router.post("/vibe-starter-queries/") + @router.post("/vibe-starter-queries") + async def get_vibe_starter_queries(): + if not blocks.vibe_mode: + raise HTTPException( + status_code=403, + detail="Vibe editor is not enabled. Use --vibe flag to enable.", + ) + + from gradio.http_server import GRADIO_WATCH_DEMO_PATH + + with open(GRADIO_WATCH_DEMO_PATH) as f: + code = f.read() + + prompt = f""" +You are a prompt generator for a gradio vibe editor. Given the following existing code, return a list of starter queries that can be used to generate a new code. +Existing code: +```python +{code} +``` +""" + + system_prompt = load_system_prompt(starter_queries=True) + content = ( + inference_client.chat_completion( + model="openai/gpt-oss-120b", + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt}, + ], + max_tokens=10000, + ) + .choices[0] + .message.content + ) + + if content is None: + raise HTTPException(status_code=500, detail="Error generating code") + + content = strip_think_blocks(content) + content = clean_out_markers(content) + + starter_queries = content.split("\n") + + return { + "starter_queries": starter_queries, + } + + def cleanup_files(files): + for file in files: + try: + if file and os.path.exists(file): + os.unlink(file) + except Exception as e: + print(f"Error cleaning up file {file}: {str(e)}") + + from gradio.profiling import PROFILING_ENABLED + + if PROFILING_ENABLED: + from gradio.profiling import collector + + @router.get("/profiling/traces") + async def profiling_traces( + last_n: int | None = None, + ): + return ORJSONResponse(collector.get_all(last_n=last_n)) + + @router.get("/profiling/summary") + async def profiling_summary(): + return ORJSONResponse(collector.get_summary()) + + @router.post("/profiling/clear") + async def profiling_clear(): + collector.clear() + return ORJSONResponse({"status": "cleared"}) + + app.include_router(router) + return app + + +######## +# Helper functions +######## + + +def load_system_prompt(starter_queries: bool = False): + prompt_rules = """Generate code for using the Gradio python library. + +The following RULES must be followed. Whenever you are forming a response, ensure all rules have been followed otherwise start over. + +RULES: +Respond with code written in valid Python syntax, along with one coherent explanation surrounded by tags. +Any text that is not code, should be surrounded by one large tag. +Never include backticks in your response such as ``` or ```python. +Do not include any code that is not necessary for the app to run. +Respond with a full Gradio app. +Respond with a full Gradio app using correct syntax and features of the latest Gradio version. DO NOT write code that doesn't follow the signatures listed. +Do not add comments explaining the code, unless they are very necessary to understand the code. +Make sure the code includes all necessary imports. +Clearly explain the changes, summary, or reasoning for the code you respond with, inside one large tag. Make sure it's easy to parse. Use markdown formatting when it makes sense, including bullet points if there are multiple changes. + + +Here's an example of a valid response: + + +I created a simple Gradio app that greets the user. It defines a function then creates a gradio interface and launches it. + + +import gradio as gr + +def greet(name): + return "Hello " + name + "!" + +demo = gr.Interface(fn=greet, inputs="textbox", outputs="textbox") + +demo.launch() +""" + if starter_queries: + prompt_rules = """ + You are a prompt generator for a gradio vibe editor. + + Given python code of a gradio app, return a list of starter queries that can be used to generate new code. + Make sure the queries are short, useful and actually possible with Gradio. + The queries should be really simple and easy to understand. + You should respond with at most three queries, each on a new line. Do not include any other text. + Make sure the features you suggest are actually supported by Gradio, and documented in the docs section below. + Never suggest a query with more than one gradio feature or concept. + You may suggest queries that are not related to Gradio, but they must be related to the existing code and app. Never suggest queries that require external packages or libraries other than gradio. + Don't suggest adding a clear button if the app is an Interface, because Interface already has a clear button. + + Here's an example of a gradio app: + + ```python + import gradio as gr + + def greet(name): + return "Hello " + name + "!" + + demo = gr.Interface(fn=greet, inputs="textbox", outputs="textbox") + + demo.launch() + ``` + + Here's an example of a valid response: + Add a title to the app + Add examples + Rewrite this app using Blocks + + Here's an example of another valid response: + Add another textbox for name + Change the theme + Greet the user in many languages + + """ + try: + with httpx.Client() as client: + response = client.get("https://www.gradio.app/llms.txt") + system_prompt = response.text + except Exception: + system_prompt = "" + system_prompt = prompt_rules + system_prompt + prompt_rules + return system_prompt + + +def get_types(cls_set: list[type]): + docset = [] + types = [] + for cls in cls_set: + doc = inspect.getdoc(cls) or "" + doc_lines = doc.split("\n") + for line in doc_lines: + if "value (" in line: + types.append(line.split("value (")[1].split(")")[0]) + docset.append(doc_lines[1].split(":")[-1]) + return docset, types + + +@document() +def mount_gradio_app( + app: fastapi.FastAPI, + blocks: gradio.Blocks, + path: str, + server_name: str = "0.0.0.0", + server_port: int = 7860, + footer_links: ( + list[Literal["api", "gradio", "settings"] | dict[str, str]] | None + ) = None, + app_kwargs: dict[str, Any] | None = None, + *, + auth: Callable | tuple[str, str] | list[tuple[str, str]] | None = None, + auth_message: str | None = None, + auth_dependency: Callable[[fastapi.Request], str | None] | None = None, + root_path: str | None = None, + allowed_paths: list[str] | None = None, + blocked_paths: list[str] | None = None, + favicon_path: str | None = None, + show_error: bool = True, + max_file_size: str | int | None = None, + ssr_mode: bool | None = None, + node_server_name: str | None = None, + node_port: int | None = None, + enable_monitoring: bool | None = None, + pwa: bool | None = None, + i18n: I18n | None = None, + mcp_server: bool | None = None, + theme: Theme | str | None = None, + css: str | None = None, + css_paths: str | Path | Sequence[str | Path] | None = None, + js: str | Literal[True] | None = None, + head: str | None = None, + head_paths: str | Path | Sequence[str | Path] | None = None, +) -> fastapi.FastAPI: + """Mount a gradio.Blocks to an existing FastAPI application. + + Parameters: + app: The parent FastAPI application. + blocks: The blocks object we want to mount to the parent app. + path: The path at which the gradio application will be mounted, e.g. "/gradio". + server_name: The server name on which the Gradio app will be run. + server_port: The port on which the Gradio app will be run. + app_kwargs: Additional keyword arguments to pass to the underlying FastAPI app as a dictionary of parameter keys and argument values. For example, `{"docs_url": "/docs"}` + auth: If provided, username and password (or list of username-password tuples) required to access the gradio app. Can also provide function that takes username and password and returns True if valid login. + auth_message: If provided, HTML message provided on login page for this gradio app. + auth_dependency: A function that takes a FastAPI request and returns a string user ID or None. If the function returns None for a specific request, that user is not authorized to access the gradio app (they will see a 401 Unauthorized response). To be used with external authentication systems like OAuth. Cannot be used with `auth`. + root_path: The subpath corresponding to the public deployment of this FastAPI application. For example, if the application is served at "https://example.com/myapp", the `root_path` should be set to "/myapp". A full URL beginning with http:// or https:// can be provided, which will be used in its entirety. Normally, this does not need to provided (even if you are using a custom `path`). However, if you are serving the FastAPI app behind a proxy, the proxy may not provide the full path to the Gradio app in the request headers. In which case, you can provide the root path here. + allowed_paths: List of complete filepaths or parent directories that this gradio app is allowed to serve. Must be absolute paths. Warning: if you provide directories, any files in these directories or their subdirectories are accessible to all users of your app. + blocked_paths: List of complete filepaths or parent directories that this gradio app is not allowed to serve (i.e. users of your app are not allowed to access). Must be absolute paths. Warning: takes precedence over `allowed_paths` and all other directories exposed by Gradio by default. + favicon_path: If a path to a file (.png, .gif, or .ico) is provided, it will be used as the favicon for this gradio app's page. + show_error: If True, any errors in the gradio app will be displayed in an alert modal and printed in the browser console log. Otherwise, errors will only be visible in the terminal session running the Gradio app. + max_file_size: The maximum file size in bytes that can be uploaded. Can be a string of the form "", where value is any positive integer and unit is one of "b", "kb", "mb", "gb", "tb". If None, no limit is set. + footer_links: The links to display in the footer of the app. Accepts a list, where each element of the list must be one of "api", "gradio", or "settings" corresponding to the API docs, "built with Gradio", and settings pages respectively. If None, all three links will be shown in the footer. An empty list means that no footer is shown. + ssr_mode: If True, the Gradio app will be rendered using server-side rendering mode, which is typically more performant and provides better SEO, but this requires Node 20+ to be installed on the system. If False, the app will be rendered using client-side rendering mode. If None, will use GRADIO_SSR_MODE environment variable or default to False. + node_server_name: The name of the Node server to use for SSR. If None, will use GRADIO_NODE_SERVER_NAME environment variable or search for a node binary in the system. + i18n: If provided, the i18n instance to use for this gradio app. + node_port: The port on which the Node server should run. If None, will use GRADIO_NODE_SERVER_PORT environment variable or find a free port. + mcp_server: If True, the MCP server will be launched on the gradio app. If None, will use GRADIO_MCP_SERVER environment variable or default to False. + theme: A Theme object or a string representing a theme. If a string, will look for a built-in theme with that name (e.g. "soft" or "default"), or will attempt to load a theme from the Hugging Face Hub (e.g. "gradio/monochrome"). If None, will use the Default theme. + css: Custom css as a code string. This css will be included in the demo webpage. + css_paths: Custom css as a pathlib.Path to a css file or a list of such paths. This css files will be read, concatenated, and included in the demo webpage. If the `css` parameter is also set, the css from `css` will be included first. + js: Custom js as a code string. The custom js should be in the form of a single js function. This function will automatically be executed when the page loads. For more flexibility, use the head parameter to insert js inside ', +) +``` + +## Server Functions + +You can call Python functions directly from your `js_on_load` code using the `server_functions` parameter. Pass a list of Python functions to `server_functions`, and they become available as async methods on a `server` object inside `js_on_load`. + +$code_html_server_functions +$demo_html_server_functions + + +## Component Classes + +If you are reusing the same HTML component in multiple places, you can create a custom component class by subclassing `gr.HTML` and setting default values for the templates and other arguments. Here's an example of creating a reusable StarRating component. + +$code_star_rating_component +$demo_star_rating_component + +Note: Gradio requires all components to accept certain arguments, such as `render`. You do not need +to handle these arguments, but you do need to accept them in your component constructor and pass +them to the parent `gr.HTML` class. Otherwise, your component may not behave correctly. The easiest +way is to add `**kwargs` to your `__init__` method and pass it to `super().__init__()`, just like in the code example above. + +We've created several custom HTML components as reusable components as examples you can reference in [this directory](https://github.com/gradio-app/gradio/tree/main/gradio/components/custom_html_components). + + +## Embedding Components in HTML + +The `gr.HTML` component can also be used as a container for other Gradio components using the `@children` placeholder. This allows you to create custom layouts with HTML/CSS. + +The `@children` must be at the top-level of the `html_template`. Since children cannot be nested inside the template, target the parent element directly with your CSS and JavaScript if you need to style or interact with the container of the children. + +Here's a basic example: + +$code_html_children +$demo_html_children + +In this example, the `@children` placeholder marks where the child components (the Name and Email textboxes) will be rendered. Notice how in the `css_template` we target the parent element to style the container div that wraps the children. + + +### API / MCP support + +To make your custom HTML component work with Gradio's built-in support for API and MCP (Model Context Protocol) usage, you need to define how its data should be serialized. There are two ways to do this: + +**Option 1: Define an `api_info()` method** + +Add an `api_info()` method that returns a JSON schema dictionary describing your component's data format. This is what we do in the StarRating class above. + +**Option 2: Define a Pydantic data model** + +For more complex data structures, you can define a Pydantic model that inherits from `GradioModel` or `GradioRootModel`: + +```python +from gradio.data_classes import GradioModel, GradioRootModel + +class MyComponentData(GradioModel): + items: List[str] + count: int + +class MyComponent(gr.HTML): + data_model = MyComponentData +``` + +Use `GradioModel` when your data is a dictionary with named fields, or `GradioRootModel` when your data is a simple type (string, list, etc.) that doesn't need to be wrapped in a dictionary. By defining a `data_model`, your component automatically implements API methods. + +## Sharing Components with `push_to_hub` + +Once you've built a custom HTML component, you can share it with the community by pushing it to the [HTML Components Gallery](https://www.gradio.app/custom-components/html-gallery). The gallery lets anyone browse, interact with, and copy the Python code for community-contributed components. + +Call `push_to_hub` on any `gr.HTML` instance or subclass: + +```python +star_rating = StarRating() +star_rating.push_to_hub( + name="Star Rating", + description="Interactive 5-star rating with click-to-rate", + author="your-hf-username", + tags=["input", "rating"], + repo_url="https://github.com/your-username/your-repo", +) +``` + +This opens a pull request on the gallery's HuggingFace dataset repo. Once approved, your component will appear in the gallery for others to discover and use. + +Tip: The `push_to_hub` method has a `head` parameter that deserves special attention. If your component uses an external library loaded via the `head` parameter of `launch` (e.g. `head=''`), pass the same `head` string to `push_to_hub` so that the gallery can load those scripts when rendering your component. + +### Authentication + +You need a HuggingFace **write token** to push components. Either pass it directly: + +```python +star_rating.push_to_hub(..., token="hf_xxxxx") +``` + +Or log in beforehand with the HuggingFace CLI, and the cached token will be used automatically: + +```bash +huggingface-cli login +``` + +## Security Considerations + +Keep in mind that using `gr.HTML` to create custom components involves injecting raw HTML and JavaScript into your Gradio app. Be cautious about using untrusted user input into `html_template` and `js_on_load`, as this could lead to cross-site scripting (XSS) vulnerabilities. + +You should also expect that any Python event listeners that take your `gr.HTML` component as input could have any arbitrary value passed to them, not just the values you expect the frontend to be able to set for `value`. Sanitize and validate user input appropriately in public applications. + +## Next Steps + +- Browse the [HTML Components Gallery](https://www.gradio.app/custom-components/html-gallery) to see what the community has built and copy components into your own apps. +- Check out more examples in [this directory](https://github.com/gradio-app/gradio/tree/main/gradio/components/custom_html_components). +- Share your own components with `push_to_hub` to help others! \ No newline at end of file diff --git a/guides/03_building-with-blocks/07_custom-CSS-and-JS.md b/guides/03_building-with-blocks/07_custom-CSS-and-JS.md new file mode 100644 index 0000000..dff5775 --- /dev/null +++ b/guides/03_building-with-blocks/07_custom-CSS-and-JS.md @@ -0,0 +1,163 @@ +# Customizing your demo with CSS and Javascript + +Gradio allows you to customize your demo in several ways. You can customize the layout of your demo, add custom HTML, and add custom theming as well. This tutorial will go beyond that and walk you through how to add custom CSS and JavaScript code to your demo in order to add custom styling, animations, custom UI functionality, analytics, and more. + +## Adding custom CSS to your demo + +Gradio themes are the easiest way to customize the look and feel of your app. You can choose from a variety of themes, or create your own. To do so, pass the `theme=` kwarg to the `launch()` method of the `Blocks` constructor. For example: + +```python +with gr.Blocks() as demo: + ... # your code here +demo.launch(theme=gr.themes.Glass()) + ... +``` + +Gradio comes with a set of prebuilt themes which you can load from `gr.themes.*`. You can extend these themes or create your own themes from scratch - see the [Theming guide](/guides/theming-guide) for more details. + +For additional styling ability, you can pass any CSS to your app as a string using the `css=` kwarg in the `launch()` method. You can also pass a pathlib.Path to a css file or a list of such paths to the `css_paths=` kwarg in the `launch()` method. + +**Warning**: The use of query selectors in custom JS and CSS is _not_ guaranteed to work across Gradio versions that bind to Gradio's own HTML elements as the Gradio HTML DOM may change. We recommend using query selectors sparingly. + +The base class for the Gradio app is `gradio-container`, so here's an example that changes the background color of the Gradio app: + +```python +with gr.Blocks() as demo: + ... # your code here +demo.launch(css=".gradio-container {background-color: red}") + ... +``` + +If you'd like to reference external files in your css, preface the file path (which can be a relative or absolute path) with `"/gradio_api/file="`, for example: + +```python +with gr.Blocks() as demo: + ... # your code here +demo.launch(css=".gradio-container {background: url('/gradio_api/file=clouds.jpg')}") + ... +``` + +Note: By default, most files in the host machine are not accessible to users running the Gradio app. As a result, you should make sure that any referenced files (such as `clouds.jpg` here) are either URLs or [allowed paths, as described here](/main/guides/file-access). + + +## The `elem_id` and `elem_classes` Arguments + +You can `elem_id` to add an HTML element `id` to any component, and `elem_classes` to add a class or list of classes. This will allow you to select elements more easily with CSS. This approach is also more likely to be stable across Gradio versions as built-in class names or ids may change (however, as mentioned in the warning above, we cannot guarantee complete compatibility between Gradio versions if you use custom CSS as the DOM elements may themselves change). + +```python +css = """ +#warning {background-color: #FFCCCB} +.feedback textarea {font-size: 24px !important} +""" + +with gr.Blocks() as demo: + box1 = gr.Textbox(value="Good Job", elem_classes="feedback") + box2 = gr.Textbox(value="Failure", elem_id="warning", elem_classes="feedback") +demo.launch(css=css) +``` + +The CSS `#warning` ruleset will only target the second Textbox, while the `.feedback` ruleset will target both. Note that when targeting classes, you might need to put the `!important` selector to override the default Gradio styles. + +## Adding custom JavaScript to your demo + +There are 3 ways to add javascript code to your Gradio demo: + +1. You can add JavaScript code as a string to the `js` parameter of the `Blocks` or `Interface` initializer. This will run the JavaScript code when the demo is first loaded. + +Below is an example of adding custom js to show an animated welcome message when the demo first loads. + +$code_blocks_js_load +$demo_blocks_js_load + + +2. When using `Blocks` and event listeners, events have a `js` argument that can take a JavaScript function as a string and treat it just like a Python event listener function. You can pass both a JavaScript function and a Python function (in which case the JavaScript function is run first) or only Javascript (and set the Python `fn` to `None`). Take a look at the code below: + +$code_blocks_js_methods +$demo_blocks_js_methods + +3. Lastly, you can add JavaScript code to the `head` param of the `Blocks` initializer. This will add the code to the head of the HTML document. For example, you can add Google Analytics to your demo like so: + + +```python +head = f""" + + +""" + +with gr.Blocks() as demo: + gr.HTML("

My App

") + +demo.launch(head=head) +``` + +The `head` parameter accepts any HTML tags you would normally insert into the `` of a page. For example, you can also include `` tags to `head` in order to update the social sharing preview for your Gradio app like this: + +```py +import gradio as gr + +custom_head = """ + +Sample App + + + + + + + + + + + + + + + + + +""" + +with gr.Blocks(title="My App") as demo: + gr.HTML("

My App

") + +demo.launch(head=custom_head) +``` + + + +Note that injecting custom JS can affect browser behavior and accessibility (e.g. keyboard shortcuts may be lead to unexpected behavior if your Gradio app is embedded in another webpage). You should test your interface across different browsers and be mindful of how scripts may interact with browser defaults. Here's an example where pressing `Shift + s` triggers the `click` event of a specific `Button` component if the browser focus is _not_ on an input component (e.g. `Textbox` component): + +```python +import gradio as gr + +shortcut_js = """ + +""" + +with gr.Blocks() as demo: + action_button = gr.Button(value="Name", elem_id="my_btn") + textbox = gr.Textbox() + action_button.click(lambda : "button pressed", None, textbox) + +demo.launch(head=shortcut_js) +``` + diff --git a/guides/04_additional-features/01_queuing.md b/guides/04_additional-features/01_queuing.md new file mode 100644 index 0000000..9f84bae --- /dev/null +++ b/guides/04_additional-features/01_queuing.md @@ -0,0 +1,48 @@ +# Queuing + +Every Gradio app comes with a built-in queuing system that can scale to thousands of concurrent users. Because many of your event listeners may involve heavy processing, Gradio automatically creates a queue to handle every event listener in the backend. Every event listener in your app automatically has a queue to process incoming events. + +## Configuring the Queue + +By default, each event listener has its own queue, which handles one request at a time. This can be configured via two arguments: + +- `concurrency_limit`: This sets the maximum number of concurrent executions for an event listener. By default, the limit is 1 unless configured otherwise in `Blocks.queue()`. You can also set it to `None` for no limit (i.e., an unlimited number of concurrent executions). For example: + +```python +import gradio as gr + +with gr.Blocks() as demo: + prompt = gr.Textbox() + image = gr.Image() + generate_btn = gr.Button("Generate Image") + generate_btn.click(image_gen, prompt, image, concurrency_limit=5) +``` + +In the code above, up to 5 requests can be processed simultaneously for this event listener. Additional requests will be queued until a slot becomes available. + +If you want to manage multiple event listeners using a shared queue, you can use the `concurrency_id` argument: + +- `concurrency_id`: This allows event listeners to share a queue by assigning them the same ID. For example, if your setup has only 2 GPUs but multiple functions require GPU access, you can create a shared queue for all those functions. Here's how that might look: + +```python +import gradio as gr + +with gr.Blocks() as demo: + prompt = gr.Textbox() + image = gr.Image() + generate_btn_1 = gr.Button("Generate Image via model 1") + generate_btn_2 = gr.Button("Generate Image via model 2") + generate_btn_3 = gr.Button("Generate Image via model 3") + generate_btn_1.click(image_gen_1, prompt, image, concurrency_limit=2, concurrency_id="gpu_queue") + generate_btn_2.click(image_gen_2, prompt, image, concurrency_id="gpu_queue") + generate_btn_3.click(image_gen_3, prompt, image, concurrency_id="gpu_queue") +``` + +In this example, all three event listeners share a queue identified by `"gpu_queue"`. The queue can handle up to 2 concurrent requests at a time, as defined by the `concurrency_limit`. + +### Notes + +- To ensure unlimited concurrency for an event listener, set `concurrency_limit=None`. This is useful if your function is calling e.g. an external API which handles the rate limiting of requests itself. +- The default concurrency limit for all queues can be set globally using the `default_concurrency_limit` parameter in `Blocks.queue()`. + +These configurations make it easy to manage the queuing behavior of your Gradio app. diff --git a/guides/04_additional-features/02_streaming-outputs.md b/guides/04_additional-features/02_streaming-outputs.md new file mode 100644 index 0000000..ab0cd1f --- /dev/null +++ b/guides/04_additional-features/02_streaming-outputs.md @@ -0,0 +1,74 @@ +# Streaming outputs + +In some cases, you may want to stream a sequence of outputs rather than show a single output at once. For example, you might have an image generation model and you want to show the image that is generated at each step, leading up to the final image. Or you might have a chatbot which streams its response one token at a time instead of returning it all at once. + +In such cases, you can supply a **generator** function into Gradio instead of a regular function. Creating generators in Python is very simple: instead of a single `return` value, a function should `yield` a series of values instead. Usually the `yield` statement is put in some kind of loop. Here's an example of an generator that simply counts up to a given number: + +```python +def my_generator(x): + for i in range(x): + yield i +``` + +You supply a generator into Gradio the same way as you would a regular function. For example, here's a a (fake) image generation model that generates noise for several steps before outputting an image using the `gr.Interface` class: + +$code_fake_diffusion +$demo_fake_diffusion + +Note that we've added a `time.sleep(1)` in the iterator to create an artificial pause between steps so that you are able to observe the steps of the iterator (in a real image generation model, this probably wouldn't be necessary). + +Similarly, Gradio can handle streaming inputs, e.g. an image generation model that reruns every time a user types a letter in a textbox. This is covered in more details in our guide on building [reactive Interfaces](/guides/reactive-interfaces). + +## Streaming Media + +Gradio can stream audio and video directly from your generator function. +This lets your user hear your audio or see your video nearly as soon as it's `yielded` by your function. +All you have to do is + +1. Set `streaming=True` in your `gr.Audio` or `gr.Video` output component. +2. Write a python generator that yields the next "chunk" of audio or video. +3. Set `autoplay=True` so that the media starts playing automatically. + +For audio, the next "chunk" can be either an `.mp3` or `.wav` file or a `bytes` sequence of audio. +For video, the next "chunk" has to be either `.mp4` file or a file with `h.264` codec with a `.ts` extension. +For smooth playback, make sure chunks are consistent lengths and larger than 1 second. + +We'll finish with some simple examples illustrating these points. + +### Streaming Audio + +```python +import gradio as gr +from time import sleep + +def keep_repeating(audio_file): + for _ in range(10): + sleep(0.5) + yield audio_file + +gr.Interface(keep_repeating, + gr.Audio(sources=["microphone"], type="filepath"), + gr.Audio(streaming=True, autoplay=True) +).launch() +``` + +### Streaming Video + +```python +import gradio as gr +from time import sleep + +def keep_repeating(video_file): + for _ in range(10): + sleep(0.5) + yield video_file + +gr.Interface(keep_repeating, + gr.Video(sources=["webcam"], format="mp4"), + gr.Video(streaming=True, autoplay=True) +).launch() +``` + +## End-to-End Examples + +For an end-to-end example of streaming media, see the object detection from video [guide](/main/guides/object-detection-from-video) or the streaming AI-generated audio with [transformers](https://huggingface.co/docs/transformers/index) [guide](/main/guides/streaming-ai-generated-audio). \ No newline at end of file diff --git a/guides/04_additional-features/03_streaming-inputs.md b/guides/04_additional-features/03_streaming-inputs.md new file mode 100644 index 0000000..0d5ed7a --- /dev/null +++ b/guides/04_additional-features/03_streaming-inputs.md @@ -0,0 +1,70 @@ +# Streaming inputs + +Tip: Check out [FastRTC](https://fastrtc.org/), our companion library for building low latency streaming web apps with a familiar Gradio syntax. + +In the previous guide, we covered how to stream a sequence of outputs from an event handler. Gradio also allows you to stream images from a user's camera or audio chunks from their microphone **into** your event handler. This can be used to create real-time object detection apps or conversational chat applications with Gradio. + +Currently, the `gr.Image` and the `gr.Audio` components support input streaming via the `stream` event. +Let's create the simplest streaming app possible, which simply returns the webcam stream unmodified. + +$code_streaming_simple +$demo_streaming_simple + +Try it out! The streaming event is triggered when the user starts recording. Under the hood, the webcam will take a photo every 0.1 seconds and send it to the server. The server will then return that image. + +There are two unique keyword arguments for the `stream` event: + +* `time_limit` - This is the amount of time the gradio server will spend processing the event. Media streams are naturally unbounded so it's important to set a time limit so that one user does not hog the Gradio queue. The time limit only counts the time spent processing the stream, not the time spent waiting in the queue. The orange bar displayed at the bottom of the input image represents the remaining time. When the time limit expires, the user will automatically rejoin the queue. + +* `stream_every` - This is the frequency (in seconds) with which the stream will capture input and send it to the server. For demos like image detection or manipulation, setting a smaller value is desired to get a "real-time" effect. For demos like speech transcription, a higher value is useful so that the transcription algorithm has more context of what's being said. + +## A Realistic Image Demo + +Let's create a demo where a user can choose a filter to apply to their webcam stream. Users can choose from an edge-detection filter, a cartoon filter, or simply flipping the stream vertically. + +$code_streaming_filter +$demo_streaming_filter + +You will notice that if you change the filter value it will immediately take effect in the output stream. That is an important difference of stream events in comparison to other Gradio events. The input values of the stream can be changed while the stream is being processed. + +Tip: We set the "streaming" parameter of the image output component to be "True". Doing so lets the server automatically convert our output images into base64 format, a format that is efficient for streaming. + +## Unified Image Demos + +For some image streaming demos, like the one above, we don't need to display separate input and output components. Our app would look cleaner if we could just display the modified output stream. + +We can do so by just specifying the input image component as the output of the stream event. + +$code_streaming_filter_unified +$demo_streaming_filter_unified + +## Keeping track of past inputs or outputs + +Your streaming function should be stateless. It should take the current input and return its corresponding output. However, there are cases where you may want to keep track of past inputs or outputs. For example, you may want to keep a buffer of the previous `k` inputs to improve the accuracy of your transcription demo. You can do this with Gradio's `gr.State()` component. + +Let's showcase this with a sample demo: + +```python +def transcribe_handler(current_audio, state, transcript): + next_text = transcribe(current_audio, history=state) + state.append(current_audio) + state = state[-3:] + return state, transcript + next_text + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + mic = gr.Audio(sources="microphone") + state = gr.State(value=[]) + with gr.Column(): + transcript = gr.Textbox(label="Transcript") + mic.stream(transcribe_handler, [mic, state, transcript], [state, transcript], + time_limit=10, stream_every=1) + + +demo.launch() +``` + +## End-to-End Examples + +For an end-to-end example of streaming from the webcam, see the object detection from webcam [guide](/main/guides/object-detection-from-webcam-with-webrtc). \ No newline at end of file diff --git a/guides/04_additional-features/04_alerts.md b/guides/04_additional-features/04_alerts.md new file mode 100644 index 0000000..761a6f9 --- /dev/null +++ b/guides/04_additional-features/04_alerts.md @@ -0,0 +1,18 @@ +# Alerts + +You may wish to display alerts to the user. To do so, raise a `gr.Error("custom message")` in your function to halt the execution of your function and display an error message to the user. + +You can also issue `gr.Warning("custom message")` or `gr.Info("custom message")` by having them as standalone lines in your function, which will immediately display modals while continuing the execution of your function. The only difference between `gr.Info()` and `gr.Warning()` is the color of the alert. + +```python +def start_process(name): + gr.Info("Starting process") + if name is None: + gr.Warning("Name is empty") + ... + if success == False: + raise gr.Error("Process failed") +``` + +Tip: Note that `gr.Error()` is an exception that has to be raised, while `gr.Warning()` and `gr.Info()` are functions that are called directly. + diff --git a/guides/04_additional-features/05_progress-bars.md b/guides/04_additional-features/05_progress-bars.md new file mode 100644 index 0000000..44d71d4 --- /dev/null +++ b/guides/04_additional-features/05_progress-bars.md @@ -0,0 +1,8 @@ +# Progress Bars + +Gradio supports the ability to create custom Progress Bars so that you have customizability and control over the progress update that you show to the user. In order to enable this, simply add an argument to your method that has a default value of a `gr.Progress` instance. Then you can update the progress levels by calling this instance directly with a float between 0 and 1, or using the `tqdm()` method of the `Progress` instance to track progress over an iterable, as shown below. + +$code_progress_simple +$demo_progress_simple + +If you use the `tqdm` library, you can even report progress updates automatically from any `tqdm.tqdm` that already exists within your function by setting the default argument as `gr.Progress(track_tqdm=True)`! diff --git a/guides/04_additional-features/06_batch-functions.md b/guides/04_additional-features/06_batch-functions.md new file mode 100644 index 0000000..6e7e66f --- /dev/null +++ b/guides/04_additional-features/06_batch-functions.md @@ -0,0 +1,59 @@ +# Batch functions + +Gradio supports the ability to pass _batch_ functions. Batch functions are just +functions which take in a list of inputs and return a list of predictions. + +For example, here is a batched function that takes in two lists of inputs (a list of +words and a list of ints), and returns a list of trimmed words as output: + +```py +import time + +def trim_words(words, lens): + trimmed_words = [] + time.sleep(5) + for w, l in zip(words, lens): + trimmed_words.append(w[:int(l)]) + return [trimmed_words] +``` + +The advantage of using batched functions is that if you enable queuing, the Gradio server can automatically _batch_ incoming requests and process them in parallel, +potentially speeding up your demo. Here's what the Gradio code looks like (notice the `batch=True` and `max_batch_size=16`) + +With the `gr.Interface` class: + +```python +demo = gr.Interface( + fn=trim_words, + inputs=["textbox", "number"], + outputs=["output"], + batch=True, + max_batch_size=16 +) + +demo.launch() +``` + +With the `gr.Blocks` class: + +```py +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + word = gr.Textbox(label="word") + leng = gr.Number(label="leng") + output = gr.Textbox(label="Output") + with gr.Row(): + run = gr.Button() + + event = run.click(trim_words, [word, leng], output, batch=True, max_batch_size=16) + +demo.launch() +``` + +In the example above, 16 requests could be processed in parallel (for a total inference time of 5 seconds), instead of each request being processed separately (for a total +inference time of 80 seconds). Many Hugging Face `transformers` and `diffusers` models work very naturally with Gradio's batch mode: here's [an example demo using diffusers to +generate images in batches](https://github.com/gradio-app/gradio/blob/main/demo/diffusers_with_batching/run.py) + + diff --git a/guides/04_additional-features/07_sharing-your-app.md b/guides/04_additional-features/07_sharing-your-app.md new file mode 100644 index 0000000..2c7da82 --- /dev/null +++ b/guides/04_additional-features/07_sharing-your-app.md @@ -0,0 +1,476 @@ +# Sharing Your App + +In this Guide, we dive more deeply into the various aspects of sharing a Gradio app with others. We will cover: + +1. [Sharing demos with the share parameter](#sharing-demos) +2. [Hosting on HF Spaces](#hosting-on-hf-spaces) +3. [Sharing Deep Links](#sharing-deep-links) +4. [Embedding hosted spaces](#embedding-hosted-spaces) +5. [Using the API page](#api-page) +6. [Accessing network requests](#accessing-the-network-request-directly) +7. [Mounting within FastAPI](#mounting-within-another-fast-api-app) +8. [Authentication](#authentication) +9. [MCP Servers](#mcp-servers) +10. [Rate Limits](#rate-limits) +11. [Analytics](#analytics) +12. [Progressive Web Apps (PWAs)](#progressive-web-app-pwa) + +## Sharing Demos + +Gradio demos can be easily shared publicly by setting `share=True` in the `launch()` method. Like this: + +```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 🚀 +``` + +This generates a public, shareable link that you can send to anybody! When you send this link, the user on the other side can try out the model in their browser. Because the processing happens on your device (as long as your device stays on), you don't have to worry about any packaging any dependencies. + +![sharing](https://github.com/gradio-app/gradio/blob/main/guides/assets/sharing.svg?raw=true) + + +A share link usually looks something like this: **https://07ff8706ab.gradio.live**. Although the link is served through the Gradio Share Servers, these servers are only a proxy for your local server, and do not store any data sent through your app. Share links expire after 1 week. (it is [also possible to set up your own Share Server](https://github.com/huggingface/frp/) on your own cloud server to overcome this restriction.) + +Tip: Keep in mind that share links are publicly accessible, meaning that anyone can use your model for prediction! Therefore, make sure not to expose any sensitive information through the functions you write, or allow any critical changes to occur on your device. Or you can [add authentication to your Gradio app](#authentication) as discussed below. + +Note that by default, `share=False`, which means that your server is only running locally. (This is the default, except in Google Colab notebooks, where share links are automatically created). As an alternative to using share links, you can use use [SSH port-forwarding](https://www.ssh.com/ssh/tunneling/example) to share your local server with specific users. + + +## Hosting on HF Spaces + +If you'd like to have a permanent link to your Gradio demo on the internet, use Hugging Face Spaces. [Hugging Face Spaces](http://huggingface.co/spaces/) provides the infrastructure to permanently host your machine learning model for free! + +After you have [created a free Hugging Face account](https://huggingface.co/join), you have two methods to deploy your Gradio app to Hugging Face Spaces: + +1. From terminal: run `gradio deploy` in your app directory. The CLI will gather some basic metadata, upload all the files in the current directory (respecting any `.gitignore` file that may be present in the root of the directory), and then launch your app on Spaces. To update your Space, you can re-run this command or enable the Github Actions option in the CLI to automatically update the Spaces on `git push`. + +2. From your browser: Drag and drop a folder containing your Gradio model and all related files [here](https://huggingface.co/new-space). See [this guide how to host on Hugging Face Spaces](https://huggingface.co/blog/gradio-spaces) for more information, or watch the embedded video: + + + +## Sharing Deep Links + +You can add a button to your Gradio app that creates a unique URL you can use to share your app and all components **as they currently are** with others. This is useful for sharing unique and interesting generations from your application , or for saving a snapshot of your app at a particular point in time. + +To add a deep link button to your app, place the `gr.DeepLinkButton` component anywhere in your app. +For the URL to be accessible to others, your app must be available at a public URL. So be sure to host your app like Hugging Face Spaces or use the `share=True` parameter when launching your app. + +Let's see an example of how this works. Here's a simple Gradio chat ap that uses the `gr.DeepLinkButton` component. After a couple of messages, click the deep link button and paste it into a new browser tab to see the app as it is at that point in time. + +$code_deep_link +$demo_deep_link + + +## Embedding Hosted Spaces + +Once you have hosted your app on Hugging Face Spaces (or on your own server), you may want to embed the demo on a different website, such as your blog or your portfolio. Embedding an interactive demo allows people to try out the machine learning model that you have built, without needing to download or install anything — right in their browser! The best part is that you can embed interactive demos even in static websites, such as GitHub pages. + +There are two ways to embed your Gradio demos. You can find quick links to both options directly on the Hugging Face Space page, in the "Embed this Space" dropdown option: + +![Embed this Space dropdown option](https://github.com/gradio-app/gradio/blob/main/guides/assets/embed_this_space.png?raw=true) + +### Embedding with Web Components + +Web components typically offer a better experience to users than IFrames. Web components load lazily, meaning that they won't slow down the loading time of your website, and they automatically adjust their height based on the size of the Gradio app. + +To embed with Web Components: + +1. Import the gradio JS library into into your site by adding the script below in your site (replace {GRADIO_VERSION} in the URL with the library version of Gradio you are using). + +```html + +``` + +2. Add + +```html + +``` + +element where you want to place the app. Set the `src=` attribute to your Space's embed URL, which you can find in the "Embed this Space" button. For example: + +```html + +``` + + + +You can see examples of how web components look on the Gradio landing page. + +You can also customize the appearance and behavior of your web component with attributes that you pass into the `` tag: + +- `src`: as we've seen, the `src` attributes links to the URL of the hosted Gradio demo that you would like to embed +- `space`: an optional shorthand if your Gradio demo is hosted on Hugging Face Space. Accepts a `username/space_name` instead of a full URL. Example: `gradio/Echocardiogram-Segmentation`. If this attribute attribute is provided, then `src` does not need to be provided. +- `control_page_title`: a boolean designating whether the html title of the page should be set to the title of the Gradio app (by default `"false"`) +- `initial_height`: the initial height of the web component while it is loading the Gradio app, (by default `"300px"`). Note that the final height is set based on the size of the Gradio app. +- `container`: whether to show the border frame and information about where the Space is hosted (by default `"true"`) +- `info`: whether to show just the information about where the Space is hosted underneath the embedded app (by default `"true"`) +- `autoscroll`: whether to autoscroll to the output when prediction has finished (by default `"false"`) +- `eager`: whether to load the Gradio app as soon as the page loads (by default `"false"`) +- `theme_mode`: whether to use the `dark`, `light`, or default `system` theme mode (by default `"system"`) +- `render`: an event that is triggered once the embedded space has finished rendering. + +Here's an example of how to use these attributes to create a Gradio app that does not lazy load and has an initial height of 0px. + +```html + +``` + +Here's another example of how to use the `render` event. An event listener is used to capture the `render` event and will call the `handleLoadComplete()` function once rendering is complete. + +```html + +``` + +_Note: While Gradio's CSS will never impact the embedding page, the embedding page can affect the style of the embedded Gradio app. Make sure that any CSS in the parent page isn't so general that it could also apply to the embedded Gradio app and cause the styling to break. Element selectors such as `header { ... }` and `footer { ... }` will be the most likely to cause issues._ + +### Embedding with IFrames + +To embed with IFrames instead (if you cannot add javascript to your website, for example), add this element: + +```html + +``` + +Again, you can find the `src=` attribute to your Space's embed URL, which you can find in the "Embed this Space" button. + +Note: if you use IFrames, you'll probably want to add a fixed `height` attribute and set `style="border:0;"` to remove the border. In addition, if your app requires permissions such as access to the webcam or the microphone, you'll need to provide that as well using the `allow` attribute. + +## API Page + +You can use almost any Gradio app as an API! In the footer of a Gradio app [like this one](https://huggingface.co/spaces/gradio/hello_world), you'll see a "Use via API" link. + +![Use via API](https://github.com/gradio-app/gradio/blob/main/guides/assets/use_via_api.png?raw=true) + +This is a page that lists the endpoints that can be used to query the Gradio app, via our supported clients: either [the Python client](https://gradio.app/guides/getting-started-with-the-python-client/), or [the JavaScript client](https://gradio.app/guides/getting-started-with-the-js-client/). For each endpoint, Gradio automatically generates the parameters and their types, as well as example inputs, like this. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/view-api.png) + +The endpoints are automatically created when you launch a Gradio application. If you are using Gradio `Blocks`, you can also name each event listener, such as + +```python +btn.click(add, [num1, num2], output, api_name="addition") +``` + +This will add and document the endpoint `/addition/` to the automatically generated API page. Read more about the [API page here](./view-api-page). + +## Accessing the Network Request Directly + +When a user makes a prediction to your app, you may need the underlying network request, in order to get the request headers (e.g. for advanced authentication), log the client's IP address, getting the query parameters, or for other reasons. Gradio supports this in a similar manner to FastAPI: simply add a function parameter whose type hint is `gr.Request` and Gradio will pass in the network request as that parameter. Here is an example: + +```python +import gradio as gr + +def echo(text, request: gr.Request): + if request: + print("Request headers dictionary:", request.headers) + print("IP address:", request.client.host) + print("Query parameters:", dict(request.query_params)) + return text + +io = gr.Interface(echo, "textbox", "textbox").launch() +``` + +Note: if your function is called directly instead of through the UI (this happens, for +example, when examples are cached, or when the Gradio app is called via API), then `request` will be `None`. +You should handle this case explicitly to ensure that your app does not throw any errors. That is why +we have the explicit check `if request`. + +## Mounting Within Another FastAPI App + +In some cases, you might have an existing FastAPI app, and you'd like to add a path for a Gradio demo. +You can easily do this with `gradio.mount_gradio_app()`. + +Here's a complete example: + +$code_custom_path + +Note that this approach also allows you run your Gradio apps on custom paths (`http://localhost:8000/gradio` in the example above). + + +## Authentication + +### Password-protected app + +You may wish to put an authentication page in front of your app to limit who can open your app. With the `auth=` keyword argument in the `launch()` method, you can provide a tuple with a username and password, or a list of acceptable username/password tuples; Here's an example that provides password-based authentication for a single user named "admin": + +```python +demo.launch(auth=("admin", "pass1234")) +``` + +For more complex authentication handling, you can even pass a function that takes a username and password as arguments, and returns `True` to allow access, `False` otherwise. + +Here's an example of a function that accepts any login where the username and password are the same: + +```python +def same_auth(username, password): + return username == password +demo.launch(auth=same_auth) +``` + +If you have multiple users, you may wish to customize the content that is shown depending on the user that is logged in. You can retrieve the logged in user by [accessing the network request directly](#accessing-the-network-request-directly) as discussed above, and then reading the `.username` attribute of the request. Here's an example: + + +```python +import gradio as gr + +def update_message(request: gr.Request): + return f"Welcome, {request.username}" + +with gr.Blocks() as demo: + m = gr.Markdown() + demo.load(update_message, None, m) + +demo.launch(auth=[("Abubakar", "Abubakar"), ("Ali", "Ali")]) +``` + +Note: For authentication to work properly, third party cookies must be enabled in your browser. This is not the case by default for Safari or for Chrome Incognito Mode. + +If users visit the `/logout` page of your Gradio app, they will automatically be logged out and session cookies deleted. This allows you to add logout functionality to your Gradio app as well. Let's update the previous example to include a log out button: + +```python +import gradio as gr + +def update_message(request: gr.Request): + return f"Welcome, {request.username}" + +with gr.Blocks() as demo: + m = gr.Markdown() + logout_button = gr.Button("Logout", link="/logout") + demo.load(update_message, None, m) + +demo.launch(auth=[("Pete", "Pete"), ("Dawood", "Dawood")]) +``` +By default, visiting `/logout` logs the user out from **all sessions** (e.g. if they are logged in from multiple browsers or devices, all will be signed out). If you want to log out only from the **current session**, add the query parameter `all_session=false` (i.e. `/logout?all_session=false`). + +Note: Gradio's built-in authentication provides a straightforward and basic layer of access control but does not offer robust security features for applications that require stringent access controls (e.g. multi-factor authentication, rate limiting, or automatic lockout policies). + +### OAuth (Login via Hugging Face) + +Gradio natively supports OAuth login via Hugging Face. In other words, you can easily add a _"Sign in with Hugging Face"_ button to your demo, which allows you to get a user's HF username as well as other information from their HF profile. Check out [this Space](https://huggingface.co/spaces/Wauplin/gradio-oauth-demo) for a live demo. + +To enable OAuth, you must set `hf_oauth: true` as a Space metadata in your README.md file. This will register your Space +as an OAuth application on Hugging Face. Next, you can use `gr.LoginButton` to add a login button to +your Gradio app. Once a user is logged in with their HF account, you can retrieve their profile by adding a parameter of type +`gr.OAuthProfile` to any Gradio function. The user profile will be automatically injected as a parameter value. If you want +to perform actions on behalf of the user (e.g. list user's private repos, create repo, etc.), you can retrieve the user +token by adding a parameter of type `gr.OAuthToken`. You must define which scopes you will use in your Space metadata +(see [documentation](https://huggingface.co/docs/hub/spaces-oauth#scopes) for more details). + +Here is a short example: + +$code_login_with_huggingface + +When the user clicks on the login button, they get redirected in a new page to authorize your Space. + +
+ +
+ +Users can revoke access to their profile at any time in their [settings](https://huggingface.co/settings/connected-applications). + +As seen above, OAuth features are available only when your app runs in a Space. However, you often need to test your app +locally before deploying it. To test OAuth features locally, your machine must be logged in to Hugging Face. Please run `huggingface-cli login` or set `HF_TOKEN` as environment variable with one of your access token. You can generate a new token in your settings page (https://huggingface.co/settings/tokens). Then, clicking on the `gr.LoginButton` will log in to your local Hugging Face profile, allowing you to debug your app with your Hugging Face account before deploying it to a Space. + +**Security Note**: It is important to note that adding a `gr.LoginButton` does not restrict users from using your app, in the same way that adding [username-password authentication](/guides/sharing-your-app#password-protected-app) does. This means that users of your app who have not logged in with Hugging Face can still access and run events in your Gradio app -- the difference is that the `gr.OAuthProfile` or `gr.OAuthToken` will be `None` in the corresponding functions. + + +### OAuth (with external providers) + +It is also possible to authenticate with external OAuth providers (e.g. Google OAuth) in your Gradio apps. To do this, first mount your Gradio app within a FastAPI app ([as discussed above](#mounting-within-another-fast-api-app)). Then, you must write an *authentication function*, which gets the user's username from the OAuth provider and returns it. This function should be passed to the `auth_dependency` parameter in `gr.mount_gradio_app`. + +Similar to [FastAPI dependency functions](https://fastapi.tiangolo.com/tutorial/dependencies/), the function specified by `auth_dependency` will run before any Gradio-related route in your FastAPI app. The function should accept a single parameter: the FastAPI `Request` and return either a string (representing a user's username) or `None`. If a string is returned, the user will be able to access the Gradio-related routes in your FastAPI app. + +First, let's show a simplistic example to illustrate the `auth_dependency` parameter: + +```python +from fastapi import FastAPI, Request +import gradio as gr + +app = FastAPI() + +def get_user(request: Request): + return request.headers.get("user") + +demo = gr.Interface(lambda s: f"Hello {s}!", "textbox", "textbox") + +app = gr.mount_gradio_app(app, demo, path="/demo", auth_dependency=get_user) + +if __name__ == '__main__': + uvicorn.run(app) +``` + +In this example, only requests that include a "user" header will be allowed to access the Gradio app. Of course, this does not add much security, since any user can add this header in their request. + +Here's a more complete example showing how to add Google OAuth to a Gradio app (assuming you've already created OAuth Credentials on the [Google Developer Console](https://console.cloud.google.com/project)): + +```python +import os +from authlib.integrations.starlette_client import OAuth, OAuthError +from fastapi import FastAPI, Depends, Request +from starlette.config import Config +from starlette.responses import RedirectResponse +from starlette.middleware.sessions import SessionMiddleware +import uvicorn +import gradio as gr + +app = FastAPI() + +# Replace these with your own OAuth settings +GOOGLE_CLIENT_ID = "..." +GOOGLE_CLIENT_SECRET = "..." +SECRET_KEY = "..." + +config_data = {'GOOGLE_CLIENT_ID': GOOGLE_CLIENT_ID, 'GOOGLE_CLIENT_SECRET': GOOGLE_CLIENT_SECRET} +starlette_config = Config(environ=config_data) +oauth = OAuth(starlette_config) +oauth.register( + name='google', + server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', + client_kwargs={'scope': 'openid email profile'}, +) + +SECRET_KEY = os.environ.get('SECRET_KEY') or "a_very_secret_key" +app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY) + +# Dependency to get the current user +def get_user(request: Request): + user = request.session.get('user') + if user: + return user['name'] + return None + +@app.get('/') +def public(user: dict = Depends(get_user)): + if user: + return RedirectResponse(url='/gradio') + else: + return RedirectResponse(url='/login-demo') + +@app.route('/logout') +async def logout(request: Request): + request.session.pop('user', None) + return RedirectResponse(url='/') + +@app.route('/login') +async def login(request: Request): + redirect_uri = request.url_for('auth') + # If your app is running on https, you should ensure that the + # `redirect_uri` is https, e.g. uncomment the following lines: + # + # from urllib.parse import urlparse, urlunparse + # redirect_uri = urlunparse(urlparse(str(redirect_uri))._replace(scheme='https')) + return await oauth.google.authorize_redirect(request, redirect_uri) + +@app.route('/auth') +async def auth(request: Request): + try: + access_token = await oauth.google.authorize_access_token(request) + except OAuthError: + return RedirectResponse(url='/') + request.session['user'] = dict(access_token)["userinfo"] + return RedirectResponse(url='/') + +with gr.Blocks() as login_demo: + gr.Button("Login", link="/login") + +app = gr.mount_gradio_app(app, login_demo, path="/login-demo") + +def greet(request: gr.Request): + return f"Welcome to Gradio, {request.username}" + +with gr.Blocks() as main_demo: + m = gr.Markdown("Welcome to Gradio!") + gr.Button("Logout", link="/logout") + main_demo.load(greet, None, m) + +app = gr.mount_gradio_app(app, main_demo, path="/gradio", auth_dependency=get_user) + +if __name__ == '__main__': + uvicorn.run(app) +``` + +There are actually two separate Gradio apps in this example! One that simply displays a log in button (this demo is accessible to any user), while the other main demo is only accessible to users that are logged in. You can try this example out on [this Space](https://huggingface.co/spaces/gradio/oauth-example). + +## MCP Servers + +Gradio apps can function as MCP (Model Context Protocol) servers, allowing LLMs to use your app's functions as tools. By simply setting `mcp_server=True` in the `.launch()` method, Gradio automatically converts your app's functions into MCP tools that can be called by MCP clients like Claude Desktop, Cursor, or Cline. The server exposes tools based on your function names, docstrings, and type hints, and can handle file uploads, authentication headers, and progress updates. You can also create MCP-only functions using `gr.api` and expose resources and prompts using decorators. For a comprehensive guide on building MCP servers with Gradio, see [Building an MCP Server with Gradio](https://www.gradio.app/guides/building-mcp-server-with-gradio). + +## Rate Limits + +When publishing your app publicly, and making it available via API or via MCP server, you might want to set rate limits to prevent users from abusing your app. You can identify users using their IP address (using the `gr.Request` object [as discussed above](#accessing-the-network-request-directly)) or, if they are logged in via Hugging Face OAuth, using their username. To see a complete example of how to set rate limits, please see [this Gradio app](https://github.com/gradio-app/gradio/blob/main/demo/rate_limit/run.py). + +## Analytics + +By default, Gradio collects certain analytics to help us better understand the usage of the `gradio` library. This includes the following information: + +* What environment the Gradio app is running on (e.g. Colab Notebook, Hugging Face Spaces) +* What input/output components are being used in the Gradio app +* Whether the Gradio app is utilizing certain advanced features, such as `auth` or `show_error` +* The IP address which is used solely to measure the number of unique developers using Gradio +* The version of Gradio that is running + +No information is collected from _users_ of your Gradio app. If you'd like to disable analytics altogether, you can do so by setting the `analytics_enabled` parameter to `False` in `gr.Blocks`, `gr.Interface`, or `gr.ChatInterface`. Or, you can set the GRADIO_ANALYTICS_ENABLED environment variable to `"False"` to apply this to all Gradio apps created across your system. + +*Note*: this reflects the analytics policy as of `gradio>=4.32.0`. + +## Progressive Web App (PWA) + +[Progressive Web Apps (PWAs)](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) are web applications that are regular web pages or websites, but can appear to the user like installable platform-specific applications. + +Gradio apps can be easily served as PWAs by setting the `pwa=True` parameter in the `launch()` method. Here's an example: + +```python +import gradio as gr + +def greet(name): + return "Hello " + name + "!" + +demo = gr.Interface(fn=greet, inputs="textbox", outputs="textbox") + +demo.launch(pwa=True) # Launch your app as a PWA +``` + +This will generate a PWA that can be installed on your device. Here's how it looks: + +![Installing PWA](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/install-pwa.gif) + +When you specify `favicon_path` in the `launch()` method, the icon will be used as the app's icon. Here's an example: + +```python +demo.launch(pwa=True, favicon_path="./hf-logo.svg") # Use a custom icon for your PWA +``` + +![Custom PWA Icon](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/pwa-favicon.png) diff --git a/guides/04_additional-features/08_file-access.md b/guides/04_additional-features/08_file-access.md new file mode 100644 index 0000000..9cc2466 --- /dev/null +++ b/guides/04_additional-features/08_file-access.md @@ -0,0 +1,97 @@ +# Security and File Access + +Sharing your Gradio app with others (by hosting it on Spaces, on your own server, or through temporary share links) **exposes** certain files on your machine to the internet. Files that are exposed can be accessed at a special URL: + +```bash +http:///gradio_api/file= +``` + +This guide explains which files are exposed as well as some best practices for making sure the files on your machine are secure. + +## Files Gradio allows users to access + +- **1. Static files**. You can designate static files or directories using the `gr.set_static_paths` function. Static files are not be copied to the Gradio cache (see below) and will be served directly from your computer. This can help save disk space and reduce the time your app takes to launch but be mindful of possible security implications as any static files are accessible to all useres of your Gradio app. + +- **2. Files in the `allowed_paths` parameter in `launch()`**. This parameter allows you to pass in a list of additional directories or exact filepaths you'd like to allow users to have access to. (By default, this parameter is an empty list). + +- **3. Files in Gradio's cache**. After you launch your Gradio app, Gradio copies certain files into a temporary cache and makes these files accessible to users. Let's unpack this in more detail below. + + +## The Gradio cache + +First, it's important to understand why Gradio has a cache at all. Gradio copies files to a cache directory before returning them to the frontend. This prevents files from being overwritten by one user while they are still needed by another user of your application. For example, if your prediction function returns a video file, then Gradio will move that video to the cache after your prediction function runs and returns a URL the frontend can use to show the video. Any file in the cache is available via URL to all users of your running application. + +Tip: You can customize the location of the cache by setting the `GRADIO_TEMP_DIR` environment variable to an absolute path, such as `/home/usr/scripts/project/temp/`. + +### Files Gradio moves to the cache + +Gradio moves three kinds of files into the cache + +1. Files specified by the developer before runtime, e.g. cached examples, default values of components, or files passed into parameters such as the `avatar_images` of `gr.Chatbot` + +2. File paths returned by a prediction function in your Gradio application, if they ALSO meet one of the conditions below: + +* It is in the `allowed_paths` parameter of the `Blocks.launch` method. +* It is in the current working directory of the python interpreter. +* It is in the temp directory obtained by `tempfile.gettempdir()`. + +**Note:** files in the current working directory whose name starts with a period (`.`) will not be moved to the cache, even if they are returned from a prediction function, since they often contain sensitive information. + +If none of these criteria are met, the prediction function that is returning that file will raise an exception instead of moving the file to cache. Gradio performs this check so that arbitrary files on your machine cannot be accessed. + +3. Files uploaded by a user to your Gradio app (e.g. through the `File` or `Image` input components). + +Tip: If at any time Gradio blocks a file that you would like it to process, add its path to the `allowed_paths` parameter. + +## The files Gradio will not allow others to access + +While running, Gradio apps will NOT ALLOW users to access: + +- **Files that you explicitly block via the `blocked_paths` parameter in `launch()`**. You can pass in a list of additional directories or exact filepaths to the `blocked_paths` parameter in `launch()`. This parameter takes precedence over the files that Gradio exposes by default, or by the `allowed_paths` parameter or the `gr.set_static_paths` function. + +- **Any other paths on the host machine**. Users should NOT be able to access other arbitrary paths on the host. + +## Uploading Files + +Sharing your Gradio application will also allow users to upload files to your computer or server. You can set a maximum file size for uploads to prevent abuse and to preserve disk space. You can do this with the `max_file_size` parameter of `.launch`. For example, the following two code snippets limit file uploads to 5 megabytes per file. + +```python +import gradio as gr + +demo = gr.Interface(lambda x: x, "image", "image") + +demo.launch(max_file_size="5mb") +# or +demo.launch(max_file_size=5 * gr.FileSize.MB) +``` + +## Best Practices + +* Set a `max_file_size` for your application. +* Do not return arbitrary user input from a function that is connected to a file-based output component (`gr.Image`, `gr.File`, etc.). For example, the following interface would allow anyone to move an arbitrary file in your local directory to the cache: `gr.Interface(lambda s: s, "text", "file")`. This is because the user input is treated as an arbitrary file path. +* Make `allowed_paths` as small as possible. If a path in `allowed_paths` is a directory, any file within that directory can be accessed. Make sure the entires of `allowed_paths` only contains files related to your application. +* Run your gradio application from the same directory the application file is located in. This will narrow the scope of files Gradio will be allowed to move into the cache. For example, prefer `python app.py` to `python Users/sources/project/app.py`. + + +## Example: Accessing local files +Both `gr.set_static_paths` and the `allowed_paths` parameter in launch expect absolute paths. Below is a minimal example to display a local `.png` image file in an HTML block. + +```txt +├── assets +│ └── logo.png +└── app.py +``` +For the example directory structure, `logo.png` and any other files in the `assets` folder can be accessed from your Gradio app in `app.py` as follows: + +```python +from pathlib import Path + +import gradio as gr + +gr.set_static_paths(paths=[Path.cwd().absolute()/"assets"]) + +with gr.Blocks() as demo: + gr.HTML("") + +demo.launch() +``` diff --git a/guides/04_additional-features/09_multipage-apps.md b/guides/04_additional-features/09_multipage-apps.md new file mode 100644 index 0000000..ea873b3 --- /dev/null +++ b/guides/04_additional-features/09_multipage-apps.md @@ -0,0 +1,129 @@ +# Multipage Apps + +Your Gradio app can support multiple pages with the `Blocks.route()` method. Here's what a multipage Gradio app generally looks like: + +```python +with gr.Blocks() as demo: # Main page + name = gr.Textbox(label="Name") + ... +with demo.route("Second page", "/second"): + num = gr.Number() + ... + +demo.launch() +``` + +This allows you to define links to separate pages, each with a separate URL, which are linked to the top of the Gradio app in an automatically-generated navbar. + +Here's a complete example: + +$code_multipage + +All of these pages will share the same backend, including the same queue. + +Note: multipage apps do not support interactions between pages, e.g. an event listener on one page cannot output to a component on another page. Use `gr.Tabs()` for this type of functionality instead of pages. + +**Separate Files** + +For maintainability, you may want to write the code for different pages in different files. Because any Gradio Blocks can be imported and rendered inside another Blocks using the `.render()` method, you can do this as follows. + +Create one main file, say `app.py` and create separate Python files for each page: + +``` +- app.py +- main_page.py +- second_page.py +``` + +The Python file corresponding to each page should consist of a regular Gradio Blocks, Interface, or ChatInterface application, e.g. + +`main_page.py` + +```py +import gradio as gr + +with gr.Blocks() as demo: + gr.Image() + +if __name__ == "__main__": + demo.launch() +``` + +`second_page.py` + +```py +import gradio as gr + +with gr.Blocks() as demo: + t = gr.Textbox() + demo.load(lambda : "Loaded", None, t) + +if __name__ == "__main__": + demo.launch() +``` + +In your main `app.py` file, simply import the Gradio demos from the page files and `.render()` them: + +`app.py` + +```py +import gradio as gr + +import main_page, second_page + +with gr.Blocks() as demo: + main_page.demo.render() +with demo.route("Second Page"): + second_page.demo.render() + +if __name__ == "__main__": + demo.launch() +``` + +This allows you to run each page as an independent Gradio app for testing, while also creating a single file `app.py` that serves as the entrypoint for the complete multipage app. + +## Customizing the Navbar + +By default, Gradio automatically generates a navigation bar for multipage apps that displays all your pages with "Home" as the title for the main page. You can customize the navbar behavior using the `gr.Navbar` component. + +### Per-Page Navbar Configuration + +You can have different navbar configurations for each page of your app: + +```python +import gradio as gr + +with gr.Blocks() as demo: + # Navbar for the main page + navbar = gr.Navbar( + visible=True, + main_page_name="Dashboard", + value=[("About", "https://example.com/about")] + ) + + gr.Textbox(label="Main page content") + +with demo.route("Settings"): + # Different navbar for the Settings page + navbar = gr.Navbar( + visible=True, + main_page_name="Home", + value=[("Documentation", "https://docs.example.com")] + ) + gr.Textbox(label="Settings page") + +demo.launch() +``` + + +**Important Notes:** +- You can have one `gr.Navbar` component per page. Each page's navbar configuration is independent. +- The `main_page_name` parameter customizes the title of the home page link in the navbar. +- The `value` parameter allows you to add additional links to the navbar, which can be internal pages or external URLs. +- If no `gr.Navbar` component is present on a page, the default navbar behavior is used (visible with "Home" as the home page title). +- You can update the navbar properties using standard Gradio event handling, just like with any other component. + +Here's an example that demonstrates the last point: + +$code_navbar_customization + diff --git a/guides/04_additional-features/10_environment-variables.md b/guides/04_additional-features/10_environment-variables.md new file mode 100644 index 0000000..3e9a780 --- /dev/null +++ b/guides/04_additional-features/10_environment-variables.md @@ -0,0 +1,265 @@ +# Environment Variables + +Environment variables in Gradio provide a way to customize your applications and launch settings without changing the codebase. In this guide, we'll explore the key environment variables supported in Gradio and how to set them. + +## Key Environment Variables + +### 1. `GRADIO_SERVER_PORT` + +- **Description**: Specifies the port on which the Gradio app will run. +- **Default**: `7860` +- **Example**: + ```bash + export GRADIO_SERVER_PORT=8000 + ``` + +### 2. `GRADIO_SERVER_NAME` + +- **Description**: Defines the host name for the Gradio server. To make Gradio accessible from any IP address, set this to `"0.0.0.0"` +- **Default**: `"127.0.0.1"` +- **Example**: + ```bash + export GRADIO_SERVER_NAME="0.0.0.0" + ``` + +### 3. `GRADIO_NUM_PORTS` + +- **Description**: Defines the number of ports to try when starting the Gradio server. +- **Default**: `100` +- **Example**: + ```bash + export GRADIO_NUM_PORTS=200 + ``` + +### 4. `GRADIO_ANALYTICS_ENABLED` + +- **Description**: Whether Gradio should provide +- **Default**: `"True"` +- **Options**: `"True"`, `"False"` +- **Example**: + ```sh + export GRADIO_ANALYTICS_ENABLED="True" + ``` + +### 5. `GRADIO_DEBUG` + +- **Description**: Enables or disables debug mode in Gradio. If debug mode is enabled, the main thread does not terminate allowing error messages to be printed in environments such as Google Colab. +- **Default**: `0` +- **Example**: + ```sh + export GRADIO_DEBUG=1 + ``` + +### 6. `GRADIO_FLAGGING_MODE` + +- **Description**: Controls whether users can flag inputs/outputs in the Gradio interface. See [the Guide on flagging](/guides/using-flagging) for more details. +- **Default**: `"manual"` +- **Options**: `"never"`, `"manual"`, `"auto"` +- **Example**: + ```sh + export GRADIO_FLAGGING_MODE="never" + ``` + +### 7. `GRADIO_TEMP_DIR` + +- **Description**: Specifies the directory where temporary files created by Gradio are stored. +- **Default**: System default temporary directory +- **Example**: + ```sh + export GRADIO_TEMP_DIR="/path/to/temp" + ``` + +### 8. `GRADIO_ROOT_PATH` + +- **Description**: Sets the root path for the Gradio application. Useful if running Gradio [behind a reverse proxy](/guides/running-gradio-on-your-web-server-with-nginx). +- **Default**: `""` +- **Example**: + ```sh + export GRADIO_ROOT_PATH="/myapp" + ``` + +### 9. `GRADIO_SHARE` + +- **Description**: Enables or disables sharing the Gradio app. +- **Default**: `"False"` +- **Options**: `"True"`, `"False"` +- **Example**: + ```sh + export GRADIO_SHARE="True" + ``` + +### 10. `GRADIO_ALLOWED_PATHS` + +- **Description**: Sets a list of complete filepaths or parent directories that gradio is allowed to serve. Must be absolute paths. Warning: if you provide directories, any files in these directories or their subdirectories are accessible to all users of your app. Multiple items can be specified by separating items with commas. +- **Default**: `""` +- **Example**: + ```sh + export GRADIO_ALLOWED_PATHS="/mnt/sda1,/mnt/sda2" + ``` + +### 11. `GRADIO_BLOCKED_PATHS` + +- **Description**: Sets a list of complete filepaths or parent directories that gradio is not allowed to serve (i.e. users of your app are not allowed to access). Must be absolute paths. Warning: takes precedence over `allowed_paths` and all other directories exposed by Gradio by default. Multiple items can be specified by separating items with commas. +- **Default**: `""` +- **Example**: + ```sh + export GRADIO_BLOCKED_PATHS="/users/x/gradio_app/admin,/users/x/gradio_app/keys" + ``` + +### 12. `FORWARDED_ALLOW_IPS` + +- **Description**: This is not a Gradio-specific environment variable, but rather one used in server configurations, specifically `uvicorn` which is used by Gradio internally. This environment variable is useful when deploying applications behind a reverse proxy. It defines a list of IP addresses that are trusted to forward traffic to your application. When set, the application will trust the `X-Forwarded-For` header from these IP addresses to determine the original IP address of the user making the request. This means that if you use the `gr.Request` [object's](https://www.gradio.app/docs/gradio/request) `client.host` property, it will correctly get the user's IP address instead of the IP address of the reverse proxy server. Note that only trusted IP addresses (i.e. the IP addresses of your reverse proxy servers) should be added, as any server with these IP addresses can modify the `X-Forwarded-For` header and spoof the client's IP address. +- **Default**: `"127.0.0.1"` +- **Example**: + ```sh + export FORWARDED_ALLOW_IPS="127.0.0.1,192.168.1.100" + ``` + +### 13. `GRADIO_CACHE_EXAMPLES` + +- **Description**: Whether or not to cache examples by default in `gr.Interface()`, `gr.ChatInterface()` or in `gr.Examples()` when no explicit argument is passed for the `cache_examples` parameter. You can set this environment variable to either the string "true" or "false". +- **Default**: `"false"` +- **Example**: + ```sh + export GRADIO_CACHE_EXAMPLES="true" + ``` + + +### 14. `GRADIO_CACHE_MODE` + +- **Description**: How to cache examples. Only applies if `cache_examples` is set to `True` either via enviornment variable or by an explicit parameter, AND no no explicit argument is passed for the `cache_mode` parameter in `gr.Interface()`, `gr.ChatInterface()` or in `gr.Examples()`. Can be set to either the strings "lazy" or "eager." If "lazy", examples are cached after their first use for all users of the app. If "eager", all examples are cached at app launch. + +- **Default**: `"eager"` +- **Example**: + ```sh + export GRADIO_CACHE_MODE="lazy" + ``` + + +### 15. `GRADIO_EXAMPLES_CACHE` + +- **Description**: If you set `cache_examples=True` in `gr.Interface()`, `gr.ChatInterface()` or in `gr.Examples()`, Gradio will run your prediction function and save the results to disk. By default, this is in the `.gradio/cached_examples//` subdirectory within your app's working directory. You can customize the location of cached example files created by Gradio by setting the environment variable `GRADIO_EXAMPLES_CACHE` to an absolute path or a path relative to your working directory. +- **Default**: `".gradio/cached_examples/"` +- **Example**: + ```sh + export GRADIO_EXAMPLES_CACHE="custom_cached_examples/" + ``` + + +### 16. `GRADIO_SSR_MODE` + +- **Description**: Controls whether server-side rendering (SSR) is enabled. When enabled, the initial HTML is rendered on the server rather than the client, which can improve initial page load performance and SEO. + +- **Default**: `"False"` (except on Hugging Face Spaces, where this environment variable sets it to `True`) +- **Options**: `"True"`, `"False"` +- **Example**: + ```sh + export GRADIO_SSR_MODE="True" + ``` + +### 17. `GRADIO_NODE_SERVER_NAME` + +- **Description**: Defines the host name for the Gradio node server. (Only applies if `ssr_mode` is set to `True`.) +- **Default**: `GRADIO_SERVER_NAME` if it is set, otherwise `"127.0.0.1"` +- **Example**: + ```sh + export GRADIO_NODE_SERVER_NAME="0.0.0.0" + ``` + +### 18. `GRADIO_NODE_NUM_PORTS` + +- **Description**: Defines the number of ports to try when starting the Gradio node server. (Only applies if `ssr_mode` is set to `True`.) +- **Default**: `100` +- **Example**: + ```sh + export GRADIO_NODE_NUM_PORTS=200 + ``` + +### 19. `GRADIO_RESET_EXAMPLES_CACHE` + +- **Description**: If set to "True", Gradio will delete and recreate the examples cache directory when the app starts instead of reusing the cached example if they already exist. +- **Default**: `"False"` +- **Options**: `"True"`, `"False"` +- **Example**: + ```sh + export GRADIO_RESET_EXAMPLES_CACHE="True" + ``` + +### 20. `GRADIO_CHAT_FLAGGING_MODE` + +- **Description**: Controls whether users can flag messages in `gr.ChatInterface` applications. Similar to `GRADIO_FLAGGING_MODE` but specifically for chat interfaces. +- **Default**: `"never"` +- **Options**: `"never"`, `"manual"` +- **Example**: + ```sh + export GRADIO_CHAT_FLAGGING_MODE="manual" + ``` + +### 21. `GRADIO_WATCH_DIRS` + +- **Description**: Specifies directories to watch for file changes when running Gradio in development mode. When files in these directories change, the Gradio app will automatically reload. Multiple directories can be specified by separating them with commas. This is primarily used by the `gradio` CLI command for development workflows. +- **Default**: `""` +- **Example**: + ```sh + export GRADIO_WATCH_DIRS="/path/to/src,/path/to/templates" + ``` + +### 22. `GRADIO_VIBE_MODE` + +- **Description**: Enables the Vibe editor mode, which provides an in-browser chat that can be used to write or edit your Gradio app using natural language. When enabled, anyone who can access the Gradio endpoint can modify files and run arbitrary code on the host machine. Use with extreme caution in production environments. +- **Default**: `""` +- **Options**: Any non-empty string enables the mode +- **Example**: + ```sh + export GRADIO_VIBE_MODE="1" + ``` + +### 23. `GRADIO_MCP_SERVER` + +- **Description**: Enables the MCP (Model Context Protocol) server functionality in Gradio. When enabled, the Gradio app will be set up as an MCP server and documented functions will be added as MCP tools that can be used by LLMs. This allows LLMs to interact with your Gradio app's functionality through the MCP protocol. +- **Default**: `"False"` +- **Options**: `"True"`, `"False"` +- **Example**: + ```sh + export GRADIO_MCP_SERVER="True" + ``` + + +### 24. `GRADIO_NUM_WORKERS` + +- **Description**: Number of multiple workers to launch in the background to offload traffic for file I/O and static assets from the main Gradio server. Only works when SSR mode is set. +- **Default**: not set. +- **Options**: Any positive integer. +- **Example**: + ```sh + export GRADIO_NUM_WORKERS=4 + ``` + +### 25. `GRADIO_HEARTBEAT_INTERVAL` + +- **Description**: Sets the interval, in seconds, between heartbeats that keep a client session alive. When a client disconnects, this heartbeat is used to trigger `unload` events and clean up session state. Lowering this value can help detect disconnections faster in environments such as Kubernetes, where the default interval can delay session cleanup. +- **Default**: `15` +- **Example**: + ```sh + export GRADIO_HEARTBEAT_INTERVAL=5 + ``` + +## How to Set Environment Variables + +To set environment variables in your terminal, use the `export` command followed by the variable name and its value. For example: + +```sh +export GRADIO_SERVER_PORT=8000 +``` + +If you're using a `.env` file to manage your environment variables, you can add them like this: + +```sh +GRADIO_SERVER_PORT=8000 +GRADIO_SERVER_NAME="localhost" +``` + +Then, use a tool like `dotenv` to load these variables when running your application. + + + diff --git a/guides/04_additional-features/11_resource-cleanup.md b/guides/04_additional-features/11_resource-cleanup.md new file mode 100644 index 0000000..89529ca --- /dev/null +++ b/guides/04_additional-features/11_resource-cleanup.md @@ -0,0 +1,48 @@ +# Resource Cleanup + +Your Gradio application may create resources during its lifetime. +Examples of resources are `gr.State` variables, any variables you create and explicitly hold in memory, or files you save to disk. +Over time, these resources can use up all of your server's RAM or disk space and crash your application. + +Gradio provides some tools for you to clean up the resources created by your app: + +1. Automatic deletion of `gr.State` variables. +2. Automatic cache cleanup with the `delete_cache` parameter. +2. The `Blocks.unload` event. + +Let's take a look at each of them individually. + +## Automatic deletion of `gr.State` + +When a user closes their browser tab, Gradio will automatically delete any `gr.State` variables associated with that user session after 60 minutes. If the user connects again within those 60 minutes, no state will be deleted. + +You can control the deletion behavior further with the following two parameters of `gr.State`: + +1. `delete_callback` - An arbitrary function that will be called when the variable is deleted. This function must take the state value as input. This function is useful for deleting variables from GPU memory. +2. `time_to_live` - The number of seconds the state should be stored for after it is created or updated. This will delete variables before the session is closed, so it's useful for clearing state for potentially long running sessions. + +## Automatic cache cleanup via `delete_cache` + +Your Gradio application will save uploaded and generated files to a special directory called the cache directory. Gradio uses a hashing scheme to ensure that duplicate files are not saved to the cache but over time the size of the cache will grow (especially if your app goes viral 😉). + +Gradio can periodically clean up the cache for you if you specify the `delete_cache` parameter of `gr.Blocks()`, `gr.Interface()`, or `gr.ChatInterface()`. +This parameter is a tuple of the form `[frequency, age]` both expressed in number of seconds. +Every `frequency` seconds, the temporary files created by this Blocks instance will be deleted if more than `age` seconds have passed since the file was created. +For example, setting this to (86400, 86400) will delete temporary files every day if they are older than a day old. +Additionally, the cache will be deleted entirely when the server restarts. + +## The `unload` event + +Additionally, Gradio now includes a `Blocks.unload()` event, allowing you to run arbitrary cleanup functions when users disconnect (this does not have a 60 minute delay). +Unlike other gradio events, this event does not accept inputs or outptus. +You can think of the `unload` event as the opposite of the `load` event. + +## Putting it all together + +The following demo uses all of these features. When a user visits the page, a special unique directory is created for that user. +As the user interacts with the app, images are saved to disk in that special directory. +When the user closes the page, the images created in that session are deleted via the `unload` event. +The state and files in the cache are cleaned up automatically as well. + +$code_state_cleanup +$demo_state_cleanup \ No newline at end of file diff --git a/guides/04_additional-features/12_themes.md b/guides/04_additional-features/12_themes.md new file mode 100644 index 0000000..0b04d04 --- /dev/null +++ b/guides/04_additional-features/12_themes.md @@ -0,0 +1,22 @@ +# Gradio Themes + +Gradio themes are the easiest way to customize the look and feel of your app. You can choose from a variety of themes, or create your own. To do so, pass the `theme=` kwarg to the `launch()` method of `Interface`, `ChatInterface`, or `Blocks`. For example: + +```python +demo = gr.Interface() +demo.launch(theme=gr.themes.Monochrome()) +``` + +or + +```python +with gr.Blocks() as demo: + ... # your code here +demo.launch(theme=gr.themes.Soft()) + ... +``` + +Gradio comes with a set of prebuilt themes which you can load from `gr.themes.*`. You can extend these themes or create your own themes from scratch - see the [theming guide](https://gradio.app/guides/theming-guide) for more details. + +For additional styling ability, you can pass any CSS (as well as custom JavaScript) to your Gradio application. This is discussed in more detail in our [custom JS and CSS guide](/guides/custom-CSS-and-JS). + diff --git a/guides/04_additional-features/13_client-side-functions.md b/guides/04_additional-features/13_client-side-functions.md new file mode 100644 index 0000000..bba81e4 --- /dev/null +++ b/guides/04_additional-features/13_client-side-functions.md @@ -0,0 +1,71 @@ +# Client Side Functions + +Gradio allows you to run certain "simple" functions directly in the browser by setting `js=True` in your event listeners. This will **automatically convert your Python code into JavaScript**, which significantly improves the responsiveness of your app by avoiding a round trip to the server for simple UI updates. + +The difference in responsiveness is most noticeable on hosted applications (like Hugging Face Spaces), when the server is under heavy load, with high-latency connections, or when many users are accessing the app simultaneously. + +## When to Use Client Side Functions + +Client side functions are ideal for updating component properties (like visibility, placeholders, interactive state, or styling). + +Here's a basic example: + +```py +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row() as row: + btn = gr.Button("Hide this row") + + # This function runs in the browser without a server roundtrip + btn.click( + lambda: gr.Row(visible=False), + None, + row, + js=True + ) + +demo.launch() +``` + + +## Limitations + +Client side functions have some important restrictions: +* They can only update component properties (not values) +* They cannot take any inputs + +Here are some functions that will work with `js=True`: + +```py +# Simple property updates +lambda: gr.Textbox(lines=4) + +# Multiple component updates +lambda: [gr.Textbox(lines=4), gr.Button(interactive=False)] + +# Using gr.update() for property changes +lambda: gr.update(visible=True, interactive=False) +``` + +We are working to increase the space of functions that can be transpiled to JavaScript so that they can be run in the browser. [Follow the Groovy library for more info](https://github.com/abidlabs/groovy-transpiler). + + +## Complete Example + +Here's a more complete example showing how client side functions can improve the user experience: + +$code_todo_list_js + + +## Behind the Scenes + +When you set `js=True`, Gradio: + +1. Transpiles your Python function to JavaScript + +2. Runs the function directly in the browser + +3. Still sends the request to the server (for consistency and to handle any side effects) + +This provides immediate visual feedback while ensuring your application state remains consistent. diff --git a/guides/04_additional-features/14_view-api-page.md b/guides/04_additional-features/14_view-api-page.md new file mode 100644 index 0000000..f1f3b34 --- /dev/null +++ b/guides/04_additional-features/14_view-api-page.md @@ -0,0 +1,101 @@ +# API Page + +You can use almost any Gradio app programmatically via the built-in API! In the footer of any Gradio app, you'll see a "Use via API" link. Clicking on the link opens up a detailed documentation page for the API that Gradio generates based on the function signatures in your Gradio app. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/view-api-animated.gif) + +## Configuring the API Page + +**API endpoint names** + +When you create a Gradio application, the API endpoint names are automatically generated based on the function names. You can change this by using the `api_name` parameter in `gr.Interface` or `gr.ChatInterface`. If you are using Gradio `Blocks`, you can name each event listener, like this: + +```python +btn.click(add, [num1, num2], output, api_name="addition") +``` + +**Controlling API endpoint visibility** + +When building a complex Gradio app, you might want to control how API endpoints appear or behave. Use the `api_visibility` parameter in any `Blocks` event listener to control this: + +- `"public"` (default): The endpoint is shown in API docs and accessible to all +- `"undocumented"`: The endpoint is hidden from API docs but still accessible to downstream apps +- `"private"`: The endpoint is hidden from API docs and not callable by the Gradio client libraries (e.g. `gradio_client` or `@gradio/client`). Note: this does **not** block direct HTTP requests to the endpoint — it should not be relied upon as a security measure. + +To hide an API endpoint from the documentation while still allowing programmatic access: + +```python +btn.click(add, [num1, num2], output, api_visibility="undocumented") +``` + +**Hiding endpoints from client libraries** + +If you want to hide an API endpoint from the API docs and prevent it from being called by the Gradio client libraries, set `api_visibility="private"`: + +```python +btn.click(add, [num1, num2], output, api_visibility="private") +``` + +Note: setting `api_visibility="private"` also means that downstream apps will not be able to load your Gradio app using `gr.load()` as this function uses the Gradio API under the hood. However, the underlying HTTP endpoint is still accessible — this setting should not be relied upon for security. + +**Adding API endpoints** + +You can also add new API routes to your Gradio application that do not correspond to events in your UI. + +For example, in this Gradio application, we add a new route that adds numbers and slices a list: + +```py +import gradio as gr +with gr.Blocks() as demo: + with gr.Row(): + input = gr.Textbox() + button = gr.Button("Submit") + output = gr.Textbox() + def fn(a: int, b: int, c: list[str]) -> tuple[int, str]: + return a + b, c[a:b] + gr.api(fn, api_name="add_and_slice") + +_, url, _ = demo.launch() +``` + +This will create a new route `/add_and_slice` which will show up in the "view API" page. It can be programmatically called by the Python or JS Clients (discussed below) like this: + +```py +from gradio_client import Client + +client = Client(url) +result = client.predict( + a=3, + b=5, + c=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + api_name="/add_and_slice" +) +print(result) +``` + +## The Clients + +This API page not only lists all of the endpoints that can be used to query the Gradio app, but also shows the usage of both [the Gradio Python client](https://gradio.app/guides/getting-started-with-the-python-client/), and [the Gradio JavaScript client](https://gradio.app/guides/getting-started-with-the-js-client/). + +For each endpoint, Gradio automatically generates a complete code snippet with the parameters and their types, as well as example inputs, allowing you to immediately test an endpoint. Here's an example showing an image file input and `str` output: + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/view-api-snippet.png) + + +## The API Recorder 🪄 + +Instead of reading through the view API page, you can also use Gradio's built-in API recorder to generate the relevant code snippet. Simply click on the "API Recorder" button, use your Gradio app via the UI as you would normally, and then the API Recorder will generate the code using the Clients to recreate your all of your interactions programmatically. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/api-recorder.gif) + +## MCP Server + +The API page also includes instructions on how to use the Gradio app as an Model Context Protocol (MCP) server, which is a standardized way to expose functions as tools so that they can be used by LLMs. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/view-api-mcp.png) + +For the MCP sever, each tool, its description, and its parameters are listed, along with instructions on how to integrate with popular MCP Clients. Read more about Gradio's [MCP integration here](https://www.gradio.app/guides/building-mcp-server-with-gradio). + +## OpenAPI Specification + +You can access the complete OpenAPI (formerly Swagger) specification of your Gradio app's API at the endpoint `/gradio_api/openapi.json`. The OpenAPI specification is a standardized, language-agnostic interface description for REST APIs that enables both humans and computers to discover and understand the capabilities of your service. diff --git a/guides/04_additional-features/15_internationalization.md b/guides/04_additional-features/15_internationalization.md new file mode 100644 index 0000000..1cea585 --- /dev/null +++ b/guides/04_additional-features/15_internationalization.md @@ -0,0 +1,59 @@ +Tags: internationalization, i18n, language +Related spaces: + +# Internationalization (i18n) + +Gradio comes with ready-to-use internationalization (i18n) support: + +- Built-in translations: Gradio automatically translates standard UI elements (like "Submit", "Clear", "Cancel") in more than 40 languages based on the user's browser locale. +- Custom translations: For app-specific text, Gradio provides the I18n class that lets you extend the built-in system with your own translations. + +## Setting Up Translations + +You can initialize the `I18n` class with multiple language dictionaries to add custom translations: + +```python +import gradio as gr + +# Create an I18n instance with translations for multiple languages +i18n = gr.I18n( + en={"greeting": "Hello, welcome to my app!", "submit": "Submit"}, + es={"greeting": "¡Hola, bienvenido a mi aplicación!", "submit": "Enviar"}, + fr={"greeting": "Bonjour, bienvenue dans mon application!", "submit": "Soumettre"} +) + +with gr.Blocks() as demo: + # Use the i18n method to translate the greeting + gr.Markdown(i18n("greeting")) + with gr.Row(): + input_text = gr.Textbox(label="Input") + output_text = gr.Textbox(label="Output") + + submit_btn = gr.Button(i18n("submit")) + +# Pass the i18n instance to the launch method +demo.launch(i18n=i18n) +``` + +## How It Works + +When you use the `i18n` instance with a translation key, Gradio will show the corresponding translation to users based on their browser's language settings or the language they've selected in your app. + +If a translation isn't available for the user's locale, the system will fall back to English (if available) or display the key itself. + +## Valid Locale Codes + +Locale codes should follow the BCP 47 format (e.g., 'en', 'en-US', 'zh-CN'). The `I18n` class will warn you if you use an invalid locale code. + +## Supported Component Properties + +The following component properties typically support internationalization: + +- `description` +- `info` +- `title` +- `placeholder` +- `value` +- `label` + +Note that support may vary depending on the component, and some properties might have exceptions where internationalization is not applicable. You can check this by referring to the typehint for the parameter and if it contains `I18nData`, then it supports internationalization. \ No newline at end of file diff --git a/guides/04_additional-features/16_custom-buttons.md b/guides/04_additional-features/16_custom-buttons.md new file mode 100644 index 0000000..ca419be --- /dev/null +++ b/guides/04_additional-features/16_custom-buttons.md @@ -0,0 +1,83 @@ +# Custom Buttons + +Many Gradio components support custom buttons in their toolbar, allowing you to add interactive buttons that can trigger Python functions, JavaScript functions, or both. Custom buttons appear alongside built-in buttons (like "copy" or "download") in the component's toolbar. + +## Basic Usage + +To add custom buttons to a component, pass a list of `gr.Button()` instances to the `buttons` parameter: + +```python +import gradio as gr + +refresh_btn = gr.Button("Refresh", variant="secondary", size="sm") +clear_btn = gr.Button("Clear", variant="secondary", size="sm") + +textbox = gr.Textbox( + value="Sample text", + label="Text Input", + buttons=[refresh_btn, clear_btn] +) +``` + +You can also mix built-in buttons (as strings) with custom buttons: + +```python +code = gr.Code( + value="print('Hello')", + language="python", + buttons=["copy", "download", refresh_btn, export_btn] +) +``` + +## Connecting Button Events + +Custom buttons work just like regular `gr.Button` components. You can connect them to Python functions or JavaScript functions using the `.click()` method: + +### Python Functions + +```python +def refresh_data(): + import random + return f"Refreshed: {random.randint(1000, 9999)}" + +refresh_btn.click(refresh_data, outputs=textbox) +``` + +### JavaScript Functions + +```python +clear_btn.click( + None, + inputs=[], + outputs=textbox, + js="() => ''" +) +``` + +### Combined Python and JavaScript + +You can use the same button for both Python and JavaScript logic: + +```python +alert_btn.click( + None, + inputs=textbox, + outputs=[], + js="(text) => { alert('Text: ' + text); return []; }" +) +``` + +## Complete Example + +Here's a complete example showing custom buttons with both Python and JavaScript functions: + +$code_textbox_custom_buttons + + +## Notes + +- Custom buttons appear in the component's toolbar, typically in the top-right corner +- Only the `value` of the Button is used, other attributes like `icon` are not used. +- Buttons are rendered in the order they appear in the `buttons` list +- Built-in buttons (like "copy", "download") can be hidden by omitting them from the list +- Custom buttons work with component events in the same way as as regular buttons diff --git a/guides/04_additional-features/17_caching.md b/guides/04_additional-features/17_caching.md new file mode 100644 index 0000000..881f7f4 --- /dev/null +++ b/guides/04_additional-features/17_caching.md @@ -0,0 +1,151 @@ +# Caching Function Results + +ML inference is often expensive: image editing, video classification, or audio transcription can each take seconds, minutes, or longer. If a user submits the same inputs twice, there's no reason to re-run the model. Gradio provides two caching mechanisms: `@gr.cache` for automatic exact-match caching, and `gr.Cache()` for manual cache control inside your functions. + +## Automatic caching with `@gr.cache` + +Add `@gr.cache` to any function to automatically cache its results. The decorator hashes inputs by their content — two different numpy arrays with the same pixel values will produce a cache hit. Cache hits bypass the Gradio queue entirely. + +```python +import gradio as gr + +@gr.cache +def classify(image): + return model.predict(image) +``` + +### Generators + +For generator functions, `@gr.cache` caches **all yielded values** and replays them on a hit. This is particularly important for streaming media (`gr.Audio` or `gr.Video` with `streaming=True`) where each yield is a chunk of the output: + +```python +@gr.cache +def stream_response(prompt): + response = "" + for token in model.generate(prompt): + response += token + yield response +``` + +### Async + +Async functions and async generators work identically: + +```python +@gr.cache +async def transcribe(audio): + return await model.transcribe(audio) +``` + +### Parameters + +The behavior of `@gr.cache()` can be customized with a few parameters, most notably the `key`: + +```python +@gr.cache( + key=lambda kw: kw["prompt"], # only cache based on prompt, ignore temperature + max_size=256, # max entries (LRU eviction), default 128 + max_memory="512mb", # max memory before eviction + per_session=True, # isolate cache per user session +) +def generate(prompt, temperature=0.7): + return llm(prompt, temperature=temperature) +``` + +- **`key`** — function that takes the kwargs dict and returns what to hash. Useful for ignoring parameters like temperature or seed. +- **`max_size`** — maximum number of entries. LRU eviction when full. Default 128. Set to 0 for unlimited. +- **`max_memory`** — maximum memory usage. Accepts strings like `"512mb"`, `"2gb"` or raw bytes. LRU eviction when exceeded. +- **`per_session`** — when `True`, each user session gets an isolated cache namespace. Prevents one user's cached results from being served to another, clears that session's entries when the client disconnects, and still applies `max_size` and `max_memory` to the shared cache store across all sessions. + + +Access the cache programmatically via `fn.cache`: + +```python +generate.cache.clear() +print(len(generate.cache)) +``` + +When a queued event is served from `@gr.cache`, Gradio shows a small `from cache` timing badge in the UI which appears temporarily in the relevant output components. + +### Caching intermediate helper calls + +You can also apply `gr.cache()` to a callable at runtime to cache an intermediate step inside a larger Gradio callback: + +```python +def embed(text): + return embedding_model(text) + +def predict(text): + embedding = gr.cache(embed, per_session=True)(text) + return rerank(embedding) +``` + +This is especially useful when only part of your function is deterministic or reusable. Runtime `gr.cache(fn)(...)` uses the same cache store for repeated calls to that helper and shows the same `used cache` badge as `gr.Cache()` (see below) when a hit is reused during a request. + +`gr.cache()` must wrap a callable. If you accidentally write `gr.cache(fn(...))`, Gradio raises an error and tells you to use `gr.cache(fn)(...)` instead. + +## Manual cache control with `gr.Cache()` + +For full control over what gets cached and when, use `gr.Cache()` as an injectable parameter (like `gr.Progress`). Gradio injects the same instance on every call, giving you a thread-safe `get`/`set` interface: + +```python +def my_function(prompt, c=gr.Cache()): + hit = c.get(prompt) + if hit is not None: + return hit["result"] + result = expensive_computation(prompt) + c.set(prompt, result=result) + return result +``` + +If a queued function gets a successful hit from `c.get(...)`, Gradio also shows a timing badge in the UI. This badge says `used cache` instead of `from cache`, because the request still ran, but part of its work was reused from `gr.Cache()`. + +A minimal example is available in the [`gr.Cache()` manual cache demo](https://github.com/gradio-app/gradio/blob/main/demo/cache_manual_demo/run.py). + +### Why use `gr.Cache()` over a plain dict? + +- **Thread-safe** — built-in locking for concurrent requests +- **LRU eviction** + **memory limits** — bounded memory usage (`max_size`, `max_memory`) +- **Per-session isolation** — `gr.Cache(per_session=True)` partitions the cache by user session, prevents data leakage between users, clears that session's entries when the client disconnects, and still applies `max_size` and `max_memory` across the combined cache entries of all sessions +- **Content-aware keys** — numpy arrays, PIL images, DataFrames all work as cache keys + +### KV Cache Example + +You can cache arbitrary intermediate state, not just function outputs. Here's how to cache transformer KV states for prefix reuse: + +```python +def generate(prompt, c=gr.Cache(per_session=True)): + best_key = None + best_len = 0 + for cached_key in c.keys(): + if prompt.startswith(cached_key) and len(cached_key) > best_len: + best_key = cached_key + best_len = len(cached_key) + + if best_key: + past_kv = c.get(best_key)["kv"] + output = model.generate(prompt, past_key_values=past_kv) + else: + output = model.generate(prompt) + + c.set(prompt, kv=model.past_key_values) + return output.text +``` + +For a full runnable version, see the [`gr.Cache()` KV cache demo](https://github.com/gradio-app/gradio/blob/main/demo/cache_kv_demo/run.py). + + +## When to use caching + +`@gr.cache` is most useful for **deterministic** functions where the same input always produces the same output: image classification, audio transcription, embedding computation, structured data extraction. + +It is less useful for **non-deterministic** functions like text generation or image generation, where users might want different outputs even for the same input. For those, `gr.Cache()` with manual control may be more appropriate as you can cache intermediate state (like KV caches) without caching the output completely. + + +## Next steps + +Take a look at these complete examples and then build your own Gradio app with caching! + +- [`@gr.cache()` function types demo](https://github.com/gradio-app/gradio/blob/main/demo/cache_demo/run.py) - sync, async, generator, and async generator caching +- [`gr.Cache()` manual cache demo](https://github.com/gradio-app/gradio/blob/main/demo/cache_manual_demo/run.py) - normalized manual cache keys with explicit `get` / `set` +- [`gr.Cache()` KV cache demo](https://github.com/gradio-app/gradio/blob/main/demo/cache_kv_demo/run.py) - transformer prefix reuse with cached KV state diff --git a/guides/04_additional-features/18_workflows.md b/guides/04_additional-features/18_workflows.md new file mode 100644 index 0000000..91bf677 --- /dev/null +++ b/guides/04_additional-features/18_workflows.md @@ -0,0 +1,178 @@ +# gr.Workflow + +`gr.Workflow` is a visual, node-based AI pipeline builder built into Gradio. It lets you chain together Hugging Face Spaces, models, datasets, and your own Python functions on a drag-and-drop canvas: + +image + + +## Quickstart + +The simplest possible Workflow app: + +```python +import gradio as gr + +gr.Workflow().launch() +``` + +Open the app, drag Spaces and models from the sidebar onto the canvas, connect their ports, and hit **Run**. As you create nodes and edges, a `workflow.json` file will automatically be created in your working directory. You can also use a coding agent to write or edit this file, allowing you to create workflows programmatically. + +## Binding Python functions + +Pass your own Python functions via `bind=` and they appear as callable nodes on the canvas. Gradio inspects the function signature to auto-generate input/output ports. + +```python +import gradio as gr + +def summarize(text: str) -> str: + return text[:200] + +gr.Workflow(bind=[summarize]).launch() +``` + +Use a dict to give nodes explicit names: + +```python +gr.Workflow(bind={"My Summarizer": summarize}).launch() +``` + +## Defining edges in code + +For pipelines you want to ship with a fixed topology, declare edges programmatically: + +```python +import gradio as gr + +def clean(text: str) -> str: + return text.strip().lower() + +def tag(text: str) -> str: + return f"[processed] {text}" + +gr.Workflow( + bind=[clean, tag], + edges=[("clean", "tag")], +).launch() +``` + +Each edge is a `(from_fn, to_fn)` tuple. Use `"fn_name.port_label"` to target a specific port when a node has multiple inputs or outputs. + +> **Note:** `edges=` is only applied when no workflow file exists yet. If `workflow.json` already exists, `edges=` is ignored — delete the file to regenerate the topology from `bind` and `edges`. + +## Loading from a JSON file + +Pass a `graph=` path to load a saved workflow topology. The canvas reads from the file on each page load and autosaves back to it when you make edits. + +```python +gr.Workflow(graph="workflow.json").launch() +``` + +If the file doesn't exist yet, it's created on first save. Combine `graph=` with `bind=` to pre-wire Space nodes alongside your Python functions. + +## Workflow JSON format + +A workflow is a JSON file with three node collections: + +```json +{ + "schema_version": "2", + "name": "My Pipeline", + "references": [ + { + "id": "ref_image", "label": "Input Photo", "role": "reference", + "asset_type": "image", + "inputs": [{"id": "in", "label": "Image", "type": "image"}], + "outputs": [{"id": "out","label": "Image", "type": "image"}], + "x": 80, "y": 120, "width": 220, "height": 124, "data": {} + } + ], + "operators": [ + { + "id": "op_flux", "label": "FLUX.1", "role": "operator", + "kind": "space", + "space_id": "black-forest-labs/FLUX.1-schnell", + "endpoint": "/infer", + "inputs": [{"id": "in_0", "label": "Prompt", "type": "text", "required": true}], + "outputs": [{"id": "out_0","label": "Result","type": "image","output_index": 0}], + "x": 400, "y": 120, "width": 220, "height": 124, "data": {} + } + ], + "subjects": [ + { + "id": "sub_img", "label": "Output Image", "role": "subject", + "asset_type": "image", + "inputs": [{"id": "in", "label": "Image", "type": "image"}], + "outputs": [{"id": "out","label": "Image","type": "image"}], + "x": 700, "y": 120, "width": 220, "height": 107, "data": {} + } + ], + "edges": [ + { + "id": "e1", + "from_node_id": "ref_image", "from_port_id": "out", + "to_node_id": "op_flux", "to_port_id": "in_0", + "type": "image" + } + ] +} +``` + +| Collection | Role | +|---|---| +| `references` | Inputs — uploaded files, editable text, literal values | +| `operators` | Processing steps — Spaces, models, datasets, Python functions | +| `subjects` | Outputs — the results being created | + +### Operator kinds + +| `kind` | What it calls | +|---|---| +| `"space"` | Any Gradio Space on the Hub via `gradio_client` | +| `"model"` | HF Inference API — set `pipeline_tag` to select the task | +| `"dataset"` | Streams rows from any Hub dataset | +| `"fn"` | A Python function passed via `bind=` | + +## Port types + +Ports are typed so the canvas can validate connections. Supported types: + +`image` · `audio` · `video` · `text` · `number` · `boolean` · `gallery` · `file` · `json` · `model3d` + +## Fan-out pipelines + +One reference can feed multiple operators simultaneously — they run in parallel: + +```python +# workflow.json excerpt — one product photo → 4 FLUX Kontext branches +"edges": [ + {"from_node_id": "ref_product", ..., "to_node_id": "op_kontext_0", ...}, + {"from_node_id": "ref_product", ..., "to_node_id": "op_kontext_1", ...}, + {"from_node_id": "ref_product", ..., "to_node_id": "op_kontext_2", ...}, + {"from_node_id": "ref_product", ..., "to_node_id": "op_kontext_3", ...} +] +``` + +## Deploying to Spaces + +A Workflow app is a standard Gradio app — deploy it to Hugging Face Spaces exactly like any other, by uploading the code to a Space, or by simply running in your terminal: + +``` +gradio deploy +``` + +On a Space, the canvas authenticates visitors via OAuth. The Space owner gets write access (can edit and save the workflow); visitors get a read-only view and can run the pipeline with their own HF token. As a result, you should set `hf_oauth: true` [in your Space](https://huggingface.co/docs/hub/en/spaces-oauth). + +## API access + +Every Workflow app is a Gradio app, meaning that it exposes a Gradio REST API endpoint for each output (subject) node. The endpoint name is derived from the subject's label — for example, a subject labelled "Output Image" becomes `/output_image`. Use `client.view_api()` to see the exact names for your workflow: + +```python +from gradio_client import Client + +client = Client("your-username/my-workflow") +client.view_api() # lists available endpoints and their parameters + +result = client.predict("a sunset over mountains", api_name="/output_image") +``` + +This also means that you can reuse your workflows within larger workflows, making it possible to build modular and complex applications with Gradio Workflows! diff --git a/guides/05_chatbots/01_creating-a-chatbot-fast.md b/guides/05_chatbots/01_creating-a-chatbot-fast.md new file mode 100644 index 0000000..39b5f91 --- /dev/null +++ b/guides/05_chatbots/01_creating-a-chatbot-fast.md @@ -0,0 +1,414 @@ +# How to Create a Chatbot with Gradio + +Tags: LLM, CHATBOT, NLP + +## Introduction + +Chatbots are a popular application of large language models (LLMs). Using Gradio, you can easily build a chat application and share that with your users, or try it yourself using an intuitive UI. + +This tutorial uses `gr.ChatInterface()`, which is a high-level abstraction that allows you to create your chatbot UI fast, often with a _few lines of Python_. It can be easily adapted to support multimodal chatbots, or chatbots that require further customization. + +**Prerequisites**: please make sure you are using the latest version of Gradio: + +```bash +$ pip install --upgrade gradio +``` + +## Note for OpenAI-API compatible endpoints + +If you have a chat server serving an OpenAI-API compatible endpoint (such as Ollama), you can spin up a ChatInterface in a single line of Python. First, also run `pip install openai`. Then, with your own URL, model, and optional token: + +```python +import gradio as gr + +gr.load_chat("http://localhost:11434/v1/", model="llama3.2", token="***").launch() +``` + +Read about `gr.load_chat` in [the docs](https://www.gradio.app/docs/gradio/load_chat). If you have your own model, keep reading to see how to create an application around any chat model in Python! + +## Defining a chat function + +To create a chat application with `gr.ChatInterface()`, the first thing you should do is define your **chat function**. In the simplest case, your chat function should accept two arguments: `message` and `history` (the arguments can be named anything, but must be in this order). + +- `message`: a `str` representing the user's most recent message. +- `history`: a list of openai-style dictionaries with `role` and `content` keys, representing the previous conversation history. May also include additional keys representing message metadata. + +The `history` would look like this: + +```python +[ + {"role": "user", "content": [{"type": "text", "text": "What is the capital of France?"}]}, + {"role": "assistant", "content": [{"type": "text", "text": "Paris"}]} +] +``` + +while the next `message` would be: + +```py +"And what is its largest city?" +``` + +Your chat function simply needs to return: + +* a `str` value, which is the chatbot's response based on the chat `history` and most recent `message`, for example, in this case: + +``` +Paris is also the largest city. +``` + +Let's take a look at a few example chat functions: + +**Example: a chatbot that randomly responds with yes or no** + +Let's write a chat function that responds `Yes` or `No` randomly. + +Here's our chat function: + +```python +import random + +def random_response(message, history): + return random.choice(["Yes", "No"]) +``` + +Now, we can plug this into `gr.ChatInterface()` and call the `.launch()` method to create the web interface: + +```python +import gradio as gr + +gr.ChatInterface( + fn=random_response, +).launch() +``` + +That's it! Here's our running demo, try it out: + +$demo_chatinterface_random_response + +**Example: a chatbot that alternates between agreeing and disagreeing** + +Of course, the previous example was very simplistic, it didn't take user input or the previous history into account! Here's another simple example showing how to incorporate a user's input as well as the history. + +```python +import gradio as gr + +def alternatingly_agree(message, history): + if len([h for h in history if h['role'] == "assistant"]) % 2 == 0: + return f"Yes, I do think that: {message}" + else: + return "I don't think so" + +gr.ChatInterface( + fn=alternatingly_agree, +).launch() +``` + +We'll look at more realistic examples of chat functions in our next Guide, which shows [examples of using `gr.ChatInterface` with popular LLMs](../guides/chatinterface-examples). + +## Streaming chatbots + +In your chat function, you can use `yield` to generate a sequence of partial responses, each replacing the previous ones. This way, you'll end up with a streaming chatbot. It's that simple! + +```python +import time +import gradio as gr + +def slow_echo(message, history): + for i in range(len(message)): + time.sleep(0.3) + yield "You typed: " + message[: i+1] + +gr.ChatInterface( + fn=slow_echo, +).launch() +``` + +While the response is streaming, the "Submit" button turns into a "Stop" button that can be used to stop the generator function. + +Tip: Even though you are yielding the latest message at each iteration, Gradio only sends the "diff" of each message from the server to the frontend, which reduces latency and data consumption over your network. + +## Customizing the Chat UI + +If you're familiar with Gradio's `gr.Interface` class, the `gr.ChatInterface` includes many of the same arguments that you can use to customize the look and feel of your Chatbot. For example, you can: + +- add a title and description above your chatbot using `title` and `description` arguments. +- add a theme or custom css using `theme` and `css` arguments respectively in the `launch()` method. +- add `examples` and even enable `cache_examples`, which make your Chatbot easier for users to try it out. +- customize the chatbot (e.g. to change the height or add a placeholder) or textbox (e.g. to add a max number of characters or add a placeholder). + +**Adding examples** + +You can add preset examples to your `gr.ChatInterface` with the `examples` parameter, which takes a list of string examples. Any examples will appear as "buttons" within the Chatbot before any messages are sent. If you'd like to include images or other files as part of your examples, you can do so by using this dictionary format for each example instead of a string: `{"text": "What's in this image?", "files": ["cheetah.jpg"]}`. Each file will be a separate message that is added to your Chatbot history. + +You can change the displayed text for each example by using the `example_labels` argument. You can add icons to each example as well using the `example_icons` argument. Both of these arguments take a list of strings, which should be the same length as the `examples` list. + +If you'd like to cache the examples so that they are pre-computed and the results appear instantly, set `cache_examples=True`. + +**Customizing the chatbot or textbox component** + +If you want to customize the `gr.Chatbot` or `gr.Textbox` that compose the `ChatInterface`, then you can pass in your own chatbot or textbox components. Here's an example of how we to apply the parameters we've discussed in this section: + +```python +import gradio as gr + +def yes_man(message, history): + if message.endswith("?"): + return "Yes" + else: + return "Ask me anything!" + +gr.ChatInterface( + yes_man, + chatbot=gr.Chatbot(height=300), + textbox=gr.Textbox(placeholder="Ask me a yes or no question", container=False, scale=7), + title="Yes Man", + description="Ask Yes Man any question", + examples=["Hello", "Am I cool?", "Are tomatoes vegetables?"], + cache_examples=True, +).launch(theme="ocean") +``` + +Here's another example that adds a "placeholder" for your chat interface, which appears before the user has started chatting. The `placeholder` argument of `gr.Chatbot` accepts Markdown or HTML: + +```python +gr.ChatInterface( + yes_man, + chatbot=gr.Chatbot(placeholder="Your Personal Yes-Man
Ask Me Anything"), +... +``` + +The placeholder appears vertically and horizontally centered in the chatbot. + +## Multimodal Chat Interface + +You may want to add multimodal capabilities to your chat interface. For example, you may want users to be able to upload images or files to your chatbot and ask questions about them. You can make your chatbot "multimodal" by passing in a single parameter (`multimodal=True`) to the `gr.ChatInterface` class. + +When `multimodal=True`, the signature of your chat function changes slightly: the first parameter of your function (what we referred to as `message` above) should accept a dictionary consisting of the submitted text and uploaded files that looks like this: + +```py +{ + "text": "user input", + "files": [ + "updated_file_1_path.ext", + "updated_file_2_path.ext", + ... + ] +} +``` + +This second parameter of your chat function, `history`, will be in the same openai-style dictionary format as before. However, if the history contains uploaded files, the `content` key will be a dictionary with a "type" key whose value is "file" and the file will be represented as a dictionary. All the files will be grouped in message in the history. So after uploading two files and asking a question, your history might look like this: + +```python +[ + {"role": "user", "content": [{"type": "file", "file": {"path": "cat1.png"}}, + {"type": "file", "file": {"path": "cat1.png"}}, + {"type": "text", "text": "What's the difference between these two images?"}]} +] +``` + +The return type of your chat function does *not change* when setting `multimodal=True` (i.e. in the simplest case, you should still return a string value). We discuss more complex cases, e.g. returning files [below](#returning-complex-responses). + +If you are customizing a multimodal chat interface, you should pass in an instance of `gr.MultimodalTextbox` to the `textbox` parameter. You can customize the `MultimodalTextbox` further by passing in the `sources` parameter, which is a list of sources to enable. Here's an example that illustrates how to set up and customize and multimodal chat interface: + + +```python +import gradio as gr + +def count_images(message, history): + num_images = len(message["files"]) + total_images = 0 + for message in history: + for content in message["content"]: + if content["type"] == "file": + total_images += 1 + return f"You just uploaded {num_images} images, total uploaded: {total_images+num_images}" + +demo = gr.ChatInterface( + fn=count_images, + examples=[ + {"text": "No files", "files": []} + ], + multimodal=True, + textbox=gr.MultimodalTextbox(file_count="multiple", file_types=["image"], sources=["upload", "microphone"]) +) + +demo.launch() +``` + +## Additional Inputs + +You may want to add additional inputs to your chat function and expose them to your users through the chat UI. For example, you could add a textbox for a system prompt, or a slider that sets the number of tokens in the chatbot's response. The `gr.ChatInterface` class supports an `additional_inputs` parameter which can be used to add additional input components. + +The `additional_inputs` parameters accepts a component or a list of components. You can pass the component instances directly, or use their string shortcuts (e.g. `"textbox"` instead of `gr.Textbox()`). If you pass in component instances, and they have _not_ already been rendered, then the components will appear underneath the chatbot within a `gr.Accordion()`. + +Here's a complete example: + +$code_chatinterface_system_prompt + +If the components you pass into the `additional_inputs` have already been rendered in a parent `gr.Blocks()`, then they will _not_ be re-rendered in the accordion. This provides flexibility in deciding where to lay out the input components. In the example below, we position the `gr.Textbox()` on top of the Chatbot UI, while keeping the slider underneath. + +```python +import gradio as gr +import time + +def echo(message, history, system_prompt, tokens): + response = f"System prompt: {system_prompt}\n Message: {message}." + for i in range(min(len(response), int(tokens))): + time.sleep(0.05) + yield response[: i+1] + +with gr.Blocks() as demo: + system_prompt = gr.Textbox("You are helpful AI.", label="System Prompt") + slider = gr.Slider(10, 100, render=False) + + gr.ChatInterface( + echo, additional_inputs=[system_prompt, slider], + ) + +demo.launch() +``` + +**Examples with additional inputs** + +You can also add example values for your additional inputs. Pass in a list of lists to the `examples` parameter, where each inner list represents one sample, and each inner list should be `1 + len(additional_inputs)` long. The first element in the inner list should be the example value for the chat message, and each subsequent element should be an example value for one of the additional inputs, in order. When additional inputs are provided, examples are rendered in a table underneath the chat interface. + +If you need to create something even more custom, then its best to construct the chatbot UI using the low-level `gr.Blocks()` API. We have [a dedicated guide for that here](/guides/creating-a-custom-chatbot-with-blocks). + +## Additional Outputs + +In the same way that you can accept additional inputs into your chat function, you can also return additional outputs. Simply pass in a list of components to the `additional_outputs` parameter in `gr.ChatInterface` and return additional values for each component from your chat function. Here's an example that extracts code and outputs it into a separate `gr.Code` component: + +$code_chatinterface_artifacts + +**Note:** unlike the case of additional inputs, the components passed in `additional_outputs` must be already defined in your `gr.Blocks` context -- they are not rendered automatically. If you need to render them after your `gr.ChatInterface`, you can set `render=False` when they are first defined and then `.render()` them in the appropriate section of your `gr.Blocks()` as we do in the example above. + +## Returning Complex Responses + +We mentioned earlier that in the simplest case, your chat function should return a `str` response, which will be rendered as Markdown in the chatbot. However, you can also return more complex responses as we discuss below: + + +**Returning files or Gradio components** + +Currently, the following Gradio components can be displayed inside the chat interface: +* `gr.Image` +* `gr.Plot` +* `gr.Audio` +* `gr.HTML` +* `gr.Video` +* `gr.Gallery` +* `gr.File` + +Simply return one of these components from your function to use it with `gr.ChatInterface`. Here's an example that returns an audio file: + +```py +import gradio as gr + +def music(message, history): + if message.strip(): + return gr.Audio("https://github.com/gradio-app/gradio/raw/main/test/test_files/audio_sample.wav") + else: + return "Please provide the name of an artist" + +gr.ChatInterface( + music, + textbox=gr.Textbox(placeholder="Which artist's music do you want to listen to?", scale=7), +).launch() +``` + +Similarly, you could return image files with `gr.Image`, video files with `gr.Video`, or arbitrary files with the `gr.File` component. + +**Returning Multiple Messages** + +You can return multiple assistant messages from your chat function simply by returning a `list` of messages, each of which is a valid chat type. This lets you, for example, send a message along with files, as in the following example: + +$code_chatinterface_echo_multimodal + + +**Displaying intermediate thoughts or tool usage** + +The `gr.ChatInterface` class supports displaying intermediate thoughts or tool usage direct in the chatbot. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/nested-thought.png) + + To do this, you will need to return a `gr.ChatMessage` object from your chat function. Here is the schema of the `gr.ChatMessage` data class as well as two internal typed dictionaries: + + ```py +MessageContent = Union[str, FileDataDict, FileData, Component] + +@dataclass +class ChatMessage: + content: MessageContent | list[MessageContent] + metadata: MetadataDict = None + options: list[OptionDict] = None + +class MetadataDict(TypedDict): + title: NotRequired[str] + id: NotRequired[int | str] + parent_id: NotRequired[int | str] + log: NotRequired[str] + duration: NotRequired[float] + status: NotRequired[Literal["pending", "done"]] + +class OptionDict(TypedDict): + label: NotRequired[str] + value: str + ``` + +As you can see, the `gr.ChatMessage` dataclass is similar to the openai-style message format, e.g. it has a "content" key that refers to the chat message content. But it also includes a "metadata" key whose value is a dictionary. If this dictionary includes a "title" key, the resulting message is displayed as an intermediate thought with the title being displayed on top of the thought. Here's an example showing the usage: + +$code_chatinterface_thoughts + +You can even show nested thoughts, which is useful for agent demos in which one tool may call other tools. To display nested thoughts, include "id" and "parent_id" keys in the "metadata" dictionary. Read our [dedicated guide on displaying intermediate thoughts and tool usage](/guides/agents-and-tool-usage) for more realistic examples. + +**Providing preset responses** + +When returning an assistant message, you may want to provide preset options that a user can choose in response. To do this, again, you will again return a `gr.ChatMessage` instance from your chat function. This time, make sure to set the `options` key specifying the preset responses. + +As shown in the schema for `gr.ChatMessage` above, the value corresponding to the `options` key should be a list of dictionaries, each with a `value` (a string that is the value that should be sent to the chat function when this response is clicked) and an optional `label` (if provided, is the text displayed as the preset response instead of the `value`). + +This example illustrates how to use preset responses: + +$code_chatinterface_options + +## Modifying the Chatbot Value Directly + +You may wish to modify the value of the chatbot with your own events, other than those prebuilt in the `gr.ChatInterface`. For example, you could create a dropdown that prefills the chat history with certain conversations or add a separate button to clear the conversation history. The `gr.ChatInterface` supports these events, but you need to use the `gr.ChatInterface.chatbot_value` as the input or output component in such events. In this example, we use a `gr.Radio` component to prefill the the chatbot with certain conversations: + +$code_chatinterface_prefill + +## Using Your Chatbot via API + +Once you've built your Gradio chat interface and are hosting it on [Hugging Face Spaces](https://hf.space) or somewhere else, then you can query it with a simple API. The API route will be the name of the function you pass to the ChatInterface. So if `gr.ChatInterface(respond)`, then the API route is `/respond`. The endpoint just expects the user's message and will return the response, internally keeping track of the message history. + +![](https://github.com/gradio-app/gradio/assets/1778297/7b10d6db-6476-4e2e-bebd-ecda802c3b8f) + +To use the endpoint, you should use either the [Gradio Python Client](/guides/getting-started-with-the-python-client) or the [Gradio JS client](/guides/getting-started-with-the-js-client). Or, you can deploy your Chat Interface to other platforms, such as a: + +* Slack bot [[tutorial]](../guides/creating-a-slack-bot-from-a-gradio-app) +* Website widget [[tutorial]](../guides/creating-a-website-widget-from-a-gradio-chatbot) + +## Chat History + +You can enable persistent chat history for your ChatInterface, allowing users to maintain multiple conversations and easily switch between them. When enabled, conversations are stored locally and privately in the user's browser using local storage. So if you deploy a ChatInterface e.g. on [Hugging Face Spaces](https://hf.space), each user will have their own separate chat history that won't interfere with other users' conversations. This means multiple users can interact with the same ChatInterface simultaneously while maintaining their own private conversation histories. + +To enable this feature, simply set `gr.ChatInterface(save_history=True)` (as shown in the example in the next section). Users will then see their previous conversations in a side panel and can continue any previous chat or start a new one. + +## Collecting User Feedback + +To gather feedback on your chat model, set `gr.ChatInterface(flagging_mode="manual")` and users will be able to thumbs-up or thumbs-down assistant responses. Each flagged response, along with the entire chat history, will get saved in a CSV file in the app working directory (this can be configured via the `flagging_dir` parameter). + +You can also change the feedback options via `flagging_options` parameter. The default options are "Like" and "Dislike", which appear as the thumbs-up and thumbs-down icons. Any other options appear under a dedicated flag icon. This example shows a ChatInterface that has both chat history (mentioned in the previous section) and user feedback enabled: + +$code_chatinterface_streaming_echo + +Note that in this example, we set several flagging options: "Like", "Spam", "Inappropriate", "Other". Because the case-sensitive string "Like" is one of the flagging options, the user will see a thumbs-up icon next to each assistant message. The three other flagging options will appear in a dropdown under the flag icon. + +## What's Next? + +Now that you've learned about the `gr.ChatInterface` class and how it can be used to create chatbot UIs quickly, we recommend reading one of the following: + +* [Our next Guide](../guides/chatinterface-examples) shows examples of how to use `gr.ChatInterface` with popular LLM libraries. +* If you'd like to build very custom chat applications from scratch, you can build them using the low-level Blocks API, as [discussed in this Guide](../guides/creating-a-custom-chatbot-with-blocks). +* Once you've deployed your Gradio Chat Interface, its easy to use in other applications because of the built-in API. Here's a tutorial on [how to deploy a Gradio chat interface as a Discord bot](../guides/creating-a-discord-bot-from-a-gradio-app). + + diff --git a/guides/05_chatbots/02_chatinterface-examples.md b/guides/05_chatbots/02_chatinterface-examples.md new file mode 100644 index 0000000..37786d0 --- /dev/null +++ b/guides/05_chatbots/02_chatinterface-examples.md @@ -0,0 +1,76 @@ +# Using Popular LLM libraries and APIs + +Tags: LLM, CHATBOT, API + +In this Guide, we go through several examples of how to use `gr.ChatInterface` with popular LLM libraries and API providers. + +We will cover the following libraries and API providers: + +* [Llama Index](#llama-index) +* [LangChain](#lang-chain) +* [OpenAI](#open-ai) +* [Hugging Face `transformers`](#hugging-face-transformers) +* [SambaNova](#samba-nova) +* [Hyperbolic](#hyperbolic) +* [Anthropic's Claude](#anthropics-claude) +* [MiniMax](#minimax) + +For many LLM libraries and providers, there exist community-maintained integration libraries that make it even easier to spin up Gradio apps. We reference these libraries in the appropriate sections below. + +## Llama Index + +Let's start by using `llama-index` on top of `openai` to build a RAG chatbot on any text or PDF files that you can demo and share in less than 30 lines of code. You'll need to have an OpenAI key for this example (keep reading for the free, open-source equivalent!) + +$code_llm_llamaindex + +## LangChain + +Here's an example using `langchain` on top of `openai` to build a general-purpose chatbot. As before, you'll need to have an OpenAI key for this example. + +$code_llm_langchain + +Tip: For quick prototyping, the community-maintained langchain-gradio repo makes it even easier to build chatbots on top of LangChain. + +## OpenAI + +Of course, we could also use the `openai` library directy. Here a similar example to the LangChain , but this time with streaming as well: + +Tip: For quick prototyping, the openai-gradio library makes it even easier to build chatbots on top of OpenAI models. + + +## Hugging Face `transformers` + +Of course, in many cases you want to run a chatbot locally. Here's the equivalent example using the SmolLM2-135M-Instruct model using the Hugging Face `transformers` library. + +$code_llm_hf_transformers + +## SambaNova + +The SambaNova Cloud API provides access to full-precision open-source models, such as the Llama family. Here's an example of how to build a Gradio app around the SambaNova API + +$code_llm_sambanova + +Tip: For quick prototyping, the sambanova-gradio library makes it even easier to build chatbots on top of SambaNova models. + +## Hyperbolic + +The Hyperbolic AI API provides access to many open-source models, such as the Llama family. Here's an example of how to build a Gradio app around the Hyperbolic + +$code_llm_hyperbolic + +Tip: For quick prototyping, the hyperbolic-gradio library makes it even easier to build chatbots on top of Hyperbolic models. + + +## Anthropic's Claude + +Anthropic's Claude model can also be used via API. Here's a simple 20 questions-style game built on top of the Anthropic API: + +$code_llm_claude + + +## MiniMax + +The MiniMax API exposes the M-series models through an OpenAI-compatible endpoint, so the standard `openai` client works out of the box. Here's an example of how to build a Gradio app around MiniMax: + +$code_llm_minimax + diff --git a/guides/05_chatbots/03_agents-and-tool-usage.md b/guides/05_chatbots/03_agents-and-tool-usage.md new file mode 100644 index 0000000..b6fe983 --- /dev/null +++ b/guides/05_chatbots/03_agents-and-tool-usage.md @@ -0,0 +1,513 @@ +# Building a UI for an LLM Agent + +Tags: LLM, AGENTS, CHAT + +The Gradio Chatbot can natively display intermediate thoughts and tool usage in a collapsible accordion next to a chat message. This makes it perfect for creating UIs for LLM agents and chain-of-thought (CoT) or reasoning demos. This guide will show you how to display thoughts and tool usage with `gr.Chatbot` and `gr.ChatInterface`. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/nested-thoughts.png) + +## The `ChatMessage` dataclass + +Every element of the chatbot value is a dictionary of `role` and `content` keys. You can always use plain python dictionaries to add new values to the chatbot but Gradio also provides the `ChatMessage` dataclass to help you with IDE autocompletion. The schema of `ChatMessage` is as follows: + + ```py +MessageContent = Union[str, FileDataDict, FileData, Component] + +@dataclass +class ChatMessage: + content: MessageContent | [MessageContent] + role: Literal["user", "assistant"] + metadata: MetadataDict = None + options: list[OptionDict] = None + +class MetadataDict(TypedDict): + title: NotRequired[str] + id: NotRequired[int | str] + parent_id: NotRequired[int | str] + log: NotRequired[str] + duration: NotRequired[float] + status: NotRequired[Literal["pending", "done"]] + +class OptionDict(TypedDict): + label: NotRequired[str] + value: str + ``` + + +For our purposes, the most important key is the `metadata` key, which accepts a dictionary. If this dictionary includes a `title` for the message, it will be displayed in a collapsible accordion representing a thought. It's that simple! Take a look at this example: + + +```python +import gradio as gr + +with gr.Blocks() as demo: + chatbot = gr.Chatbot( + value=[ + gr.ChatMessage( + role="user", + content="What is the weather in San Francisco?" + ), + gr.ChatMessage( + role="assistant", + content="I need to use the weather API tool?", + metadata={"title": "🧠 Thinking"} + ) + ] + ) + +demo.launch() +``` + + + +In addition to `title`, the dictionary provided to `metadata` can take several optional keys: + +* `log`: an optional string value to be displayed in a subdued font next to the thought title. +* `duration`: an optional numeric value representing the duration of the thought/tool usage, in seconds. Displayed in a subdued font next inside parentheses next to the thought title. +* `status`: if set to `"pending"`, a spinner appears next to the thought title and the accordion is initialized open. If `status` is `"done"`, the thought accordion is initialized closed. If `status` is not provided, the thought accordion is initialized open and no spinner is displayed. +* `id` and `parent_id`: if these are provided, they can be used to nest thoughts inside other thoughts. + +Below, we show several complete examples of using `gr.Chatbot` and `gr.ChatInterface` to display tool use or thinking UIs. + +## Building with Agents + +### A real example using transformers.agents + +We'll create a Gradio application simple agent that has access to a text-to-image tool. + +Tip: Make sure you read the [smolagents documentation](https://huggingface.co/docs/smolagents/index) first + +We'll start by importing the necessary classes from transformers and gradio. + +```python +import gradio as gr +from gradio import ChatMessage +from transformers import Tool, ReactCodeAgent # type: ignore +from transformers.agents import stream_to_gradio, HfApiEngine # type: ignore + +# Import tool from Hub +image_generation_tool = Tool.from_space( + space_id="black-forest-labs/FLUX.1-schnell", + name="image_generator", + description="Generates an image following your prompt. Returns a PIL Image.", + api_name="/infer", +) + +llm_engine = HfApiEngine("Qwen/Qwen2.5-Coder-32B-Instruct") +# Initialize the agent with both tools and engine +agent = ReactCodeAgent(tools=[image_generation_tool], llm_engine=llm_engine) +``` + +Then we'll build the UI: + +```python +def interact_with_agent(prompt, history): + messages = [] + yield messages + for msg in stream_to_gradio(agent, prompt): + messages.append(asdict(msg)) + yield messages + yield messages + + +demo = gr.ChatInterface( + interact_with_agent, + chatbot= gr.Chatbot( + label="Agent", + avatar_images=( + None, + "https://em-content.zobj.net/source/twitter/53/robot-face_1f916.png", + ), + ), + examples=[ + ["Generate an image of an astronaut riding an alligator"], + ["I am writing a children's book for my daughter. Can you help me with some illustrations?"], + ], +) +``` + +You can see the full demo code [here](https://huggingface.co/spaces/gradio/agent_chatbot/blob/main/app.py). + + +![transformers_agent_code](https://github.com/freddyaboulton/freddyboulton/assets/41651716/c8d21336-e0e6-4878-88ea-e6fcfef3552d) + + +### A real example using langchain agents + +We'll create a UI for langchain agent that has access to a search engine. + +We'll begin with imports and setting up the langchain agent. Note that you'll need an .env file with the following environment variables set - + +``` +SERPAPI_API_KEY= +HF_TOKEN= +OPENAI_API_KEY= +``` + +```python +from langchain import hub +from langchain.agents import AgentExecutor, create_openai_tools_agent, load_tools +from langchain_openai import ChatOpenAI +from gradio import ChatMessage +import gradio as gr + +from dotenv import load_dotenv + +load_dotenv() + +model = ChatOpenAI(temperature=0, streaming=True) + +tools = load_tools(["serpapi"]) + +# Get the prompt to use - you can modify this! +prompt = hub.pull("hwchase17/openai-tools-agent") +agent = create_openai_tools_agent( + model.with_config({"tags": ["agent_llm"]}), tools, prompt +) +agent_executor = AgentExecutor(agent=agent, tools=tools).with_config( + {"run_name": "Agent"} +) +``` + +Then we'll create the Gradio UI + +```python +async def interact_with_langchain_agent(prompt, messages): + messages.append(ChatMessage(role="user", content=prompt)) + yield messages + async for chunk in agent_executor.astream( + {"input": prompt} + ): + if "steps" in chunk: + for step in chunk["steps"]: + messages.append(ChatMessage(role="assistant", content=step.action.log, + metadata={"title": f"🛠️ Used tool {step.action.tool}"})) + yield messages + if "output" in chunk: + messages.append(ChatMessage(role="assistant", content=chunk["output"])) + yield messages + + +with gr.Blocks() as demo: + gr.Markdown("# Chat with a LangChain Agent 🦜⛓️ and see its thoughts 💭") + chatbot = gr.Chatbot( + label="Agent", + avatar_images=( + None, + "https://em-content.zobj.net/source/twitter/141/parrot_1f99c.png", + ), + ) + input = gr.Textbox(lines=1, label="Chat Message") + input.submit(interact_with_langchain_agent, [input_2, chatbot_2], [chatbot_2]) + +demo.launch() +``` + +![langchain_agent_code](https://github.com/freddyaboulton/freddyboulton/assets/41651716/762283e5-3937-47e5-89e0-79657279ea67) + +That's it! See our finished langchain demo [here](https://huggingface.co/spaces/gradio/langchain-agent). + + +## Building with Visibly Thinking LLMs + + +The Gradio Chatbot can natively display intermediate thoughts of a _thinking_ LLM. This makes it perfect for creating UIs that show how an AI model "thinks" while generating responses. Below guide will show you how to build a chatbot that displays Gemini AI's thought process in real-time. + + +### A real example using Gemini 2.0 Flash Thinking API + +Let's create a complete chatbot that shows its thoughts and responses in real-time. We'll use Google's Gemini API for accessing Gemini 2.0 Flash Thinking LLM and Gradio for the UI. + +We'll begin with imports and setting up the gemini client. Note that you'll need to [acquire a Google Gemini API key](https://aistudio.google.com/apikey) first - + +```python +import gradio as gr +from gradio import ChatMessage +from typing import Iterator +import google.generativeai as genai + +genai.configure(api_key="your-gemini-api-key") +model = genai.GenerativeModel("gemini-2.0-flash-thinking-exp-1219") +``` + +First, let's set up our streaming function that handles the model's output: + +```python +def stream_gemini_response(user_message: str, messages: list) -> Iterator[list]: + """ + Streams both thoughts and responses from the Gemini model. + """ + # Initialize response from Gemini + response = model.generate_content(user_message, stream=True) + + # Initialize buffers + thought_buffer = "" + response_buffer = "" + thinking_complete = False + + # Add initial thinking message + messages.append( + ChatMessage( + role="assistant", + content="", + metadata={"title": "⏳Thinking: *The thoughts produced by the Gemini2.0 Flash model are experimental"} + ) + ) + + for chunk in response: + parts = chunk.candidates[0].content.parts + current_chunk = parts[0].text + + if len(parts) == 2 and not thinking_complete: + # Complete thought and start response + thought_buffer += current_chunk + messages[-1] = ChatMessage( + role="assistant", + content=thought_buffer, + metadata={"title": "⏳Thinking: *The thoughts produced by the Gemini2.0 Flash model are experimental"} + ) + + # Add response message + messages.append( + ChatMessage( + role="assistant", + content=parts[1].text + ) + ) + thinking_complete = True + + elif thinking_complete: + # Continue streaming response + response_buffer += current_chunk + messages[-1] = ChatMessage( + role="assistant", + content=response_buffer + ) + + else: + # Continue streaming thoughts + thought_buffer += current_chunk + messages[-1] = ChatMessage( + role="assistant", + content=thought_buffer, + metadata={"title": "⏳Thinking: *The thoughts produced by the Gemini2.0 Flash model are experimental"} + ) + + yield messages +``` + +Then, let's create the Gradio interface: + +```python +with gr.Blocks() as demo: + gr.Markdown("# Chat with Gemini 2.0 Flash and See its Thoughts 💭") + + chatbot = gr.Chatbot( + label="Gemini2.0 'Thinking' Chatbot", + render_markdown=True, + ) + + input_box = gr.Textbox( + lines=1, + label="Chat Message", + placeholder="Type your message here and press Enter..." + ) + + # Set up event handlers + msg_store = gr.State("") # Store for preserving user message + + input_box.submit( + lambda msg: (msg, msg, ""), # Store message and clear input + inputs=[input_box], + outputs=[msg_store, input_box, input_box], + queue=False + ).then( + user_message, # Add user message to chat + inputs=[msg_store, chatbot], + outputs=[input_box, chatbot], + queue=False + ).then( + stream_gemini_response, # Generate and stream response + inputs=[msg_store, chatbot], + outputs=chatbot + ) + +demo.launch() +``` + +This creates a chatbot that: + +- Displays the model's thoughts in a collapsible section +- Streams the thoughts and final response in real-time +- Maintains a clean chat history + + That's it! You now have a chatbot that not only responds to users but also shows its thinking process, creating a more transparent and engaging interaction. See our finished Gemini 2.0 Flash Thinking demo [here](https://huggingface.co/spaces/ysharma/Gemini2-Flash-Thinking). + + + ## Building with Citations + +The Gradio Chatbot can display citations from LLM responses, making it perfect for creating UIs that show source documentation and references. This guide will show you how to build a chatbot that displays Claude's citations in real-time. + +### A real example using Anthropic's Citations API +Let's create a complete chatbot that shows both responses and their supporting citations. We'll use Anthropic's Claude API with citations enabled and Gradio for the UI. + +We'll begin with imports and setting up the Anthropic client. Note that you'll need an `ANTHROPIC_API_KEY` environment variable set: + +```python +import gradio as gr +import anthropic +import base64 +from typing import List, Dict, Any + +client = anthropic.Anthropic() +``` + +First, let's set up our message formatting functions that handle document preparation: + +```python +def encode_pdf_to_base64(file_obj) -> str: + """Convert uploaded PDF file to base64 string.""" + if file_obj is None: + return None + with open(file_obj.name, 'rb') as f: + return base64.b64encode(f.read()).decode('utf-8') + +def format_message_history( + history: list, + enable_citations: bool, + doc_type: str, + text_input: str, + pdf_file: str +) -> List[Dict]: + """Convert Gradio chat history to Anthropic message format.""" + formatted_messages = [] + + # Add previous messages + for msg in history[:-1]: + if msg["role"] == "user": + formatted_messages.append({"role": "user", "content": msg["content"]}) + + # Prepare the latest message with document + latest_message = {"role": "user", "content": []} + + if enable_citations: + if doc_type == "plain_text": + latest_message["content"].append({ + "type": "document", + "source": { + "type": "text", + "media_type": "text/plain", + "data": text_input.strip() + }, + "title": "Text Document", + "citations": {"enabled": True} + }) + elif doc_type == "pdf" and pdf_file: + pdf_data = encode_pdf_to_base64(pdf_file) + if pdf_data: + latest_message["content"].append({ + "type": "document", + "source": { + "type": "base64", + "media_type": "application/pdf", + "data": pdf_data + }, + "title": pdf_file.name, + "citations": {"enabled": True} + }) + + # Add the user's question + latest_message["content"].append({"type": "text", "text": history[-1]["content"]}) + + formatted_messages.append(latest_message) + return formatted_messages +``` + +Then, let's create our bot response handler that processes citations: + +```python +def bot_response( + history: list, + enable_citations: bool, + doc_type: str, + text_input: str, + pdf_file: str +) -> List[Dict[str, Any]]: + try: + messages = format_message_history(history, enable_citations, doc_type, text_input, pdf_file) + response = client.messages.create(model="claude-3-5-sonnet-20241022", max_tokens=1024, messages=messages) + + # Initialize main response and citations + main_response = "" + citations = [] + + # Process each content block + for block in response.content: + if block.type == "text": + main_response += block.text + if enable_citations and hasattr(block, 'citations') and block.citations: + for citation in block.citations: + if citation.cited_text not in citations: + citations.append(citation.cited_text) + + # Add main response + history.append({"role": "assistant", "content": main_response}) + + # Add citations in a collapsible section + if enable_citations and citations: + history.append({ + "role": "assistant", + "content": "\n".join([f"• {cite}" for cite in citations]), + "metadata": {"title": "📚 Citations"} + }) + + return history + + except Exception as e: + history.append({ + "role": "assistant", + "content": "I apologize, but I encountered an error while processing your request." + }) + return history +``` + +Finally, let's create the Gradio interface: + +```python +with gr.Blocks() as demo: + gr.Markdown("# Chat with Citations") + + with gr.Row(scale=1): + with gr.Column(scale=4): + chatbot = gr.Chatbot(bubble_full_width=False, show_label=False, scale=1) + msg = gr.Textbox(placeholder="Enter your message here...", show_label=False, container=False) + + with gr.Column(scale=1): + enable_citations = gr.Checkbox(label="Enable Citations", value=True, info="Toggle citation functionality" ) + doc_type_radio = gr.Radio( choices=["plain_text", "pdf"], value="plain_text", label="Document Type", info="Choose the type of document to use") + text_input = gr.Textbox(label="Document Content", lines=10, info="Enter the text you want to reference") + pdf_input = gr.File(label="Upload PDF", file_types=[".pdf"], file_count="single", visible=False) + + # Handle message submission + msg.submit( + user_message, + [msg, chatbot, enable_citations, doc_type_radio, text_input, pdf_input], + [msg, chatbot] + ).then( + bot_response, + [chatbot, enable_citations, doc_type_radio, text_input, pdf_input], + chatbot + ) + +demo.launch() +``` + +This creates a chatbot that: +- Supports both plain text and PDF documents for Claude to cite from +- Displays Citations in collapsible sections using our `metadata` feature +- Shows source quotes directly from the given documents + +The citations feature works particularly well with the Gradio Chatbot's `metadata` support, allowing us to create collapsible sections that keep the chat interface clean while still providing easy access to source documentation. + +That's it! You now have a chatbot that not only responds to users but also shows its sources, creating a more transparent and trustworthy interaction. See our finished Citations demo [here](https://huggingface.co/spaces/ysharma/anthropic-citations-with-gradio-metadata-key). + diff --git a/guides/05_chatbots/04_creating-a-custom-chatbot-with-blocks.md b/guides/05_chatbots/04_creating-a-custom-chatbot-with-blocks.md new file mode 100644 index 0000000..7de2ea2 --- /dev/null +++ b/guides/05_chatbots/04_creating-a-custom-chatbot-with-blocks.md @@ -0,0 +1,92 @@ +# How to Create a Custom Chatbot with Gradio Blocks + +Tags: NLP, TEXT, CHAT +Related spaces: https://huggingface.co/spaces/gradio/chatbot_streaming, https://huggingface.co/spaces/gradio/chatbot_examples, + +## Introduction + +**Important Note**: if you are getting started, we recommend using the `gr.ChatInterface` to create chatbots -- its a high-level abstraction that makes it possible to create beautiful chatbot applications fast, often with a single line of code. [Read more about it here](/guides/creating-a-chatbot-fast). + +This tutorial will show how to make chatbot UIs from scratch with Gradio's low-level Blocks API. This will give you full control over your Chatbot UI. You'll start by first creating a a simple chatbot to display text, a second one to stream text responses, and finally a chatbot that can handle media files as well. The chatbot interface that we create will look something like this: + +$demo_chatbot_streaming + +**Prerequisite**: We'll be using the `gradio.Blocks` class to build our Chatbot demo. +You can [read the Guide to Blocks first](https://gradio.app/blocks-and-event-listeners) if you are not already familiar with it. Also please make sure you are using the **latest version** version of Gradio: `pip install --upgrade gradio`. + +## A Simple Chatbot Demo + +Let's start with recreating the simple demo above. As you may have noticed, our bot simply randomly responds "How are you?", "Today is a great day", or "I'm very hungry" to any input. Here's the code to create this with Gradio: + +$code_chatbot_simple + +There are three Gradio components here: + +- A `Chatbot`, whose value stores the entire history of the conversation, as a list of response pairs between the user and bot. +- A `Textbox` where the user can type their message, and then hit enter/submit to trigger the chatbot response +- A `ClearButton` button to clear the Textbox and entire Chatbot history + +We have a single function, `respond()`, which takes in the entire history of the chatbot, appends a random message, waits 1 second, and then returns the updated chat history. The `respond()` function also clears the textbox when it returns. + +Of course, in practice, you would replace `respond()` with your own more complex function, which might call a pretrained model or an API, to generate a response. + +$demo_chatbot_simple + +Tip: For better type hinting and auto-completion in your IDE, you can use the `gr.ChatMessage` dataclass: + +```python +from gradio import ChatMessage + +def chat_function(message, history): + history.append(ChatMessage(role="user", content=message)) + history.append(ChatMessage(role="assistant", content="Hello, how can I help you?")) + return history +``` + +## Add Streaming to your Chatbot + +There are several ways we can improve the user experience of the chatbot above. First, we can stream responses so the user doesn't have to wait as long for a message to be generated. Second, we can have the user message appear immediately in the chat history, while the chatbot's response is being generated. Here's the code to achieve that: + +$code_chatbot_streaming + +You'll notice that when a user submits their message, we now _chain_ two event events with `.then()`: + +1. The first method `user()` updates the chatbot with the user message and clears the input field. Because we want this to happen instantly, we set `queue=False`, which would skip any queue had it been enabled. The chatbot's history is appended with `{"role": "user", "content": user_message}`. + +2. The second method, `bot()` updates the chatbot history with the bot's response. Finally, we construct the message character by character and `yield` the intermediate outputs as they are being constructed. Gradio automatically turns any function with the `yield` keyword [into a streaming output interface](/guides/key-features/#iterative-outputs). + + +Of course, in practice, you would replace `bot()` with your own more complex function, which might call a pretrained model or an API, to generate a response. + + +## Adding Markdown, Images, Audio, or Videos + +The `gr.Chatbot` component supports a subset of markdown including bold, italics, and code. For example, we could write a function that responds to a user's message, with a bold **That's cool!**, like this: + +```py +def bot(history): + response = {"role": "assistant", "content": "**That's cool!**"} + history.append(response) + return history +``` + +In addition, it can handle media files, such as images, audio, and video. You can use the `MultimodalTextbox` component to easily upload all types of media files to your chatbot. You can customize the `MultimodalTextbox` further by passing in the `sources` parameter, which is a list of sources to enable. To pass in a media file, we must pass in the file a dictionary with a `path` key pointing to a local file and an `alt_text` key. The `alt_text` is optional, so you can also just pass in a tuple with a single element `{"path": "filepath"}`, like this: + +```python +def add_message(history, message): + for x in message["files"]: + history.append({"role": "user", "content": {"path": x}}) + if message["text"] is not None: + history.append({"role": "user", "content": message["text"]}) + return history, gr.MultimodalTextbox(value=None, interactive=False, file_types=["image"], sources=["upload", "microphone"]) +``` + +Putting this together, we can create a _multimodal_ chatbot with a multimodal textbox for a user to submit text and media files. The rest of the code looks pretty much the same as before: + +$code_chatbot_multimodal +$demo_chatbot_multimodal + +And you're done! That's all the code you need to build an interface for your chatbot model. Finally, we'll end our Guide with some links to Chatbots that are running on Spaces so that you can get an idea of what else is possible: + +- [gradio/chatbot_streaming](https://huggingface.co/spaces/gradio/chatbot_streaming): A streaming chatbot demo built with `gr.Chatbot` and Blocks. +- [gradio/chatbot_examples](https://huggingface.co/spaces/gradio/chatbot_examples): A chatbot that presents new visitors with a list of multimodal examples they can use to start the conversation. diff --git a/guides/05_chatbots/05_chatbot-specific-events.md b/guides/05_chatbots/05_chatbot-specific-events.md new file mode 100644 index 0000000..d1f86c5 --- /dev/null +++ b/guides/05_chatbots/05_chatbot-specific-events.md @@ -0,0 +1,188 @@ +# Chatbot-Specific Events + +Tags: LLM, CHAT + +Users expect modern chatbot UIs to let them easily interact with individual chat messages: for example, users might want to retry message generations, undo messages, or click on a like/dislike button to upvote or downvote a generated message. + +Thankfully, the Gradio Chatbot exposes several events, such as `.retry`, `.undo`, `.like`, and `.clear`, to let you build this functionality into your application. As an application developer, you can attach functions to any of these event, allowing you to run arbitrary Python functions e.g. when a user interacts with a message. + +In this demo, we'll build a UI that implements these events. You can see our finished demo deployed on Hugging Face spaces here: + +$demo_chatbot_retry_undo_like + +Tip: `gr.ChatInterface` automatically uses the `retry` and `.undo` events so it's best to start there in order get a fully working application quickly. + + +## The UI + +First, we'll build the UI without handling these events and build from there. +We'll use the Hugging Face InferenceClient in order to get started without setting up +any API keys. + +This is what the first draft of our application looks like: + +```python +from huggingface_hub import InferenceClient +import gradio as gr + +client = InferenceClient() + +def respond( + prompt: str, + history, +): + if not history: + history = [{"role": "system", "content": "You are a friendly chatbot"}] + history.append({"role": "user", "content": prompt}) + + yield history + + response = {"role": "assistant", "content": ""} + for message in client.chat_completion( # type: ignore + history, + temperature=0.95, + top_p=0.9, + max_tokens=512, + stream=True, + model="openai/gpt-oss-20b" + ): + response["content"] += message.choices[0].delta.content or "" if message.choices else "" + yield history + [response] + + +with gr.Blocks() as demo: + gr.Markdown("# Chat with GPT-OSS 20b 🤗") + chatbot = gr.Chatbot( + label="Agent", + avatar_images=( + None, + "https://em-content.zobj.net/source/twitter/376/hugging-face_1f917.png", + ), + ) + prompt = gr.Textbox(max_lines=1, label="Chat Message") + prompt.submit(respond, [prompt, chatbot], [chatbot]) + prompt.submit(lambda: "", None, [prompt]) + +if __name__ == "__main__": + demo.launch() +``` + +## The Undo Event + +Our undo event will populate the textbox with the previous user message and also remove all subsequent assistant responses. + +In order to know the index of the last user message, we can pass `gr.UndoData` to our event handler function like so: + +```python +def handle_undo(history, undo_data: gr.UndoData): + return history[:undo_data.index], history[undo_data.index]['content'][0]["text"] +``` + +We then pass this function to the `undo` event! + +```python + chatbot.undo(handle_undo, chatbot, [chatbot, prompt]) +``` + +You'll notice that every bot response will now have an "undo icon" you can use to undo the response - + +![undo_event](https://github.com/user-attachments/assets/180b5302-bc4a-4c3e-903c-f14ec2adcaa6) + +Tip: You can also access the content of the user message with `undo_data.value` + +## The Retry Event + +The retry event will work similarly. We'll use `gr.RetryData` to get the index of the previous user message and remove all the subsequent messages from the history. Then we'll use the `respond` function to generate a new response. We could also get the previous prompt via the `value` property of `gr.RetryData`. + +```python +def handle_retry(history, retry_data: gr.RetryData): + new_history = history[:retry_data.index] + previous_prompt = history[retry_data.index]['content'][0]["text"] + yield from respond(previous_prompt, new_history) +... + +chatbot.retry(handle_retry, chatbot, chatbot) +``` + +You'll see that the bot messages have a "retry" icon now - + +![retry_event](https://github.com/user-attachments/assets/cec386a7-c4cd-4fb3-a2d7-78fd806ceac6) + +Tip: The Hugging Face inference API caches responses, so in this demo, the retry button will not generate a new response. + +## The Like Event + +By now you should hopefully be seeing the pattern! +To let users like a message, we'll add a `.like` event to our chatbot. +We'll pass it a function that accepts a `gr.LikeData` object. +In this case, we'll just print the message that was either liked or disliked. + +```python +def handle_like(data: gr.LikeData): + if data.liked: + print("You upvoted this response: ", data.value) + else: + print("You downvoted this response: ", data.value) + +chatbot.like(handle_like, None, None) +``` + +## The Edit Event + +Same idea with the edit listener! with `gr.Chatbot(editable=True)`, you can capture user edits. The `gr.EditData` object tells us the index of the message edited and the new text of the mssage. Below, we use this object to edit the history, and delete any subsequent messages. + +```python +def handle_edit(history, edit_data: gr.EditData): + new_history = history[:edit_data.index] + new_history[-1]['content'] = [{"text": edit_data.value, "type": "text"}] + return new_history + +... + +chatbot.edit(handle_edit, chatbot, chatbot) +``` + +## The Clear Event + +As a bonus, we'll also cover the `.clear()` event, which is triggered when the user clicks the clear icon to clear all messages. As a developer, you can attach additional events that should happen when this icon is clicked, e.g. to handle clearing of additional chatbot state: + +```python +from uuid import uuid4 +import gradio as gr + + +def clear(): + print("Cleared uuid") + return uuid4() + + +def chat_fn(user_input, history, uuid): + return f"{user_input} with uuid {uuid}" + + +with gr.Blocks() as demo: + uuid_state = gr.State( + uuid4 + ) + chatbot = gr.Chatbot() + chatbot.clear(clear, outputs=[uuid_state]) + + gr.ChatInterface( + chat_fn, + additional_inputs=[uuid_state], + chatbot=chatbot, + ) + +demo.launch() +``` + +In this example, the `clear` function, bound to the `chatbot.clear` event, returns a new UUID into our session state, when the chat history is cleared via the trash icon. This can be seen in the `chat_fn` function, which references the UUID saved in our session state. + +This example also shows that you can use these events with `gr.ChatInterface` by passing in a custom `gr.Chatbot` object. + +## Conclusion + +That's it! You now know how you can implement the retry, undo, like, and clear events for the Chatbot. + + + diff --git a/guides/05_chatbots/06_creating-a-discord-bot-from-a-gradio-app.md b/guides/05_chatbots/06_creating-a-discord-bot-from-a-gradio-app.md new file mode 100644 index 0000000..d2d90eb --- /dev/null +++ b/guides/05_chatbots/06_creating-a-discord-bot-from-a-gradio-app.md @@ -0,0 +1,162 @@ +# 🚀 Creating Discord Bots with Gradio 🚀 + +Tags: CHAT, DEPLOY, DISCORD + +You can make your Gradio app available as a Discord bot to let users in your Discord server interact with it directly. + +## How does it work? + +The Discord bot will listen to messages mentioning it in channels. When it receives a message (which can include text as well as files), it will send it to your Gradio app via Gradio's built-in API. Your bot will reply with the response it receives from the API. + +Because Gradio's API is very flexible, you can create Discord bots that support text, images, audio, streaming, chat history, and a wide variety of other features very easily. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/Screen%20Recording%202024-12-18%20at%204.26.55%E2%80%AFPM.gif) + +## Prerequisites + +* Install the latest version of `gradio` and the `discord.py` libraries: + +``` +pip install --upgrade gradio discord.py~=2.0 +``` + +* Have a running Gradio app. This app can be running locally or on Hugging Face Spaces. In this example, we will be using the [Gradio Playground Space](https://huggingface.co/spaces/abidlabs/gradio-playground-bot), which takes in an image and/or text and generates the code to generate the corresponding Gradio app. + +Now, we are ready to get started! + + +### 1. Create a Discord application + +First, go to the [Discord apps dashboard](https://discord.com/developers/applications). Look for the "New Application" button and click it. Give your application a name, and then click "Create". + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/discord-4.png) + +On the resulting screen, you will see basic information about your application. Under the Settings section, click on the "Bot" option. You can update your bot's username if you would like. + +Then click on the "Reset Token" button. A new token will be generated. Copy it as we will need it for the next step. + +Scroll down to the section that says "Privileged Gateway Intents". Your bot will need certain permissions to work correctly. In this tutorial, we will only be using the "Message Content Intent" so click the toggle to enable this intent. Save the changes. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/discord-3.png) + + + +### 2. Write a Discord bot + +Let's start by writing a very simple Discord bot, just to make sure that everything is working. Write the following Python code in a file called `bot.py`, pasting the discord bot token from the previous step: + +```python +# bot.py +import discord + +TOKEN = #PASTE YOUR DISCORD BOT TOKEN HERE + +client = discord.Client() + +@client.event +async def on_ready(): + print(f'{client.user} has connected to Discord!') + +client.run(TOKEN) +``` + +Now, run this file: `python bot.py`, which should run and print a message like: + +```text +We have logged in as GradioPlaygroundBot#1451 +``` + +If that is working, we are ready to add Gradio-specific code. We will be using the [Gradio Python Client](https://www.gradio.app/guides/getting-started-with-the-python-client) to query the Gradio Playground Space mentioned above. Here's the updated `bot.py` file: + +```python +import discord +from gradio_client import Client, handle_file +import httpx +import os + +TOKEN = #PASTE YOUR DISCORD BOT TOKEN HERE + +intents = discord.Intents.default() +intents.message_content = True + +client = discord.Client(intents=intents) +gradio_client = Client("abidlabs/gradio-playground-bot") + +def download_image(attachment): + response = httpx.get(attachment.url) + image_path = f"./images/{attachment.filename}" + os.makedirs("./images", exist_ok=True) + with open(image_path, "wb") as f: + f.write(response.content) + return image_path + +@client.event +async def on_ready(): + print(f'We have logged in as {client.user}') + +@client.event +async def on_message(message): + # Ignore messages from the bot itself + if message.author == client.user: + return + + # Check if the bot is mentioned in the message and reply + if client.user in message.mentions: + # Extract the message content without the bot mention + clean_message = message.content.replace(f"<@{client.user.id}>", "").strip() + + # Handle images (only the first image is used) + files = [] + if message.attachments: + for attachment in message.attachments: + if any(attachment.filename.lower().endswith(ext) for ext in ['png', 'jpg', 'jpeg', 'gif', 'webp']): + image_path = download_image(attachment) + files.append(handle_file(image_path)) + break + + # Stream the responses to the channel + for response in gradio_client.submit( + message={"text": clean_message, "files": files}, + ): + await message.channel.send(response[-1]) + +client.run(TOKEN) +``` + +### 3. Add the bot to your Discord Server + +Now we are ready to install the bot on our server. Go back to the [Discord apps dashboard](https://discord.com/developers/applications). Under the Settings section, click on the "OAuth2" option. Scroll down to the "OAuth2 URL Generator" box and select the "bot" checkbox: + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/discord-2.png) + + + +Then in "Bot Permissions" box that pops up underneath, enable the following permissions: + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/discord-1.png) + + +Copy the generated URL that appears underneath, which should look something like: + +```text +https://discord.com/oauth2/authorize?client_id=1319011745452265575&permissions=377957238784&integration_type=0&scope=bot +``` + +Paste it into your browser, which should allow you to add the Discord bot to any Discord server that you manage. + + +### 4. That's it! + +Now you can mention your bot from any channel in your Discord server, optionally attach an image, and it will respond with generated Gradio app code! + +The bot will: +1. Listen for mentions +2. Process any attached images +3. Send the text and images to your Gradio app +4. Stream the responses back to the Discord channel + + This is just a basic example - you can extend it to handle more types of files, add error handling, or integrate with different Gradio apps. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/Screen%20Recording%202024-12-18%20at%204.26.55%E2%80%AFPM.gif) + +If you build a Discord bot from a Gradio app, feel free to share it on X and tag [the Gradio account](https://x.com/Gradio), and we are happy to help you amplify! \ No newline at end of file diff --git a/guides/05_chatbots/07_creating-a-slack-bot-from-a-gradio-app.md b/guides/05_chatbots/07_creating-a-slack-bot-from-a-gradio-app.md new file mode 100644 index 0000000..dedb686 --- /dev/null +++ b/guides/05_chatbots/07_creating-a-slack-bot-from-a-gradio-app.md @@ -0,0 +1,144 @@ +# 🚀 Creating a Slack Bot from a Gradio App 🚀 + +Tags: CHAT, DEPLOY, SLACK + +You can make your Gradio app available as a Slack bot to let users in your Slack workspace interact with it directly. + +## How does it work? + +The Slack bot will listen to messages mentioning it in channels. When it receives a message (which can include text as well as files), it will send it to your Gradio app via Gradio's built-in API. Your bot will reply with the response it receives from the API. + +Because Gradio's API is very flexible, you can create Slack bots that support text, images, audio, streaming, chat history, and a wide variety of other features very easily. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/Screen%20Recording%202024-12-19%20at%203.30.00%E2%80%AFPM.gif) + +## Prerequisites + +* Install the latest version of `gradio` and the `slack-bolt` library: + +```bash +pip install --upgrade gradio slack-bolt~=1.0 +``` + +* Have a running Gradio app. This app can be running locally or on Hugging Face Spaces. In this example, we will be using the [Gradio Playground Space](https://huggingface.co/spaces/abidlabs/gradio-playground-bot), which takes in an image and/or text and generates the code to generate the corresponding Gradio app. + +Now, we are ready to get started! + +### 1. Create a Slack App + +1. Go to [api.slack.com/apps](https://api.slack.com/apps) and click "Create New App" +2. Choose "From scratch" and give your app a name +3. Select the workspace where you want to develop your app +4. Under "OAuth & Permissions", scroll to "Scopes" and add these Bot Token Scopes: + - `app_mentions:read` + - `chat:write` + - `files:read` + - `files:write` +5. In the same "OAuth & Permissions" page, scroll back up and click the button to install the app to your workspace. +6. Note the "Bot User OAuth Token" (starts with `xoxb-`) that appears as we'll need it later +7. Click on "Socket Mode" in the menu bar. When the page loads, click the toggle to "Enable Socket Mode" +8. Give your token a name, such as `socket-token` and copy the token that is generated (starts with `xapp-`) as we'll need it later. +9. Finally, go to the "Event Subscription" option in the menu bar. Click the toggle to "Enable Events" and subscribe to the `app_mention` bot event. + +### 2. Write a Slack bot + +Let's start by writing a very simple Slack bot, just to make sure that everything is working. Write the following Python code in a file called `bot.py`, pasting the two tokens from step 6 and step 8 in the previous section. + +```py +from slack_bolt import App +from slack_bolt.adapter.socket_mode import SocketModeHandler + +SLACK_BOT_TOKEN = # PASTE YOUR SLACK BOT TOKEN HERE +SLACK_APP_TOKEN = # PASTE YOUR SLACK APP TOKEN HERE + +app = App(token=SLACK_BOT_TOKEN) + +@app.event("app_mention") +def handle_app_mention_events(body, say): + user_id = body["event"]["user"] + say(f"Hi <@{user_id}>! You mentioned me and said: {body['event']['text']}") + +if __name__ == "__main__": + handler = SocketModeHandler(app, SLACK_APP_TOKEN) + handler.start() +``` + +If that is working, we are ready to add Gradio-specific code. We will be using the [Gradio Python Client](https://www.gradio.app/guides/getting-started-with-the-python-client) to query the Gradio Playground Space mentioned above. Here's the updated `bot.py` file: + +```python +from slack_bolt import App +from slack_bolt.adapter.socket_mode import SocketModeHandler + +SLACK_BOT_TOKEN = # PASTE YOUR SLACK BOT TOKEN HERE +SLACK_APP_TOKEN = # PASTE YOUR SLACK APP TOKEN HERE + +app = App(token=SLACK_BOT_TOKEN) +gradio_client = Client("abidlabs/gradio-playground-bot") + +def download_image(url, filename): + headers = {"Authorization": f"Bearer {SLACK_BOT_TOKEN}"} + response = httpx.get(url, headers=headers) + image_path = f"./images/{filename}" + os.makedirs("./images", exist_ok=True) + with open(image_path, "wb") as f: + f.write(response.content) + return image_path + +def slackify_message(message): + # Replace markdown links with slack format and remove code language specifier after triple backticks + pattern = r'\[(.*?)\]\((.*?)\)' + cleaned = re.sub(pattern, r'<\2|\1>', message) + cleaned = re.sub(r'```\w+\n', '```', cleaned) + return cleaned.strip() + +@app.event("app_mention") +def handle_app_mention_events(body, say): + # Extract the message content without the bot mention + text = body["event"]["text"] + bot_user_id = body["authorizations"][0]["user_id"] + clean_message = text.replace(f"<@{bot_user_id}>", "").strip() + + # Handle images if present + files = [] + if "files" in body["event"]: + for file in body["event"]["files"]: + if file["filetype"] in ["png", "jpg", "jpeg", "gif", "webp"]: + image_path = download_image(file["url_private_download"], file["name"]) + files.append(handle_file(image_path)) + break + + # Submit to Gradio and send responses back to Slack + for response in gradio_client.submit( + message={"text": clean_message, "files": files}, + ): + cleaned_response = slackify_message(response[-1]) + say(cleaned_response) + +if __name__ == "__main__": + handler = SocketModeHandler(app, SLACK_APP_TOKEN) + handler.start() +``` +### 3. Add the bot to your Slack Workplace + +Now, create a new channel or navigate to an existing channel in your Slack workspace where you want to use the bot. Click the "+" button next to "Channels" in your Slack sidebar and follow the prompts to create a new channel. + +Finally, invite your bot to the channel: +1. In your new channel, type `/invite @YourBotName` +2. Select your bot from the dropdown +3. Click "Invite to Channel" + +### 4. That's it! + +Now you can mention your bot in any channel it's in, optionally attach an image, and it will respond with generated Gradio app code! + +The bot will: +1. Listen for mentions +2. Process any attached images +3. Send the text and images to your Gradio app +4. Stream the responses back to the Slack channel + +This is just a basic example - you can extend it to handle more types of files, add error handling, or integrate with different Gradio apps! + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/Screen%20Recording%202024-12-19%20at%203.30.00%E2%80%AFPM.gif) + +If you build a Slack bot from a Gradio app, feel free to share it on X and tag [the Gradio account](https://x.com/Gradio), and we are happy to help you amplify! \ No newline at end of file diff --git a/guides/05_chatbots/08_creating-a-website-widget-from-a-gradio-chatbot.md b/guides/05_chatbots/08_creating-a-website-widget-from-a-gradio-chatbot.md new file mode 100644 index 0000000..56ee898 --- /dev/null +++ b/guides/05_chatbots/08_creating-a-website-widget-from-a-gradio-chatbot.md @@ -0,0 +1,211 @@ +# 🚀 Creating a Website Chat Widget with Gradio 🚀 + +Tags: CHAT, DEPLOY, WEB + +You can make your Gradio Chatbot available as an embedded chat widget on your website, similar to popular customer service widgets like Intercom. This is particularly useful for: + +- Adding AI assistance to your documentation pages +- Providing interactive help on your portfolio or product website +- Creating a custom chatbot interface for your Gradio app + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/Screen%20Recording%202024-12-19%20at%203.32.46%E2%80%AFPM.gif) + +## How does it work? + +The chat widget appears as a small button in the corner of your website. When clicked, it opens a chat interface that communicates with your Gradio app via the JavaScript Client API. Users can ask questions and receive responses directly within the widget. + + +## Prerequisites + +* A running Gradio app (local or on Hugging Face Spaces). In this example, we'll use the [Gradio Playground Space](https://huggingface.co/spaces/abidlabs/gradio-playground-bot), which helps generate code for Gradio apps based on natural language descriptions. + +### 1. Create and Style the Chat Widget + +First, add this HTML and CSS to your website: + +```html +
+ + +
+ + +``` + +### 2. Add the JavaScript + +Then, add the following JavaScript code (which uses the Gradio JavaScript Client to connect to the Space) to your website by including this in the `` section of your website: + +```html + +``` + +### 3. That's it! + +Your website now has a chat widget that connects to your Gradio app! Users can click the chat button to open the widget and start interacting with your app. + +### Customization + +You can customize the appearance of the widget by modifying the CSS. Some ideas: +- Change the colors to match your website's theme +- Adjust the size and position of the widget +- Add animations for opening/closing +- Modify the message styling + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/Screen%20Recording%202024-12-19%20at%203.32.46%E2%80%AFPM.gif) + +If you build a website widget from a Gradio app, feel free to share it on X and tag [the Gradio account](https://x.com/Gradio), and we are happy to help you amplify! \ No newline at end of file diff --git a/guides/06_data-science-and-plots/01_creating-plots.md b/guides/06_data-science-and-plots/01_creating-plots.md new file mode 100644 index 0000000..76a088b --- /dev/null +++ b/guides/06_data-science-and-plots/01_creating-plots.md @@ -0,0 +1,72 @@ +# Creating Plots + +Gradio is a great way to create extremely customizable dashboards. Gradio comes with three native Plot components: `gr.LinePlot`, `gr.ScatterPlot` and `gr.BarPlot`. All these plots have the same API. Let's take a look how to set them up. + +## Creating a Plot with a pd.Dataframe + +Plots accept a pandas Dataframe as their value. The plot also takes `x` and `y` which represent the names of the columns that represent the x and y axes respectively. Here's a simple example: + +$code_plot_guide_line +$demo_plot_guide_line + +All plots have the same API, so you could swap this out with a `gr.ScatterPlot`: + +$code_plot_guide_scatter +$demo_plot_guide_scatter + +The y axis column in the dataframe should have a numeric type, but the x axis column can be anything from strings, numbers, categories, or datetimes. + +$code_plot_guide_scatter_nominal +$demo_plot_guide_scatter_nominal + +## Breaking out Series by Color + +You can break out your plot into series using the `color` argument. + +$code_plot_guide_series_nominal +$demo_plot_guide_series_nominal + +If you wish to assign series specific colors, use the `color_map` arg, e.g. `gr.ScatterPlot(..., color_map={'white': '#FF9988', 'asian': '#88EEAA', 'black': '#333388'})` + +The color column can be numeric type as well. + +$code_plot_guide_series_quantitative +$demo_plot_guide_series_quantitative + +## Aggregating Values + +You can aggregate values into groups using the `x_bin` and `y_aggregate` arguments. If your x-axis is numeric, providing an `x_bin` will create a histogram-style binning: + +$code_plot_guide_aggregate_quantitative +$demo_plot_guide_aggregate_quantitative + +If your x-axis is a string type instead, they will act as the category bins automatically: + +$code_plot_guide_aggregate_nominal +$demo_plot_guide_aggregate_nominal + +## Selecting Regions + +You can use the `.select` listener to select regions of a plot. Click and drag on the plot below to select part of the plot. + +$code_plot_guide_selection +$demo_plot_guide_selection + +You can combine this and the `.double_click` listener to create some zoom in/out effects by changing `x_lim` which sets the bounds of the x-axis: + +$code_plot_guide_zoom +$demo_plot_guide_zoom + +If you had multiple plots with the same x column, your event listeners could target the x limits of all other plots so that the x-axes stay in sync. + +$code_plot_guide_zoom_sync +$demo_plot_guide_zoom_sync + +## Making an Interactive Dashboard + +Take a look how you can have an interactive dashboard where the plots are functions of other Components. + +$code_plot_guide_interactive +$demo_plot_guide_interactive + +It's that simple to filter and control the data presented in your visualization! \ No newline at end of file diff --git a/guides/06_data-science-and-plots/02_time-plots.md b/guides/06_data-science-and-plots/02_time-plots.md new file mode 100644 index 0000000..4396ca4 --- /dev/null +++ b/guides/06_data-science-and-plots/02_time-plots.md @@ -0,0 +1,59 @@ +# Time Plots + +Creating visualizations with a time x-axis is a common use case. Let's dive in! + +## Creating a Plot with a pd.Dataframe + +Time plots need a datetime column on the x-axis. Here's a simple example with some flight data: + +$code_plot_guide_temporal +$demo_plot_guide_temporal + +## Aggregating by Time + +You may wish to bin data by time buckets. Use `x_bin` to do so, using a string suffix with "s", "m", "h" or "d", such as "15m" or "1d". + +$code_plot_guide_aggregate_temporal +$demo_plot_guide_aggregate_temporal + +## DateTime Components + +You can use `gr.DateTime` to accept input datetime data. This works well with plots for defining the x-axis range for the data. + +$code_plot_guide_datetime +$demo_plot_guide_datetime + +Note how `gr.DateTime` can accept a full datetime string, or a shorthand using `now - [0-9]+[smhd]` format to refer to a past time. + +You will often have many time plots in which case you'd like to keep the x-axes in sync. The `DateTimeRange` custom component keeps a set of datetime plots in sync, and also uses the `.select` listener of plots to allow you to zoom into plots while keeping plots in sync. + +Because it is a custom component, you first need to `pip install gradio_datetimerange`. Then run the following: + +$code_plot_guide_datetimerange +$demo_plot_guide_datetimerange + +Try zooming around in the plots and see how DateTimeRange updates. All the plots updates their `x_lim` in sync. You also have a "Back" link in the component to allow you to quickly zoom in and out. + +## RealTime Data + +In many cases, you're working with live, realtime date, not a static dataframe. In this case, you'd update the plot regularly with a `gr.Timer()`. Assuming there's a `get_data` method that gets the latest dataframe: + +```python +with gr.Blocks() as demo: + timer = gr.Timer(5) + plot1 = gr.BarPlot(x="time", y="price") + plot2 = gr.BarPlot(x="time", y="price", color="origin") + + timer.tick(lambda: [get_data(), get_data()], outputs=[plot1, plot2]) +``` + +You can also use the `every` shorthand to attach a `Timer` to a component that has a function value: + +```python +with gr.Blocks() as demo: + timer = gr.Timer(5) + plot1 = gr.BarPlot(get_data, x="time", y="price", every=timer) + plot2 = gr.BarPlot(get_data, x="time", y="price", color="origin", every=timer) +``` + + diff --git a/guides/06_data-science-and-plots/03_filters-tables-and-stats.md b/guides/06_data-science-and-plots/03_filters-tables-and-stats.md new file mode 100644 index 0000000..dc943dc --- /dev/null +++ b/guides/06_data-science-and-plots/03_filters-tables-and-stats.md @@ -0,0 +1,21 @@ +# Filters, Tables and Stats + +Your dashboard will likely consist of more than just plots. Let's take a look at some of the other common components of a dashboard. + +## Filters + +Use any of the standard Gradio form components to filter your data. You can do this via event listeners or function-as-value syntax. Let's look at the event listener approach first: + +$code_plot_guide_filters_events +$demo_plot_guide_filters_events + +And this would be the function-as-value approach for the same demo. + +$code_plot_guide_filters + +## Tables and Stats + +Add `gr.DataFrame` and `gr.Label` to your dashboard for some hard numbers. + +$code_plot_guide_tables_stats +$demo_plot_guide_tables_stats diff --git a/guides/06_data-science-and-plots/04_connecting-to-a-database.md b/guides/06_data-science-and-plots/04_connecting-to-a-database.md new file mode 100644 index 0000000..ddeb5a9 --- /dev/null +++ b/guides/06_data-science-and-plots/04_connecting-to-a-database.md @@ -0,0 +1,47 @@ +# Connecting to a Database + +The data you wish to visualize may be stored in a database. Let's use SQLAlchemy to quickly extract database content into pandas Dataframe format so we can use it in gradio. + +First install `pip install sqlalchemy` and then let's see some examples. + +## SQLite + +```python +from sqlalchemy import create_engine +import pandas as pd + +engine = create_engine('sqlite:///your_database.db') + +with gr.Blocks() as demo: + gr.LinePlot(pd.read_sql_query("SELECT time, price from flight_info;", engine), x="time", y="price") +``` + +Let's see a a more interactive plot involving filters that modify your SQL query: + +```python +from sqlalchemy import create_engine +import pandas as pd + +engine = create_engine('sqlite:///your_database.db') + +with gr.Blocks() as demo: + origin = gr.Dropdown(["DFW", "DAL", "HOU"], value="DFW", label="Origin") + + gr.LinePlot(lambda origin: pd.read_sql_query(f"SELECT time, price from flight_info WHERE origin = {origin};", engine), inputs=origin, x="time", y="price") +``` + +## Postgres, mySQL, and other databases + +If you're using a different database format, all you have to do is swap out the engine, e.g. + +```python +engine = create_engine('postgresql://username:password@host:port/database_name') +``` + +```python +engine = create_engine('mysql://username:password@host:port/database_name') +``` + +```python +engine = create_engine('oracle://username:password@host:port/database_name') +``` \ No newline at end of file diff --git a/guides/07_streaming/01_streaming-ai-generated-audio.md b/guides/07_streaming/01_streaming-ai-generated-audio.md new file mode 100644 index 0000000..23e75cf --- /dev/null +++ b/guides/07_streaming/01_streaming-ai-generated-audio.md @@ -0,0 +1,153 @@ +# Streaming AI Generated Audio + +Tags: AUDIO, STREAMING + +In this guide, we'll build a novel AI application to showcase Gradio's audio output streaming. We're going to a build a talking [Magic 8 Ball](https://en.wikipedia.org/wiki/Magic_8_Ball) 🎱 + +A Magic 8 Ball is a toy that answers any question after you shake it. Our application will do the same but it will also speak its response! + +We won't cover all the implementation details in this blog post but the code is freely available on [Hugging Face Spaces](https://huggingface.co/spaces/gradio/magic-8-ball). + +## The Overview + +Just like the classic Magic 8 Ball, a user should ask it a question orally and then wait for a response. Under the hood, we'll use Whisper to transcribe the audio and then use an LLM to generate a magic-8-ball-style answer. Finally, we'll use Parler TTS to read the response aloud. + +## The UI + +First let's define the UI and put placeholders for all the python logic. + +```python +import gradio as gr + +with gr.Blocks() as block: + gr.HTML( + f""" +

Magic 8 Ball 🎱

+

Ask a question and receive wisdom

+

Powered by Parler-TTS + """ + ) + with gr.Group(): + with gr.Row(): + audio_out = gr.Audio(label="Spoken Answer", streaming=True, autoplay=True) + answer = gr.Textbox(label="Answer") + state = gr.State() + with gr.Row(): + audio_in = gr.Audio(label="Speak your question", sources="microphone", type="filepath") + + audio_in.stop_recording(generate_response, audio_in, [state, answer, audio_out])\ + .then(fn=read_response, inputs=state, outputs=[answer, audio_out]) + +block.launch() +``` + +We're placing the output Audio and Textbox components and the input Audio component in separate rows. In order to stream the audio from the server, we'll set `streaming=True` in the output Audio component. We'll also set `autoplay=True` so that the audio plays as soon as it's ready. +We'll be using the Audio input component's `stop_recording` event to trigger our application's logic when a user stops recording from their microphone. + +We're separating the logic into two parts. First, `generate_response` will take the recorded audio, transcribe it and generate a response with an LLM. We're going to store the response in a `gr.State` variable that then gets passed to the `read_response` function that generates the audio. + +We're doing this in two parts because only `read_response` will require a GPU. Our app will run on Hugging Faces [ZeroGPU](https://huggingface.co/zero-gpu-explorers) which has time-based quotas. Since generating the response can be done with Hugging Face's Inference API, we shouldn't include that code in our GPU function as it will needlessly use our GPU quota. + +## The Logic + +As mentioned above, we'll use [Hugging Face's Inference API](https://huggingface.co/docs/huggingface_hub/guides/inference) to transcribe the audio and generate a response from an LLM. After instantiating the client, I use the `automatic_speech_recognition` method (this automatically uses Whisper running on Hugging Face's Inference Servers) to transcribe the audio. Then I pass the question to an LLM (Mistal-7B-Instruct) to generate a response. We are prompting the LLM to act like a magic 8 ball with the system message. + +Our `generate_response` function will also send empty updates to the output textbox and audio components (returning `None`). +This is because I want the Gradio progress tracker to be displayed over the components but I don't want to display the answer until the audio is ready. + + +```python +from huggingface_hub import InferenceClient + +client = InferenceClient(token=os.getenv("HF_TOKEN")) + +def generate_response(audio): + gr.Info("Transcribing Audio", duration=5) + question = client.automatic_speech_recognition(audio).text + + messages = [{"role": "system", "content": ("You are a magic 8 ball." + "Someone will present to you a situation or question and your job " + "is to answer with a cryptic adage or proverb such as " + "'curiosity killed the cat' or 'The early bird gets the worm'." + "Keep your answers short and do not include the phrase 'Magic 8 Ball' in your response. If the question does not make sense or is off-topic, say 'Foolish questions get foolish answers.'" + "For example, 'Magic 8 Ball, should I get a dog?', 'A dog is ready for you but are you ready for the dog?'")}, + {"role": "user", "content": f"Magic 8 Ball please answer this question - {question}"}] + + response = client.chat_completion(messages, max_tokens=64, seed=random.randint(1, 5000), + model="mistralai/Mistral-7B-Instruct-v0.3") + + response = response.choices[0].message.content.replace("Magic 8 Ball", "").replace(":", "") + return response, None, None +``` + + +Now that we have our text response, we'll read it aloud with Parler TTS. The `read_response` function will be a python generator that yields the next chunk of audio as it's ready. + + +We'll be using the [Mini v0.1](https://huggingface.co/parler-tts/parler_tts_mini_v0.1) for the feature extraction but the [Jenny fine tuned version](https://huggingface.co/parler-tts/parler-tts-mini-jenny-30H) for the voice. This is so that the voice is consistent across generations. + + +Streaming audio with transformers requires a custom Streamer class. You can see the implementation [here](https://huggingface.co/spaces/gradio/magic-8-ball/blob/main/streamer.py). Additionally, we'll convert the output to bytes so that it can be streamed faster from the backend. + + +```python +from streamer import ParlerTTSStreamer +from transformers import AutoTokenizer, AutoFeatureExtractor, set_seed +import numpy as np +import spaces +import torch +from threading import Thread + + +device = "cuda:0" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" +torch_dtype = torch.float16 if device != "cpu" else torch.float32 + +repo_id = "parler-tts/parler_tts_mini_v0.1" + +jenny_repo_id = "ylacombe/parler-tts-mini-jenny-30H" + +model = ParlerTTSForConditionalGeneration.from_pretrained( + jenny_repo_id, torch_dtype=torch_dtype, low_cpu_mem_usage=True +).to(device) + +tokenizer = AutoTokenizer.from_pretrained(repo_id) +feature_extractor = AutoFeatureExtractor.from_pretrained(repo_id) + +sampling_rate = model.audio_encoder.config.sampling_rate +frame_rate = model.audio_encoder.config.frame_rate + +@spaces.GPU +def read_response(answer): + + play_steps_in_s = 2.0 + play_steps = int(frame_rate * play_steps_in_s) + + description = "Jenny speaks at an average pace with a calm delivery in a very confined sounding environment with clear audio quality." + description_tokens = tokenizer(description, return_tensors="pt").to(device) + + streamer = ParlerTTSStreamer(model, device=device, play_steps=play_steps) + prompt = tokenizer(answer, return_tensors="pt").to(device) + + generation_kwargs = dict( + input_ids=description_tokens.input_ids, + prompt_input_ids=prompt.input_ids, + streamer=streamer, + do_sample=True, + temperature=1.0, + min_new_tokens=10, + ) + + set_seed(42) + thread = Thread(target=model.generate, kwargs=generation_kwargs) + thread.start() + + for new_audio in streamer: + print(f"Sample of length: {round(new_audio.shape[0] / sampling_rate, 2)} seconds") + yield answer, numpy_to_mp3(new_audio, sampling_rate=sampling_rate) +``` + +## Conclusion + +You can see our final application [here](https://huggingface.co/spaces/gradio/magic-8-ball)! + + diff --git a/guides/07_streaming/02_object-detection-from-webcam-with-webrtc.md b/guides/07_streaming/02_object-detection-from-webcam-with-webrtc.md new file mode 100644 index 0000000..8884cab --- /dev/null +++ b/guides/07_streaming/02_object-detection-from-webcam-with-webrtc.md @@ -0,0 +1,103 @@ +# Real Time Object Detection from a Webcam Stream with FastRTC + +Tags: VISION, STREAMING, WEBCAM + +In this guide, we'll use YOLOv10 to perform real-time object detection in Gradio from a user's webcam feed. We'll utilize [FastRTC](https://fastrtc.org/) a companion library from the gradio team for building low latency streaming web applications. You can see the finished product in action below: + + + +## Setting up + +Start by installing all the dependencies. Add the following lines to a `requirements.txt` file and run `pip install -r requirements.txt`: + +```bash +opencv-python +fastrtc +onnxruntime-gpu +``` + +We'll use the ONNX runtime to speed up YOLOv10 inference. This guide assumes you have access to a GPU. If you don't, change `onnxruntime-gpu` to `onnxruntime`. Without a GPU, the model will run slower, resulting in a laggy demo. + +We'll use OpenCV for image manipulation and the [WebRTC](https://webrtc.org/) protocol to achieve near-zero latency. + +**Note**: If you want to deploy this app on any cloud provider, you'll need to use your Hugging Face token to connect to a TURN server. Learn more in this [guide](https://fastrtc.org/deployment/). If you're not familiar with TURN servers, consult this [guide](https://www.twilio.com/docs/stun-turn/faq#faq-what-is-nat). + +## The Inference Function + +We'll download the YOLOv10 model from the Hugging Face hub and instantiate a custom inference class to use this model. + +The implementation of the inference class isn't covered in this guide, but you can find the source code [here](https://huggingface.co/spaces/freddyaboulton/webrtc-yolov10n/blob/main/inference.py#L9) if you're interested. This implementation borrows heavily from this [github repository](https://github.com/ibaiGorordo/ONNX-YOLOv8-Object-Detection). + +We're using the `yolov10-n` variant because it has the lowest latency. See the [Performance](https://github.com/THU-MIG/yolov10?tab=readme-ov-file#performance) section of the README in the YOLOv10 GitHub repository. + +```python +from huggingface_hub import hf_hub_download +from inference import YOLOv10 + +model_file = hf_hub_download( + repo_id="onnx-community/yolov10n", filename="onnx/model.onnx" +) + +model = YOLOv10(model_file) + +def detection(image, conf_threshold=0.3): + image = cv2.resize(image, (model.input_width, model.input_height)) + new_image = model.detect_objects(image, conf_threshold) + return new_image +``` + +Our inference function, `detection`, accepts a numpy array from the webcam and a desired confidence threshold. Object detection models like YOLO identify many objects and assign a confidence score to each. The lower the confidence, the higher the chance of a false positive. We'll let users adjust the confidence threshold. + +The function returns a numpy array corresponding to the same input image with all detected objects in bounding boxes. + +## The Gradio Demo + +The Gradio demo is straightforward, but we'll implement a few specific features: + +1. Use the `WebRTC` custom component to ensure input and output are sent to/from the server with WebRTC. +2. The [WebRTC](https://github.com/freddyaboulton/gradio-webrtc) component will serve as both an input and output component. +3. Utilize the `time_limit` parameter of the `stream` event. This parameter sets a processing time for each user's stream. In a multi-user setting, such as on Spaces, we'll stop processing the current user's stream after this period and move on to the next. + +We'll also apply custom CSS to center the webcam and slider on the page. + +```python +import gradio as gr +from fastrtc import WebRTC + +css = """.my-group {max-width: 600px !important; max-height: 600px !important;} + .my-column {display: flex !important; justify-content: center !important; align-items: center !important;}""" + +with gr.Blocks(css=css) as demo: + gr.HTML( + """ +

+ YOLOv10 Webcam Stream (Powered by WebRTC ⚡️) +

+ """ + ) + with gr.Column(elem_classes=["my-column"]): + with gr.Group(elem_classes=["my-group"]): + image = WebRTC(label="Stream", rtc_configuration=rtc_configuration) + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + + image.stream( + fn=detection, inputs=[image, conf_threshold], outputs=[image], time_limit=10 + ) + +if __name__ == "__main__": + demo.launch() +``` + +## Conclusion + +Our app is hosted on Hugging Face Spaces [here](https://huggingface.co/spaces/freddyaboulton/webrtc-yolov10n). + +You can use this app as a starting point to build real-time image applications with Gradio. Don't hesitate to open issues in the space or in the [FastRTC GitHub repo](https://github.com/gradio-app/fastrtc) if you have any questions or encounter problems. \ No newline at end of file diff --git a/guides/07_streaming/03_object-detection-from-video.md b/guides/07_streaming/03_object-detection-from-video.md new file mode 100644 index 0000000..17ac016 --- /dev/null +++ b/guides/07_streaming/03_object-detection-from-video.md @@ -0,0 +1,184 @@ +# Streaming Object Detection from Video + +Tags: VISION, STREAMING, VIDEO + +In this guide we'll use the [RT-DETR](https://huggingface.co/docs/transformers/en/model_doc/rt_detr) model to detect objects in a user uploaded video. We'll stream the results from the server using the new video streaming features introduced in Gradio 5.0. + +![video_object_detection_stream_latest](https://github.com/user-attachments/assets/4e27ac58-5ded-495d-9e0d-5e87e68b1355) + +## Setting up the Model + +First, we'll install the following requirements in our system: + +``` +opencv-python +torch +transformers>=4.43.0 +spaces +``` + +Then, we'll download the model from the Hugging Face Hub: + +```python +from transformers import RTDetrForObjectDetection, RTDetrImageProcessor + +image_processor = RTDetrImageProcessor.from_pretrained("PekingU/rtdetr_r50vd") +model = RTDetrForObjectDetection.from_pretrained("PekingU/rtdetr_r50vd").to("cuda") +``` +We're moving the model to the GPU. We'll be deploying our model to Hugging Face Spaces and running the inference in the [free ZeroGPU cluster](https://huggingface.co/zero-gpu-explorers). + + +## The Inference Function + +Our inference function will accept a video and a desired confidence threshold. +Object detection models identify many objects and assign a confidence score to each object. The lower the confidence, the higher the chance of a false positive. So we will let our users set the confidence threshold. + +Our function will iterate over the frames in the video and run the RT-DETR model over each frame. +We will then draw the bounding boxes for each detected object in the frame and save the frame to a new output video. +The function will yield each output video in chunks of two seconds. + +In order to keep inference times as low as possible on ZeroGPU (there is a time-based quota), +we will halve the original frames-per-second in the output video and resize the input frames to be half the original +size before running the model. + +The code for the inference function is below - we'll go over it piece by piece. + +```python +import spaces +import cv2 +from PIL import Image +import torch +import time +import numpy as np +import uuid + +from draw_boxes import draw_bounding_boxes + +SUBSAMPLE = 2 + +@spaces.GPU +def stream_object_detection(video, conf_threshold): + cap = cv2.VideoCapture(video) + + # This means we will output mp4 videos + video_codec = cv2.VideoWriter_fourcc(*"mp4v") # type: ignore + fps = int(cap.get(cv2.CAP_PROP_FPS)) + + desired_fps = fps // SUBSAMPLE + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) // 2 + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) // 2 + + iterating, frame = cap.read() + + n_frames = 0 + + # Use UUID to create a unique video file + output_video_name = f"output_{uuid.uuid4()}.mp4" + + # Output Video + output_video = cv2.VideoWriter(output_video_name, video_codec, desired_fps, (width, height)) # type: ignore + batch = [] + + while iterating: + frame = cv2.resize( frame, (0,0), fx=0.5, fy=0.5) + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + if n_frames % SUBSAMPLE == 0: + batch.append(frame) + if len(batch) == 2 * desired_fps: + inputs = image_processor(images=batch, return_tensors="pt").to("cuda") + + with torch.no_grad(): + outputs = model(**inputs) + + boxes = image_processor.post_process_object_detection( + outputs, + target_sizes=torch.tensor([(height, width)] * len(batch)), + threshold=conf_threshold) + + for i, (array, box) in enumerate(zip(batch, boxes)): + pil_image = draw_bounding_boxes(Image.fromarray(array), box, model, conf_threshold) + frame = np.array(pil_image) + # Convert RGB to BGR + frame = frame[:, :, ::-1].copy() + output_video.write(frame) + + batch = [] + output_video.release() + yield output_video_name + output_video_name = f"output_{uuid.uuid4()}.mp4" + output_video = cv2.VideoWriter(output_video_name, video_codec, desired_fps, (width, height)) # type: ignore + + iterating, frame = cap.read() + n_frames += 1 +``` + +1. **Reading from the Video** + +One of the industry standards for creating videos in python is OpenCV so we will use it in this app. + +The `cap` variable is how we will read from the input video. Whenever we call `cap.read()`, we are reading the next frame in the video. + +In order to stream video in Gradio, we need to yield a different video file for each "chunk" of the output video. +We create the next video file to write to with the `output_video = cv2.VideoWriter(output_video_name, video_codec, desired_fps, (width, height))` line. The `video_codec` is how we specify the type of video file. Only "mp4" and "ts" files are supported for video sreaming at the moment. + + +2. **The Inference Loop** + +For each frame in the video, we will resize it to be half the size. OpenCV reads files in `BGR` format, so will convert to the expected `RGB` format of transfomers. That's what the first two lines of the while loop are doing. + +We take every other frame and add it to a `batch` list so that the output video is half the original FPS. When the batch covers two seconds of video, we will run the model. The two second threshold was chosen to keep the processing time of each batch small enough so that video is smoothly displayed in the server while not requiring too many separate forward passes. In order for video streaming to work properly in Gradio, the batch size should be at least 1 second. + +We run the forward pass of the model and then use the `post_process_object_detection` method of the model to scale the detected bounding boxes to the size of the input frame. + +We make use of a custom function to draw the bounding boxes (source [here](https://huggingface.co/spaces/gradio/rt-detr-object-detection/blob/main/draw_boxes.py#L14)). We then have to convert from `RGB` to `BGR` before writing back to the output video. + +Once we have finished processing the batch, we create a new output video file for the next batch. + +## The Gradio Demo + +The UI code is pretty similar to other kinds of Gradio apps. +We'll use a standard two-column layout so that users can see the input and output videos side by side. + +In order for streaming to work, we have to set `streaming=True` in the output video. Setting the video +to autoplay is not necessary but it's a better experience for users. + +```python +import gradio as gr + +with gr.Blocks() as app: + gr.HTML( + """ +

+ Video Object Detection with RT-DETR +

+ """) + with gr.Row(): + with gr.Column(): + video = gr.Video(label="Video Source") + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + with gr.Column(): + output_video = gr.Video(label="Processed Video", streaming=True, autoplay=True) + + video.upload( + fn=stream_object_detection, + inputs=[video, conf_threshold], + outputs=[output_video], + ) + + +``` + + +## Conclusion + +You can check out our demo hosted on Hugging Face Spaces [here](https://huggingface.co/spaces/gradio/rt-detr-object-detection). + +It is also embedded on this page below + +$demo_rt-detr-object-detection \ No newline at end of file diff --git a/guides/07_streaming/04_conversational-chatbot.md b/guides/07_streaming/04_conversational-chatbot.md new file mode 100644 index 0000000..0246acc --- /dev/null +++ b/guides/07_streaming/04_conversational-chatbot.md @@ -0,0 +1,189 @@ +# Building Conversational Chatbots with Gradio + +Tags: AUDIO, STREAMING, CHATBOTS + +## Introduction + +The next generation of AI user interfaces is moving towards audio-native experiences. Users will be able to speak to chatbots and receive spoken responses in return. Several models have been built under this paradigm, including GPT-4o and [mini omni](https://github.com/gpt-omni/mini-omni). + +In this guide, we'll walk you through building your own conversational chat application using mini omni as an example. You can see a demo of the finished app below: + + + +## Application Overview + +Our application will enable the following user experience: + +1. Users click a button to start recording their message +2. The app detects when the user has finished speaking and stops recording +3. The user's audio is passed to the omni model, which streams back a response +4. After omni mini finishes speaking, the user's microphone is reactivated +5. All previous spoken audio, from both the user and omni, is displayed in a chatbot component + +Let's dive into the implementation details. + +## Processing User Audio + +We'll stream the user's audio from their microphone to the server and determine if the user has stopped speaking on each new chunk of audio. + +Here's our `process_audio` function: + +```python +import numpy as np +from utils import determine_pause + +def process_audio(audio: tuple, state: AppState): + if state.stream is None: + state.stream = audio[1] + state.sampling_rate = audio[0] + else: + state.stream = np.concatenate((state.stream, audio[1])) + + pause_detected = determine_pause(state.stream, state.sampling_rate, state) + state.pause_detected = pause_detected + + if state.pause_detected and state.started_talking: + return gr.Audio(recording=False), state + return None, state +``` + +This function takes two inputs: +1. The current audio chunk (a tuple of `(sampling_rate, numpy array of audio)`) +2. The current application state + +We'll use the following `AppState` dataclass to manage our application state: + +```python +from dataclasses import dataclass + +@dataclass +class AppState: + stream: np.ndarray | None = None + sampling_rate: int = 0 + pause_detected: bool = False + stopped: bool = False + conversation: list = [] +``` + +The function concatenates new audio chunks to the existing stream and checks if the user has stopped speaking. If a pause is detected, it returns an update to stop recording. Otherwise, it returns `None` to indicate no changes. + +The implementation of the `determine_pause` function is specific to the omni-mini project and can be found [here](https://huggingface.co/spaces/gradio/omni-mini/blob/eb027808c7bfe5179b46d9352e3fa1813a45f7c3/app.py#L98). + +## Generating the Response + +After processing the user's audio, we need to generate and stream the chatbot's response. Here's our `response` function: + +```python +import io +import tempfile +from pydub import AudioSegment + +def response(state: AppState): + if not state.pause_detected and not state.started_talking: + return None, AppState() + + audio_buffer = io.BytesIO() + + segment = AudioSegment( + state.stream.tobytes(), + frame_rate=state.sampling_rate, + sample_width=state.stream.dtype.itemsize, + channels=(1 if len(state.stream.shape) == 1 else state.stream.shape[1]), + ) + segment.export(audio_buffer, format="wav") + + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: + f.write(audio_buffer.getvalue()) + + state.conversation.append({"role": "user", + "content": {"path": f.name, + "mime_type": "audio/wav"}}) + + output_buffer = b"" + + for mp3_bytes in speaking(audio_buffer.getvalue()): + output_buffer += mp3_bytes + yield mp3_bytes, state + + with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: + f.write(output_buffer) + + state.conversation.append({"role": "assistant", + "content": {"path": f.name, + "mime_type": "audio/mp3"}}) + yield None, AppState(conversation=state.conversation) +``` + +This function: +1. Converts the user's audio to a WAV file +2. Adds the user's message to the conversation history +3. Generates and streams the chatbot's response using the `speaking` function +4. Saves the chatbot's response as an MP3 file +5. Adds the chatbot's response to the conversation history + +Note: The implementation of the `speaking` function is specific to the omni-mini project and can be found [here](https://huggingface.co/spaces/gradio/omni-mini/blob/main/app.py#L116). + +## Building the Gradio App + +Now let's put it all together using Gradio's Blocks API: + +```python +import gradio as gr + +def start_recording_user(state: AppState): + if not state.stopped: + return gr.Audio(recording=True) + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + input_audio = gr.Audio( + label="Input Audio", sources="microphone", type="numpy" + ) + with gr.Column(): + chatbot = gr.Chatbot(label="Conversation") + output_audio = gr.Audio(label="Output Audio", streaming=True, autoplay=True) + state = gr.State(value=AppState()) + + stream = input_audio.stream( + process_audio, + [input_audio, state], + [input_audio, state], + stream_every=0.5, + time_limit=30, + ) + respond = input_audio.stop_recording( + response, + [state], + [output_audio, state] + ) + respond.then(lambda s: s.conversation, [state], [chatbot]) + + restart = output_audio.stop( + start_recording_user, + [state], + [input_audio] + ) + cancel = gr.Button("Stop Conversation", variant="stop") + cancel.click(lambda: (AppState(stopped=True), gr.Audio(recording=False)), None, + [state, input_audio], cancels=[respond, restart]) + +if __name__ == "__main__": + demo.launch() +``` + +This setup creates a user interface with: +- An input audio component for recording user messages +- A chatbot component to display the conversation history +- An output audio component for the chatbot's responses +- A button to stop and reset the conversation + +The app streams user audio in 0.5-second chunks, processes it, generates responses, and updates the conversation history accordingly. + +## Conclusion + +This guide demonstrates how to build a conversational chatbot application using Gradio and the mini omni model. You can adapt this framework to create various audio-based chatbot demos. To see the full application in action, visit the Hugging Face Spaces demo: https://huggingface.co/spaces/gradio/omni-mini + +Feel free to experiment with different models, audio processing techniques, or user interface designs to create your own unique conversational AI experiences! \ No newline at end of file diff --git a/guides/07_streaming/05_real-time-speech-recognition.md b/guides/07_streaming/05_real-time-speech-recognition.md new file mode 100644 index 0000000..c3bdcbc --- /dev/null +++ b/guides/07_streaming/05_real-time-speech-recognition.md @@ -0,0 +1,70 @@ +# Real Time Speech Recognition + +Tags: ASR, SPEECH, STREAMING + +## Introduction + +Automatic speech recognition (ASR), the conversion of spoken speech to text, is a very important and thriving area of machine learning. ASR algorithms run on practically every smartphone, and are becoming increasingly embedded in professional workflows, such as digital assistants for nurses and doctors. Because ASR algorithms are designed to be used directly by customers and end users, it is important to validate that they are behaving as expected when confronted with a wide variety of speech patterns (different accents, pitches, and background audio conditions). + +Using `gradio`, you can easily build a demo of your ASR model and share that with a testing team, or test it yourself by speaking through the microphone on your device. + +This tutorial will show how to take a pretrained speech-to-text model and deploy it with a Gradio interface. We will start with a **_full-context_** model, in which the user speaks the entire audio before the prediction runs. Then we will adapt the demo to make it **_streaming_**, meaning that the audio model will convert speech as you speak. + +### Prerequisites + +Make sure you have the `gradio` Python package already [installed](/getting_started). You will also need a pretrained speech recognition model. In this tutorial, we will build demos from 2 ASR libraries: + +- Transformers (for this, `pip install torch transformers torchaudio`) + +Make sure you have at least one of these installed so that you can follow along the tutorial. You will also need `ffmpeg` [installed on your system](https://www.ffmpeg.org/download.html), if you do not already have it, to process files from the microphone. + +Here's how to build a real time speech recognition (ASR) app: + +1. [Set up the Transformers ASR Model](#1-set-up-the-transformers-asr-model) +2. [Create a Full-Context ASR Demo with Transformers](#2-create-a-full-context-asr-demo-with-transformers) +3. [Create a Streaming ASR Demo with Transformers](#3-create-a-streaming-asr-demo-with-transformers) + +## 1. Set up the Transformers ASR Model + +First, you will need to have an ASR model that you have either trained yourself or you will need to download a pretrained model. In this tutorial, we will start by using a pretrained ASR model from the model, `whisper`. + +Here is the code to load `whisper` from Hugging Face `transformers`. + +```python +from transformers import pipeline + +p = pipeline("automatic-speech-recognition", model="openai/whisper-base.en") +``` + +That's it! + +## 2. Create a Full-Context ASR Demo with Transformers + +We will start by creating a _full-context_ ASR demo, in which the user speaks the full audio before using the ASR model to run inference. This is very easy with Gradio -- we simply create a function around the `pipeline` object above. + +We will use `gradio`'s built in `Audio` component, configured to take input from the user's microphone and return a filepath for the recorded audio. The output component will be a plain `Textbox`. + +$code_asr +$demo_asr + +The `transcribe` function takes a single parameter, `audio`, which is a numpy array of the audio the user recorded. The `pipeline` object expects this in float32 format, so we convert it first to float32, and then extract the transcribed text. + +## 3. Create a Streaming ASR Demo with Transformers + +To make this a *streaming* demo, we need to make these changes: + +1. Set `streaming=True` in the `Audio` component +2. Set `live=True` in the `Interface` +3. Add a `state` to the interface to store the recorded audio of a user + +Tip: You can also set `time_limit` and `stream_every` parameters in the interface. The `time_limit` caps the amount of time each user's stream can take. The default is 30 seconds so users won't be able to stream audio for more than 30 seconds. The `stream_every` parameter controls how frequently data is sent to your function. By default it is 0.5 seconds. + +Take a look below. + +$code_stream_asr + +Notice that we now have a state variable because we need to track all the audio history. `transcribe` gets called whenever there is a new small chunk of audio, but we also need to keep track of all the audio spoken so far in the state. As the interface runs, the `transcribe` function gets called, with a record of all the previously spoken audio in the `stream` and the new chunk of audio as `new_chunk`. We return the new full audio to be stored back in its current state, and we also return the transcription. Here, we naively append the audio together and call the `transcriber` object on the entire audio. You can imagine more efficient ways of handling this, such as re-processing only the last 5 seconds of audio whenever a new chunk of audio is received. + +$demo_stream_asr + +Now the ASR model will run inference as you speak! diff --git a/guides/07_streaming/06_automatic-voice-detection.md b/guides/07_streaming/06_automatic-voice-detection.md new file mode 100644 index 0000000..a6e1496 --- /dev/null +++ b/guides/07_streaming/06_automatic-voice-detection.md @@ -0,0 +1,260 @@ +# Multimodal Gradio App Powered by Groq with Automatic Speech Detection + +Tags: AUDIO, STREAMING, CHATBOTS, VOICE + +## Introduction +Modern voice applications should feel natural and responsive, moving beyond the traditional "click-to-record" pattern. By combining Groq's fast inference capabilities with automatic speech detection, we can create a more intuitive interaction model where users can simply start talking whenever they want to engage with the AI. + +> Credits: VAD and Gradio code inspired by [WillHeld's Diva-audio-chat](https://huggingface.co/spaces/WillHeld/diva-audio-chat/tree/main). + +In this tutorial, you will learn how to create a multimodal Gradio and Groq app that has automatic speech detection. You can also watch the full video tutorial which includes a demo of the application: + + + +## Background + +Many voice apps currently work by the user clicking record, speaking, then stopping the recording. While this can be a powerful demo, the most natural mode of interaction with voice requires the app to dynamically detect when the user is speaking, so they can talk back and forth without having to continually click a record button. + +Creating a natural interaction with voice and text requires a dynamic and low-latency response. Thus, we need both automatic voice detection and fast inference. With @ricky0123/vad-web powering speech detection and Groq powering the LLM, both of these requirements are met. Groq provides a lightning fast response, and Gradio allows for easy creation of impressively functional apps. + +This tutorial shows you how to build a calorie tracking app where you speak to an AI that automatically detects when you start and stop your response, and provides its own text response back to guide you with questions that allow it to give a calorie estimate of your last meal. + +## Key Components + +- **Gradio**: Provides the web interface and audio handling capabilities +- **@ricky0123/vad-web**: Handles voice activity detection +- **Groq**: Powers fast LLM inference for natural conversations +- **Whisper**: Transcribes speech to text + +### Setting Up the Environment + +First, let’s install and import our essential libraries and set up a client for using the Groq API. Here’s how to do it: + +`requirements.txt` +``` +gradio +groq +numpy +soundfile +librosa +spaces +xxhash +datasets +``` + +`app.py` +```python +import groq +import gradio as gr +import soundfile as sf +from dataclasses import dataclass, field +import os + +# Initialize Groq client securely +api_key = os.environ.get("GROQ_API_KEY") +if not api_key: + raise ValueError("Please set the GROQ_API_KEY environment variable.") +client = groq.Client(api_key=api_key) +``` + +Here, we’re pulling in key libraries to interact with the Groq API, build a sleek UI with Gradio, and handle audio data. We’re accessing the Groq API key securely with a key stored in an environment variable, which is a security best practice for avoiding leaking the API key. + +--- + +### State Management for Seamless Conversations + +We need a way to keep track of our conversation history, so the chatbot remembers past interactions, and manage other states like whether recording is currently active. To do this, let’s create an `AppState` class: + +```python +@dataclass +class AppState: + conversation: list = field(default_factory=list) + stopped: bool = False + model_outs: Any = None +``` + +Our `AppState` class is a handy tool for managing conversation history and tracking whether recording is on or off. Each instance will have its own fresh list of conversations, making sure chat history is isolated to each session. + +--- + +### Transcribing Audio with Whisper on Groq + +Next, we’ll create a function to transcribe the user’s audio input into text using Whisper, a powerful transcription model hosted on Groq. This transcription will also help us determine whether there’s meaningful speech in the input. Here’s how: + +```python +def transcribe_audio(client, file_name): + if file_name is None: + return None + + try: + with open(file_name, "rb") as audio_file: + response = client.audio.transcriptions.with_raw_response.create( + model="whisper-large-v3-turbo", + file=("audio.wav", audio_file), + response_format="verbose_json", + ) + completion = process_whisper_response(response.parse()) + return completion + except Exception as e: + print(f"Error in transcription: {e}") + return f"Error in transcription: {str(e)}" +``` + +This function opens the audio file and sends it to Groq’s Whisper model for transcription, requesting detailed JSON output. verbose_json is needed to get information to determine if speech was included in the audio. We also handle any potential errors so our app doesn’t fully crash if there’s an issue with the API request. + +```python +def process_whisper_response(completion): + """ + Process Whisper transcription response and return text or null based on no_speech_prob + + Args: + completion: Whisper transcription response object + + Returns: + str or None: Transcribed text if no_speech_prob <= 0.7, otherwise None + """ + if completion.segments and len(completion.segments) > 0: + no_speech_prob = completion.segments[0].get('no_speech_prob', 0) + print("No speech prob:", no_speech_prob) + + if no_speech_prob > 0.7: + return None + + return completion.text.strip() + + return None +``` + +We also need to interpret the audio data response. The process_whisper_response function takes the resulting completion from Whisper and checks if the audio was just background noise or had actual speaking that was transcribed. It uses a threshold of 0.7 to interpret the no_speech_prob, and will return None if there was no speech. Otherwise, it will return the text transcript of the conversational response from the human. + + +--- + +### Adding Conversational Intelligence with LLM Integration + +Our chatbot needs to provide intelligent, friendly responses that flow naturally. We’ll use a Groq-hosted Llama-3.2 for this: + +```python +def generate_chat_completion(client, history): + messages = [] + messages.append( + { + "role": "system", + "content": "In conversation with the user, ask questions to estimate and provide (1) total calories, (2) protein, carbs, and fat in grams, (3) fiber and sugar content. Only ask *one question at a time*. Be conversational and natural.", + } + ) + + for message in history: + messages.append(message) + + try: + completion = client.chat.completions.create( + model="llama-3.2-11b-vision-preview", + messages=messages, + ) + return completion.choices[0].message.content + except Exception as e: + return f"Error in generating chat completion: {str(e)}" +``` + +We’re defining a system prompt to guide the chatbot’s behavior, ensuring it asks one question at a time and keeps things conversational. This setup also includes error handling to ensure the app gracefully manages any issues. + +--- + +### Voice Activity Detection for Hands-Free Interaction + +To make our chatbot hands-free, we’ll add Voice Activity Detection (VAD) to automatically detect when someone starts or stops speaking. Here’s how to implement it using ONNX in JavaScript: + +```javascript +async function main() { + const script1 = document.createElement("script"); + script1.src = "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.14.0/dist/ort.js"; + document.head.appendChild(script1) + const script2 = document.createElement("script"); + script2.onload = async () => { + console.log("vad loaded"); + var record = document.querySelector('.record-button'); + record.textContent = "Just Start Talking!" + + const myvad = await vad.MicVAD.new({ + onSpeechStart: () => { + var record = document.querySelector('.record-button'); + var player = document.querySelector('#streaming-out') + if (record != null && (player == null || player.paused)) { + record.click(); + } + }, + onSpeechEnd: (audio) => { + var stop = document.querySelector('.stop-button'); + if (stop != null) { + stop.click(); + } + } + }) + myvad.start() + } + script2.src = "https://cdn.jsdelivr.net/npm/@ricky0123/vad-web@0.0.7/dist/bundle.min.js"; +} +``` + +This script loads our VAD model and sets up functions to start and stop recording automatically. When the user starts speaking, it triggers the recording, and when they stop, it ends the recording. + +--- + +### Building a User Interface with Gradio + +Now, let’s create an intuitive and visually appealing user interface with Gradio. This interface will include an audio input for capturing voice, a chat window for displaying responses, and state management to keep things synchronized. + +```python +with gr.Blocks() as demo: + with gr.Row(): + input_audio = gr.Audio( + label="Input Audio", + sources=["microphone"], + type="numpy", + streaming=False, + waveform_options=gr.WaveformOptions(waveform_color="#B83A4B"), + ) + with gr.Row(): + chatbot = gr.Chatbot(label="Conversation") + state = gr.State(value=AppState()) +demo.launch(theme=theme, js=js) +``` + +In this code block, we’re using Gradio’s `Blocks` API to create an interface with an audio input, a chat display, and an application state manager. The color customization for the waveform adds a nice visual touch. + +--- + +### Handling Recording and Responses + +Finally, let’s link the recording and response components to ensure the app reacts smoothly to user inputs and provides responses in real-time. + +```python + stream = input_audio.start_recording( + process_audio, + [input_audio, state], + [input_audio, state], + ) + respond = input_audio.stop_recording( + response, [state, input_audio], [state, chatbot] + ) +``` + +These lines set up event listeners for starting and stopping the recording, processing the audio input, and generating responses. By linking these events, we create a cohesive experience where users can simply talk, and the chatbot handles the rest. + +--- + +## Summary + +1. When you open the app, the VAD system automatically initializes and starts listening for speech +2. As soon as you start talking, it triggers the recording automatically +3. When you stop speaking, the recording ends and: + - The audio is transcribed using Whisper + - The transcribed text is sent to the LLM + - The LLM generates a response about calorie tracking + - The response is displayed in the chat interface +4. This creates a natural back-and-forth conversation where you can simply talk about your meals and get instant feedback on nutritional content + +This app demonstrates how to create a natural voice interface that feels responsive and intuitive. By combining Groq's fast inference with automatic speech detection, we've eliminated the need for manual recording controls while maintaining high-quality interactions. The result is a practical calorie tracking assistant that users can simply talk to as naturally as they would to a human nutritionist. + +Link to GitHub repository: [Groq Gradio Basics](https://github.com/bklieger-groq/gradio-groq-basics/tree/main/calorie-tracker) \ No newline at end of file diff --git a/guides/08_custom-components/01_custom-components-in-five-minutes.md b/guides/08_custom-components/01_custom-components-in-five-minutes.md new file mode 100644 index 0000000..23fa14b --- /dev/null +++ b/guides/08_custom-components/01_custom-components-in-five-minutes.md @@ -0,0 +1,123 @@ +# Custom Components in 5 minutes + +Gradio includes the ability for developers to create their own custom components and use them in Gradio apps. You can publish your components as Python packages so that other users can use them as well. + +Users will be able to use all of Gradio's existing functions, such as `gr.Blocks`, `gr.Interface`, API usage, themes, etc. with Custom Components. This guide will cover how to get started making custom components. + +## Installation + +You will need to have: + +* Python 3.10+ (install here) +* pip 21.3+ (`python -m pip install --upgrade pip`) +* Node.js 20+ (install here) +* npm 9+ (install here) +* Gradio 5+ (`pip install --upgrade gradio`) + +## The Workflow + +The Custom Components workflow consists of 4 steps: create, dev, build, and publish. + +1. create: creates a template for you to start developing a custom component. +2. dev: launches a development server with a sample app & hot reloading allowing you to easily develop your custom component +3. build: builds a python package containing to your custom component's Python and JavaScript code -- this makes things official! +4. publish: uploads your package to [PyPi](https://pypi.org/) and/or a sample app to [HuggingFace Spaces](https://hf.co/spaces). + +Each of these steps is done via the Custom Component CLI. You can invoke it with `gradio cc` or `gradio component` + +Tip: Run `gradio cc --help` to get a help menu of all available commands. There are some commands that are not covered in this guide. You can also append `--help` to any command name to bring up a help page for that command, e.g. `gradio cc create --help`. + +## 1. create + +Bootstrap a new template by running the following in any working directory: + +```bash +gradio cc create MyComponent --template SimpleTextbox +``` + +Instead of `MyComponent`, give your component any name. + +Instead of `SimpleTextbox`, you can use any Gradio component as a template. `SimpleTextbox` is actually a special component that a stripped-down version of the `Textbox` component that makes it particularly useful when creating your first custom component. +Some other components that are good if you are starting out: `SimpleDropdown`, `SimpleImage`, or `File`. + +Tip: Run `gradio cc show` to get a list of available component templates. + +The `create` command will: + +1. Create a directory with your component's name in lowercase with the following structure: +```directory +- backend/ <- The python code for your custom component +- frontend/ <- The javascript code for your custom component +- demo/ <- A sample app using your custom component. Modify this to develop your component! +- pyproject.toml <- Used to build the package and specify package metadata. +``` + +2. Install the component in development mode + +Each of the directories will have the code you need to get started developing! + +## 2. dev + +Once you have created your new component, you can start a development server by `entering the directory` and running + +```bash +gradio cc dev +``` + +You'll see several lines that are printed to the console. +The most important one is the one that says: + +> Frontend Server (Go here): http://localhost:7861/ + +The port number might be different for you. +Click on that link to launch the demo app in hot reload mode. +Now, you can start making changes to the backend and frontend you'll see the results reflected live in the sample app! +We'll go through a real example in a later guide. + +Tip: You don't have to run dev mode from your custom component directory. The first argument to `dev` mode is the path to the directory. By default it uses the current directory. + +## 3. build + +Once you are satisfied with your custom component's implementation, you can `build` it to use it outside of the development server. + +From your component directory, run: + +```bash +gradio cc build +``` + +This will create a `tar.gz` and `.whl` file in a `dist/` subdirectory. +If you or anyone installs that `.whl` file (`pip install `) they will be able to use your custom component in any gradio app! + +The `build` command will also generate documentation for your custom component. This takes the form of an interactive space and a static `README.md`. You can disable this by passing `--no-generate-docs`. You can read more about the documentation generator in [the dedicated guide](https://gradio.app/guides/documenting-custom-components). + +## 4. publish + +Right now, your package is only available on a `.whl` file on your computer. +You can share that file with the world with the `publish` command! + +Simply run the following command from your component directory: + +```bash +gradio cc publish +``` + +This will guide you through the following process: + +1. Upload your distribution files to PyPi. This makes it easier to upload the demo to Hugging Face spaces. Otherwise your package must be at a publicly available url. If you decide to upload to PyPi, you will need a PyPI username and password. You can get one [here](https://pypi.org/account/register/). +2. Upload a demo of your component to hugging face spaces. This is also optional. + + +Here is an example of what publishing looks like: + + + + +## Conclusion + +Now that you know the high-level workflow of creating custom components, you can go in depth in the next guides! +After reading the guides, check out this [collection](https://huggingface.co/collections/gradio/custom-components-65497a761c5192d981710b12) of custom components on the HuggingFace Hub so you can learn from other's code. + +Tip: If you want to start off from someone else's custom component see this [guide](./frequently-asked-questions#do-i-always-need-to-start-my-component-from-scratch). diff --git a/guides/08_custom-components/02_key-component-concepts.md b/guides/08_custom-components/02_key-component-concepts.md new file mode 100644 index 0000000..35ea217 --- /dev/null +++ b/guides/08_custom-components/02_key-component-concepts.md @@ -0,0 +1,124 @@ +# Gradio Components: The Key Concepts + +In this section, we discuss a few important concepts when it comes to components in Gradio. +It's important to understand these concepts when developing your own component. +Otherwise, your component may behave very different to other Gradio components! + +Tip: You can skip this section if you are familiar with the internals of the Gradio library, such as each component's preprocess and postprocess methods. + +## Interactive vs Static + +Every component in Gradio comes in a `static` variant, and most come in an `interactive` version as well. +The `static` version is used when a component is displaying a value, and the user can **NOT** change that value by interacting with it. +The `interactive` version is used when the user is able to change the value by interacting with the Gradio UI. + +Let's see some examples: + +```python +import gradio as gr + +with gr.Blocks() as demo: + gr.Textbox(value="Hello", interactive=True) + gr.Textbox(value="Hello", interactive=False) + +demo.launch() + +``` +This will display two textboxes. +The only difference: you'll be able to edit the value of the Gradio component on top, and you won't be able to edit the variant on the bottom (i.e. the textbox will be disabled). + +Perhaps a more interesting example is with the `Image` component: + +```python +import gradio as gr + +with gr.Blocks() as demo: + gr.Image(interactive=True) + gr.Image(interactive=False) + +demo.launch() +``` + +The interactive version of the component is much more complex -- you can upload images or snap a picture from your webcam -- while the static version can only be used to display images. + +Not every component has a distinct interactive version. For example, the `gr.AnnotatedImage` only appears as a static version since there's no way to interactively change the value of the annotations or the image. + +### What you need to remember + +* Gradio will use the interactive version (if available) of a component if that component is used as the **input** to any event; otherwise, the static version will be used. + +* When you design custom components, you **must** accept the boolean interactive keyword in the constructor of your Python class. In the frontend, you **may** accept the `interactive` property, a `bool` which represents whether the component should be static or interactive. If you do not use this property in the frontend, the component will appear the same in interactive or static mode. + +## The value and how it is preprocessed/postprocessed + +The most important attribute of a component is its `value`. +Every component has a `value`. +The value that is typically set by the user in the frontend (if the component is interactive) or displayed to the user (if it is static). +It is also this value that is sent to the backend function when a user triggers an event, or returned by the user's function e.g. at the end of a prediction. + +So this value is passed around quite a bit, but sometimes the format of the value needs to change between the frontend and backend. +Take a look at this example: + +```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(width=200, height=200), "image") +demo.launch() +``` + +This will create a Gradio app which has an `Image` component as the input and the output. +In the frontend, the Image component will actually **upload** the file to the server and send the **filepath** but this is converted to a `numpy` array before it is sent to a user's function. +Conversely, when the user returns a `numpy` array from their function, the numpy array is converted to a file so that it can be sent to the frontend and displayed by the `Image` component. + +Tip: By default, the `Image` component sends numpy arrays to the python function because it is a common choice for machine learning engineers, though the Image component also supports other formats using the `type` parameter. Read the `Image` docs [here](https://www.gradio.app/docs/image) to learn more. + +Each component does two conversions: + +1. `preprocess`: Converts the `value` from the format sent by the frontend to the format expected by the python function. This usually involves going from a web-friendly **JSON** structure to a **python-native** data structure, like a `numpy` array or `PIL` image. The `Audio`, `Image` components are good examples of `preprocess` methods. + +2. `postprocess`: Converts the value returned by the python function to the format expected by the frontend. This usually involves going from a **python-native** data-structure, like a `PIL` image to a **JSON** structure. + +### What you need to remember + +* Every component must implement `preprocess` and `postprocess` methods. In the rare event that no conversion needs to happen, simply return the value as-is. `Textbox` and `Number` are examples of this. + +* As a component author, **YOU** control the format of the data displayed in the frontend as well as the format of the data someone using your component will receive. Think of an ergonomic data-structure a **python** developer will find intuitive, and control the conversion from a **Web-friendly JSON** data structure (and vice-versa) with `preprocess` and `postprocess.` + +## The "Example Version" of a Component + +Gradio apps support providing example inputs -- and these are very useful in helping users get started using your Gradio app. +In `gr.Interface`, you can provide examples using the `examples` keyword, and in `Blocks`, you can provide examples using the special `gr.Examples` component. + +At the bottom of this screenshot, we show a miniature example image of a cheetah that, when clicked, will populate the same image in the input Image component: + +![img](https://user-images.githubusercontent.com/1778297/277548211-a3cb2133-2ffc-4cdf-9a83-3e8363b57ea6.png) + + +To enable the example view, you must have the following two files in the top of the `frontend` directory: + +* `Example.svelte`: this corresponds to the "example version" of your component +* `Index.svelte`: this corresponds to the "regular version" + +In the backend, you typically don't need to do anything. The user-provided example `value` is processed using the same `.postprocess()` method described earlier. If you'd like to do process the data differently (for example, if the `.postprocess()` method is computationally expensive), then you can write your own `.process_example()` method for your custom component, which will be used instead. + +The `Example.svelte` file and `process_example()` method will be covered in greater depth in the dedicated [frontend](./frontend) and [backend](./backend) guides respectively. + +### What you need to remember + +* If you expect your component to be used as input, it is important to define an "Example" view. +* If you don't, Gradio will use a default one but it won't be as informative as it can be! + +## Conclusion + +Now that you know the most important pieces to remember about Gradio components, you can start to design and build your own! diff --git a/guides/08_custom-components/03_configuration.md b/guides/08_custom-components/03_configuration.md new file mode 100644 index 0000000..234cae3 --- /dev/null +++ b/guides/08_custom-components/03_configuration.md @@ -0,0 +1,100 @@ +# Configuring Your Custom Component + +The custom components workflow focuses on [convention over configuration](https://en.wikipedia.org/wiki/Convention_over_configuration) to reduce the number of decisions you as a developer need to make when developing your custom component. +That being said, you can still configure some aspects of the custom component package and directory. +This guide will cover how. + +## The Package Name + +By default, all custom component packages are called `gradio_` where `component-name` is the name of the component's python class in lowercase. + +As an example, let's walkthrough changing the name of a component from `gradio_mytextbox` to `supertextbox`. + +1. Modify the `name` in the `pyproject.toml` file. + +```bash +[project] +name = "supertextbox" +``` + +2. Change all occurrences of `gradio_` in `pyproject.toml` to `` + +```bash +[tool.hatch.build] +artifacts = ["/backend/supertextbox/templates", "*.pyi"] + +[tool.hatch.build.targets.wheel] +packages = ["/backend/supertextbox"] +``` + +3. Rename the `gradio_` directory in `backend/` to `` + +```bash +mv backend/gradio_mytextbox backend/supertextbox +``` + + +Tip: Remember to change the import statement in `demo/app.py`! + +## Top Level Python Exports + +By default, only the custom component python class is a top level export. +This means that when users type `from gradio_ import ...`, the only class that will be available is the custom component class. +To add more classes as top level exports, modify the `__all__` property in `__init__.py` + +```python +from .mytextbox import MyTextbox +from .mytextbox import AdditionalClass, additional_function + +__all__ = ['MyTextbox', 'AdditionalClass', 'additional_function'] +``` + +## Python Dependencies + +You can add python dependencies by modifying the `dependencies` key in `pyproject.toml` + +```bash +dependencies = ["gradio", "numpy", "PIL"] +``` + + +Tip: Remember to run `gradio cc install` when you add dependencies! + +## Javascript Dependencies + +You can add JavaScript dependencies by modifying the `"dependencies"` key in `frontend/package.json` + +```json +"dependencies": { + "@gradio/atoms": "0.2.0-beta.4", + "@gradio/statustracker": "0.3.0-beta.6", + "@gradio/utils": "0.2.0-beta.4", + "your-npm-package": "" +} +``` + +## Directory Structure + +By default, the CLI will place the Python code in `backend` and the JavaScript code in `frontend`. +It is not recommended to change this structure since it makes it easy for a potential contributor to look at your source code and know where everything is. +However, if you did want to this is what you would have to do: + +1. Place the Python code in the subdirectory of your choosing. Remember to modify the `[tool.hatch.build]` `[tool.hatch.build.targets.wheel]` in the `pyproject.toml` to match! + +2. Place the JavaScript code in the subdirectory of your choosing. + +2. Add the `FRONTEND_DIR` property on the component python class. It must be the relative path from the file where the class is defined to the location of the JavaScript directory. + +```python +class SuperTextbox(Component): + FRONTEND_DIR = "../../frontend/" +``` + +The JavaScript and Python directories must be under the same common directory! + +## Conclusion + + +Sticking to the defaults will make it easy for others to understand and contribute to your custom component. +After all, the beauty of open source is that anyone can help improve your code! +But if you ever need to deviate from the defaults, you know how! \ No newline at end of file diff --git a/guides/08_custom-components/04_backend.md b/guides/08_custom-components/04_backend.md new file mode 100644 index 0000000..f5d52ef --- /dev/null +++ b/guides/08_custom-components/04_backend.md @@ -0,0 +1,227 @@ +# The Backend 🐍 + +This guide will cover everything you need to know to implement your custom component's backend processing. + +## Which Class to Inherit From + +All components inherit from one of three classes `Component`, `FormComponent`, or `BlockContext`. +You need to inherit from one so that your component behaves like all other gradio components. +When you start from a template with `gradio cc create --template`, you don't need to worry about which one to choose since the template uses the correct one. +For completeness, and in the event that you need to make your own component from scratch, we explain what each class is for. + +* `FormComponent`: Use this when you want your component to be grouped together in the same `Form` layout with other `FormComponents`. The `Slider`, `Textbox`, and `Number` components are all `FormComponents`. +* `BlockContext`: Use this when you want to place other components "inside" your component. This enabled `with MyComponent() as component:` syntax. +* `Component`: Use this for all other cases. + +Tip: If your component supports streaming output, inherit from the `StreamingOutput` class. + +Tip: If you inherit from `BlockContext`, you also need to set the metaclass to be `ComponentMeta`. See example below. + +```python +from gradio.blocks import BlockContext +from gradio.component_meta import ComponentMeta + + + + +@document() +class Row(BlockContext, metaclass=ComponentMeta): + pass +``` + +## The methods you need to implement + +When you inherit from any of these classes, the following methods must be implemented. +Otherwise the Python interpreter will raise an error when you instantiate your component! + +### `preprocess` and `postprocess` + +Explained in the [Key Concepts](./key-component-concepts#the-value-and-how-it-is-preprocessed-postprocessed) guide. +They handle the conversion from the data sent by the frontend to the format expected by the python function. + +```python + def preprocess(self, x: Any) -> Any: + """ + Convert from the web-friendly (typically JSON) value in the frontend to the format expected by the python function. + """ + return x + + def postprocess(self, y): + """ + Convert from the data returned by the python function to the web-friendly (typically JSON) value expected by the frontend. + """ + return y +``` + +### `process_example` + +Takes in the original Python value and returns the modified value that should be displayed in the examples preview in the app. +If not provided, the `.postprocess()` method is used instead. Let's look at the following example from the `SimpleDropdown` component. + +```python +def process_example(self, input_data): + return next((c[0] for c in self.choices if c[1] == input_data), None) +``` + +Since `self.choices` is a list of tuples corresponding to (`display_name`, `value`), this converts the value that a user provides to the display value (or if the value is not present in `self.choices`, it is converted to `None`). + + +### `api_info` + +A JSON-schema representation of the value that the `preprocess` expects. +This powers api usage via the gradio clients. +You do **not** need to implement this yourself if you components specifies a `data_model`. +The `data_model` in the following section. + +```python +def api_info(self) -> dict[str, list[str]]: + """ + A JSON-schema representation of the value that the `preprocess` expects and the `postprocess` returns. + """ + pass +``` + +### `example_payload` + +An example payload for your component, e.g. something that can be passed into the `.preprocess()` method +of your component. The example input is displayed in the `View API` page of a Gradio app that uses your custom component. +Must be JSON-serializable. If your component expects a file, it is best to use a publicly accessible URL. + +```python +def example_payload(self) -> Any: + """ + The example inputs for this component for API usage. Must be JSON-serializable. + """ + pass +``` + +### `example_value` + +An example value for your component, e.g. something that can be passed into the `.postprocess()` method +of your component. This is used as the example value in the default app that is created in custom component development. + +```python +def example_payload(self) -> Any: + """ + The example inputs for this component for API usage. Must be JSON-serializable. + """ + pass +``` + +### `flag` + +Write the component's value to a format that can be stored in the `csv` or `json` file used for flagging. +You do **not** need to implement this yourself if you components specifies a `data_model`. +The `data_model` in the following section. + +```python +def flag(self, x: Any | GradioDataModel, flag_dir: str | Path = "") -> str: + pass +``` + +### `read_from_flag` +Convert from the format stored in the `csv` or `json` file used for flagging to the component's python `value`. +You do **not** need to implement this yourself if you components specifies a `data_model`. +The `data_model` in the following section. + +```python +def read_from_flag( + self, + x: Any, +) -> GradioDataModel | Any: + """ + Convert the data from the csv or jsonl file into the component state. + """ + return x +``` + +## The `data_model` + +The `data_model` is how you define the expected data format your component's value will be stored in the frontend. +It specifies the data format your `preprocess` method expects and the format the `postprocess` method returns. +It is not necessary to define a `data_model` for your component but it greatly simplifies the process of creating a custom component. +If you define a custom component you only need to implement four methods - `preprocess`, `postprocess`, `example_payload`, and `example_value`! + +You define a `data_model` by defining a [pydantic model](https://docs.pydantic.dev/latest/concepts/models/#basic-model-usage) that inherits from either `GradioModel` or `GradioRootModel`. + +This is best explained with an example. Let's look at the core `Video` component, which stores the video data as a JSON object with two keys `video` and `subtitles` which point to separate files. + +```python +from gradio.data_classes import FileData, GradioModel + +class VideoData(GradioModel): + video: FileData + subtitles: Optional[FileData] = None + +class Video(Component): + data_model = VideoData +``` + +By adding these four lines of code, your component automatically implements the methods needed for API usage, the flagging methods, and example caching methods! +It also has the added benefit of self-documenting your code. +Anyone who reads your component code will know exactly the data it expects. + +Tip: If your component expects files to be uploaded from the frontend, your must use the `FileData` model! It will be explained in the following section. + +Tip: Read the pydantic docs [here](https://docs.pydantic.dev/latest/concepts/models/#basic-model-usage). + +The difference between a `GradioModel` and a `GradioRootModel` is that the `RootModel` will not serialize the data to a dictionary. +For example, the `Names` model will serialize the data to `{'names': ['freddy', 'pete']}` whereas the `NamesRoot` model will serialize it to `['freddy', 'pete']`. + +```python +from typing import List + +class Names(GradioModel): + names: List[str] + +class NamesRoot(GradioRootModel): + root: List[str] +``` + +Even if your component does not expect a "complex" JSON data structure it can be beneficial to define a `GradioRootModel` so that you don't have to worry about implementing the API and flagging methods. + +Tip: Use classes from the Python typing library to type your models. e.g. `List` instead of `list`. + +## Handling Files + +If your component expects uploaded files as input, or returns saved files to the frontend, you **MUST** use the `FileData` to type the files in your `data_model`. + +When you use the `FileData`: + +* Gradio knows that it should allow serving this file to the frontend. Gradio automatically blocks requests to serve arbitrary files in the computer running the server. + +* Gradio will automatically place the file in a cache so that duplicate copies of the file don't get saved. + +* The client libraries will automatically know that they should upload input files prior to sending the request. They will also automatically download files. + +If you do not use the `FileData`, your component will not work as expected! + + +## Adding Event Triggers To Your Component + +The events triggers for your component are defined in the `EVENTS` class attribute. +This is a list that contains the string names of the events. +Adding an event to this list will automatically add a method with that same name to your component! + +You can import the `Events` enum from `gradio.events` to access commonly used events in the core gradio components. + +For example, the following code will define `text_submit`, `file_upload` and `change` methods in the `MyComponent` class. + +```python +from gradio.events import Events +from gradio.components import FormComponent + +class MyComponent(FormComponent): + + EVENTS = [ + "text_submit", + "file_upload", + Events.change + ] +``` + + +Tip: Don't forget to also handle these events in the JavaScript code! + +## Conclusion + diff --git a/guides/08_custom-components/05_frontend.md b/guides/08_custom-components/05_frontend.md new file mode 100644 index 0000000..1ce51fc --- /dev/null +++ b/guides/08_custom-components/05_frontend.md @@ -0,0 +1,369 @@ +# The Frontend 🌐⭐️ + +This guide will cover everything you need to know to implement your custom component's frontend. + +Tip: Gradio components use Svelte. Writing Svelte is fun! If you're not familiar with it, we recommend checking out their interactive [guide](https://learn.svelte.dev/tutorial/welcome-to-svelte). + +## The directory structure + +The frontend code should have, at minimum, three files: + +* `Index.svelte`: This is the main export and where your component's layout and logic should live. +* `Example.svelte`: This is where the example view of the component is defined. + +Feel free to add additional files and subdirectories. +If you want to export any additional modules, remember to modify the `package.json` file + +```json +"exports": { + ".": "./Index.svelte", + "./example": "./Example.svelte", + "./package.json": "./package.json" +}, +``` + +## The Index.svelte file + +Your component should expose the following props that will be passed down from the parent Gradio application. + +```typescript +import type { LoadingStatus } from "@gradio/statustracker"; +import type { Gradio } from "@gradio/utils"; + +export let gradio: Gradio<{ + event_1: never; + event_2: never; +}>; + +export let elem_id = ""; +export let elem_classes: string[] = []; +export let scale: number | null = null; +export let min_width: number | undefined = undefined; +export let loading_status: LoadingStatus | undefined = undefined; +export let mode: "static" | "interactive"; +``` + +* `elem_id` and `elem_classes` allow Gradio app developers to target your component with custom CSS and JavaScript from the Python `Blocks` class. + +* `scale` and `min_width` allow Gradio app developers to control how much space your component takes up in the UI. + +* `loading_status` is used to display a loading status over the component when it is the output of an event. + +* `mode` is how the parent Gradio app tells your component whether the `interactive` or `static` version should be displayed. + +* `gradio`: The `gradio` object is created by the parent Gradio app. It stores some application-level configuration that will be useful in your component, like internationalization. You must use it to dispatch events from your component. + +A minimal `Index.svelte` file would look like: + +```svelte + + + + {#if loading_status} + + {/if} +

{value}

+
+``` + +## The Example.svelte file + +The `Example.svelte` file should expose the following props: + +```typescript + export let value: string; + export let type: "gallery" | "table"; + export let selected = false; + export let index: number; +``` + +* `value`: The example value that should be displayed. + +* `type`: This is a variable that can be either `"gallery"` or `"table"` depending on how the examples are displayed. The `"gallery"` form is used when the examples correspond to a single input component, while the `"table"` form is used when a user has multiple input components, and the examples need to populate all of them. + +* `selected`: You can also adjust how the examples are displayed if a user "selects" a particular example by using the selected variable. + +* `index`: The current index of the selected value. + +* Any additional props your "non-example" component takes! + +This is the `Example.svelte` file for the code `Radio` component: + +```svelte + + +
+ {value} +
+ + +``` + +## Handling Files + +If your component deals with files, these files **should** be uploaded to the backend server. +The `@gradio/client` npm package provides the `upload` and `prepare_files` utility functions to help you do this. + +The `prepare_files` function will convert the browser's `File` datatype to gradio's internal `FileData` type. +You should use the `FileData` data in your component to keep track of uploaded files. + +The `upload` function will upload an array of `FileData` values to the server. + +Here's an example of loading files from an `` element when its value changes. + + +```svelte + + + +``` + +The component exposes a prop named `root`. +This is passed down by the parent gradio app and it represents the base url that the files will be uploaded to and fetched from. + +For WASM support, you should get the upload function from the `Context` and pass that as the third parameter of the `upload` function. + +```typescript + +``` + +## Leveraging Existing Gradio Components + +Most of Gradio's frontend components are published on [npm](https://www.npmjs.com/), the javascript package repository. +This means that you can use them to save yourself time while incorporating common patterns in your component, like uploading files. +For example, the `@gradio/upload` package has `Upload` and `ModifyUpload` components for properly uploading files to the Gradio server. +Here is how you can use them to create a user interface to upload and display PDF files. + +```svelte + + + +{#if value === null && interactive} + + + +{:else if value !== null} + {#if interactive} + + {/if} + +{:else} + +{/if} +``` + +You can also combine existing Gradio components to create entirely unique experiences. +Like rendering a gallery of chatbot conversations. +The possibilities are endless, please read the documentation on our javascript packages [here](https://gradio.app/main/docs/js). +We'll be adding more packages and documentation over the coming weeks! + +## Matching Gradio Core's Design System + +You can explore our component library via Storybook. You'll be able to interact with our components and see them in their various states. + +For those interested in design customization, we provide the CSS variables consisting of our color palette, radii, spacing, and the icons we use - so you can easily match up your custom component with the style of our core components. This Storybook will be regularly updated with any new additions or changes. + +[Storybook Link](https://gradio.app/main/docs/js/storybook) + +## Custom configuration + +If you want to make use of the vast vite ecosystem, you can use the `gradio.config.js` file to configure your component's build process. This allows you to make use of tools like tailwindcss, mdsvex, and more. + +Currently, it is possible to configure the following: + +Vite options: +- `plugins`: A list of vite plugins to use. + +Svelte options: +- `preprocess`: A list of svelte preprocessors to use. +- `extensions`: A list of file extensions to compile to `.svelte` files. +- `build.target`: The target to build for, this may be necessary to support newer javascript features. See the [esbuild docs](https://esbuild.github.io/api/#target) for more information. + +The `gradio.config.js` file should be placed in the root of your component's `frontend` directory. A default config file is created for you when you create a new component. But you can also create your own config file, if one doesn't exist, and use it to customize your component's build process. + +### Example for a Vite plugin + +Custom components can use Vite plugins to customize the build process. Check out the [Vite Docs](https://vitejs.dev/guide/using-plugins.html) for more information. + +Here we configure [TailwindCSS](https://tailwindcss.com), a utility-first CSS framework. Setup is easiest using the version 4 prerelease. + +``` +npm install tailwindcss@next @tailwindcss/vite@next +``` + +In `gradio.config.js`: + +```typescript +import tailwindcss from "@tailwindcss/vite"; +export default { + plugins: [tailwindcss()] +}; +``` + +Then create a `style.css` file with the following content: + +```css +@import "tailwindcss"; +``` + +Import this file into `Index.svelte`. Note, that you need to import the css file containing `@import` and cannot just use a ` +``` + +Now import `PdfUploadText.svelte` in your ` + +
+ +
+ + +``` + + +Tip: Exercise for the reader - reduce the code duplication between `Index.svelte` and `Example.svelte` 😊 + + +You will not be able to render examples until we make some changes to the backend code in the next step! + +## Step 9: The backend + +The backend changes needed are smaller. +We're almost done! + +What we're going to do is: +* Add `change` and `upload` events to our component. +* Add a `height` property to let users control the height of the PDF. +* Set the `data_model` of our component to be `FileData`. This is so that Gradio can automatically cache and safely serve any files that are processed by our component. +* Modify the `preprocess` method to return a string corresponding to the path of our uploaded PDF. +* Modify the `postprocess` to turn a path to a PDF created in an event handler to a `FileData`. + +When all is said an done, your component's backend code should look like this: + +```python +from __future__ import annotations +from typing import Any, Callable, TYPE_CHECKING + +from gradio.components.base import Component +from gradio.data_classes import FileData +from gradio import processing_utils +if TYPE_CHECKING: + from gradio.components import Timer + +class PDF(Component): + + EVENTS = ["change", "upload"] + + data_model = FileData + + def __init__(self, value: Any = None, *, + height: int | None = None, + label: str | I18nData | None = None, + info: str | I18nData | None = None, + show_label: bool | None = None, + container: bool = True, + scale: int | None = None, + min_width: int | None = None, + interactive: bool | None = None, + visible: bool = True, + elem_id: str | None = None, + elem_classes: list[str] | str | None = None, + render: bool = True, + load_fn: Callable[..., Any] | None = None, + every: Timer | float | None = None): + super().__init__(value, label=label, info=info, + show_label=show_label, container=container, + scale=scale, min_width=min_width, + interactive=interactive, visible=visible, + elem_id=elem_id, elem_classes=elem_classes, + render=render, load_fn=load_fn, every=every) + self.height = height + + def preprocess(self, payload: FileData) -> str: + return payload.path + + def postprocess(self, value: str | None) -> FileData: + if not value: + return None + return FileData(path=value) + + def example_payload(self): + return "https://gradio-builds.s3.amazonaws.com/assets/pdf-guide/fw9.pdf" + + def example_value(self): + return "https://gradio-builds.s3.amazonaws.com/assets/pdf-guide/fw9.pdf" +``` + +## Step 10: Add a demo and publish! + +To test our backend code, let's add a more complex demo that performs Document Question and Answering with huggingface transformers. + +In our `demo` directory, create a `requirements.txt` file with the following packages + +``` +torch +transformers +pdf2image +pytesseract +``` + + +Tip: Remember to install these yourself and restart the dev server! You may need to install extra non-python dependencies for `pdf2image`. See [here](https://pypi.org/project/pdf2image/). Feel free to write your own demo if you have trouble. + + +```python +import gradio as gr +from gradio_pdf import PDF +from pdf2image import convert_from_path +from transformers import pipeline +from pathlib import Path + +dir_ = Path(__file__).parent + +p = pipeline( + "document-question-answering", + model="impira/layoutlm-document-qa", +) + +def qa(question: str, doc: str) -> str: + img = convert_from_path(doc)[0] + output = p(img, question) + return sorted(output, key=lambda x: x["score"], reverse=True)[0]['answer'] + + +demo = gr.Interface( + qa, + [gr.Textbox(label="Question"), PDF(label="Document")], + gr.Textbox(), +) + +demo.launch() +``` + +See our demo in action below! + + + +Finally lets build our component with `gradio cc build` and publish it with the `gradio cc publish` command! +This will guide you through the process of uploading your component to [PyPi](https://pypi.org/) and [HuggingFace Spaces](https://huggingface.co/spaces). + + +Tip: You may need to add the following lines to the `Dockerfile` of your HuggingFace Space. + +```Dockerfile +RUN mkdir -p /tmp/cache/ +RUN chmod a+rwx -R /tmp/cache/ +RUN apt-get update && apt-get install -y poppler-utils tesseract-ocr + +ENV TRANSFORMERS_CACHE=/tmp/cache/ +``` + +## Conclusion + +In order to use our new component in **any** gradio 4.0 app, simply install it with pip, e.g. `pip install gradio-pdf`. Then you can use it like the built-in `gr.File()` component (except that it will only accept and display PDF files). + +Here is a simple demo with the Blocks api: + +```python +import gradio as gr +from gradio_pdf import PDF + +with gr.Blocks() as demo: + pdf = PDF(label="Upload a PDF", interactive=True) + name = gr.Textbox() + pdf.upload(lambda f: f, pdf, name) + +demo.launch() +``` + + +I hope you enjoyed this tutorial! +The complete source code for our component is [here](https://huggingface.co/spaces/freddyaboulton/gradio_pdf/tree/main/src). +Please don't hesitate to reach out to the gradio community on the [HuggingFace Discord](https://discord.gg/hugging-face-879548962464493619) if you get stuck. diff --git a/guides/08_custom-components/08_multimodal-chatbot-part1.md b/guides/08_custom-components/08_multimodal-chatbot-part1.md new file mode 100644 index 0000000..a5dd23f --- /dev/null +++ b/guides/08_custom-components/08_multimodal-chatbot-part1.md @@ -0,0 +1,358 @@ +# Build a Custom Multimodal Chatbot - Part 1 + +This is the first in a two part series where we build a custom Multimodal Chatbot component. +In part 1, we will modify the Gradio Chatbot component to display text and media files (video, audio, image) in the same message. +In part 2, we will build a custom Textbox component that will be able to send multimodal messages (text and media files) to the chatbot. + +You can follow along with the author of this post as he implements the chatbot component in the following YouTube video! + + + +Here's a preview of what our multimodal chatbot component will look like: + +![MultiModal Chatbot](https://gradio-builds.s3.amazonaws.com/assets/MultimodalChatbot.png) + + +## Part 1 - Creating our project + +For this demo we will be tweaking the existing Gradio `Chatbot` component to display text and media files in the same message. +Let's create a new custom component directory by templating off of the `Chatbot` component source code. + +```bash +gradio cc create MultimodalChatbot --template Chatbot +``` + +And we're ready to go! + +Tip: Make sure to modify the `Author` key in the `pyproject.toml` file. + +## Part 2a - The backend data_model + +Open up the `multimodalchatbot.py` file in your favorite code editor and let's get started modifying the backend of our component. + +The first thing we will do is create the `data_model` of our component. +The `data_model` is the data format that your python component will receive and send to the javascript client running the UI. +You can read more about the `data_model` in the [backend guide](./backend). + +For our component, each chatbot message will consist of two keys: a `text` key that displays the text message and an optional list of media files that can be displayed underneath the text. + +Import the `FileData` and `GradioModel` classes from `gradio.data_classes` and modify the existing `ChatbotData` class to look like the following: + +```python +class FileMessage(GradioModel): + file: FileData + alt_text: Optional[str] = None + + +class MultimodalMessage(GradioModel): + text: Optional[str] = None + files: Optional[List[FileMessage]] = None + + +class ChatbotData(GradioRootModel): + root: List[Tuple[Optional[MultimodalMessage], Optional[MultimodalMessage]]] + + +class MultimodalChatbot(Component): + ... + data_model = ChatbotData +``` + + +Tip: The `data_model`s are implemented using `Pydantic V2`. Read the documentation [here](https://docs.pydantic.dev/latest/). + +We've done the hardest part already! + +## Part 2b - The pre and postprocess methods + +For the `preprocess` method, we will keep it simple and pass a list of `MultimodalMessage`s to the python functions that use this component as input. +This will let users of our component access the chatbot data with `.text` and `.files` attributes. +This is a design choice that you can modify in your implementation! +We can return the list of messages with the `root` property of the `ChatbotData` like so: + +```python +def preprocess( + self, + payload: ChatbotData | None, +) -> List[MultimodalMessage] | None: + if payload is None: + return payload + return payload.root +``` + + +Tip: Learn about the reasoning behind the `preprocess` and `postprocess` methods in the [key concepts guide](./key-component-concepts) + +In the `postprocess` method we will coerce each message returned by the python function to be a `MultimodalMessage` class. +We will also clean up any indentation in the `text` field so that it can be properly displayed as markdown in the frontend. + +We can leave the `postprocess` method as is and modify the `_postprocess_chat_messages` + +```python +def _postprocess_chat_messages( + self, chat_message: MultimodalMessage | dict | None +) -> MultimodalMessage | None: + if chat_message is None: + return None + if isinstance(chat_message, dict): + chat_message = MultimodalMessage(**chat_message) + chat_message.text = inspect.cleandoc(chat_message.text or "") + for file_ in chat_message.files: + file_.file.mime_type = client_utils.get_mimetype(file_.file.path) + return chat_message +``` + +Before we wrap up with the backend code, let's modify the `example_value` and `example_payload` method to return a valid dictionary representation of the `ChatbotData`: + +```python +def example_value(self) -> Any: + return [[{"text": "Hello!", "files": []}, None]] + +def example_payload(self) -> Any: + return [[{"text": "Hello!", "files": []}, None]] +``` + +Congrats - the backend is complete! + +## Part 3a - The Index.svelte file + +The frontend for the `Chatbot` component is divided into two parts - the `Index.svelte` file and the `shared/Chatbot.svelte` file. +The `Index.svelte` file applies some processing to the data received from the server and then delegates the rendering of the conversation to the `shared/Chatbot.svelte` file. +First we will modify the `Index.svelte` file to apply processing to the new data type the backend will return. + +Let's begin by porting our custom types from our python `data_model` to typescript. +Open `frontend/shared/utils.ts` and add the following type definitions at the top of the file: + +```ts +export type FileMessage = { + file: FileData; + alt_text?: string; +}; + + +export type MultimodalMessage = { + text: string; + files?: FileMessage[]; +} +``` + +Now let's import them in `Index.svelte` and modify the type annotations for `value` and `_value`. + +```ts +import type { FileMessage, MultimodalMessage } from "./shared/utils"; + +export let value: [ + MultimodalMessage | null, + MultimodalMessage | null +][] = []; + +let _value: [ + MultimodalMessage | null, + MultimodalMessage | null +][]; +``` + +We need to normalize each message to make sure each file has a proper URL to fetch its contents from. +We also need to format any embedded file links in the `text` key. +Let's add a `process_message` utility function and apply it whenever the `value` changes. + +```ts +function process_message(msg: MultimodalMessage | null): MultimodalMessage | null { + if (msg === null) { + return msg; + } + msg.text = redirect_src_url(msg.text); + msg.files = msg.files.map(normalize_messages); + return msg; +} + +$: _value = value + ? value.map(([user_msg, bot_msg]) => [ + process_message(user_msg), + process_message(bot_msg) + ]) + : []; +``` + +## Part 3b - the Chatbot.svelte file + +Let's begin similarly to the `Index.svelte` file and let's first modify the type annotations. +Import `Mulimodal` message at the top of the ` +``` + +Be sure to add this to the `` of your HTML. This will install the latest version but we advise hardcoding the version in production. You can find all available versions [here](https://www.jsdelivr.com/package/npm/@gradio/client). This approach is ideal for experimental or prototying purposes, though has some limitations. A complete example would look like this: + +```html + + + + + + +``` + +## Connecting to a running Gradio App + +Start by connecting instantiating a `client` instance and connecting it to a Gradio app that is running on Hugging Face Spaces or generally anywhere on the web. + +## Connecting to a Hugging Face Space + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("abidlabs/en2fr"); // a Space that translates from English to French +``` + +You can also connect to private Spaces by passing in your HF token with the `token` property of the options parameter. You can get your HF token here: https://huggingface.co/settings/tokens + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("abidlabs/my-private-space", { token: "hf_..." }) +``` + +## Duplicating a Space for private use + +While you can use any public Space as an API, you may get rate limited by Hugging Face if you make too many requests. For unlimited usage of a Space, simply duplicate the Space to create a private Space, and then use it to make as many requests as you'd like! You'll need to pass in your [Hugging Face token](https://huggingface.co/settings/tokens)). + +`Client.duplicate` is almost identical to `Client.connect`, the only difference is under the hood: + +```js +import { Client, handle_file } from "@gradio/client"; + +const response = await fetch( + "https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3" +); +const audio_file = await response.blob(); + +const app = await Client.duplicate("abidlabs/whisper", { token: "hf_..." }); +const transcription = await app.predict("/predict", [handle_file(audio_file)]); +``` + +If you have previously duplicated a Space, re-running `Client.duplicate` will _not_ create a new Space. Instead, the client will attach to the previously-created Space. So it is safe to re-run the `Client.duplicate` method multiple times with the same space. + +**Note:** if the original Space uses GPUs, your private Space will as well, and your Hugging Face account will get billed based on the price of the GPU. To minimize charges, your Space will automatically go to sleep after 5 minutes of inactivity. You can also set the hardware using the `hardware` and `timeout` properties of `duplicate`'s options object like this: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.duplicate("abidlabs/whisper", { + token: "hf_...", + timeout: 60, + hardware: "a10g-small" +}); +``` + +## Connecting a general Gradio app + +If your app is running somewhere else, just provide the full URL instead, including the "http://" or "https://". Here's an example of making predictions to a Gradio app that is running on a share URL: + +```js +import { Client } from "@gradio/client"; + +const app = Client.connect("https://bec81a83-5b5c-471e.gradio.live"); +``` + +## Connecting to a Gradio app with auth + +If the Gradio application you are connecting to [requires a username and password](/guides/sharing-your-app#authentication), then provide them as a tuple to the `auth` argument of the `Client` class: + +```js +import { Client } from "@gradio/client"; + +Client.connect( + space_name, + { auth: [username, password] } +) +``` + + +## Inspecting the API endpoints + +Once you have connected to a Gradio app, you can view the APIs that are available to you by calling the `Client`'s `view_api` method. + +For the Whisper Space, we can do this: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("abidlabs/whisper"); + +const app_info = await app.view_api(); + +console.log(app_info); +``` + +And we will see the following: + +```json +{ + "named_endpoints": { + "/predict": { + "parameters": [ + { + "label": "text", + "component": "Textbox", + "type": "string" + } + ], + "returns": [ + { + "label": "output", + "component": "Textbox", + "type": "string" + } + ] + } + }, + "unnamed_endpoints": {} +} +``` + +This shows us that we have 1 API endpoint in this space, and shows us how to use the API endpoint to make a prediction: we should call the `.predict()` method (which we will explore below), providing a parameter `input_audio` of type `string`, which is a url to a file. + +We should also provide the `api_name='/predict'` argument to the `predict()` method. Although this isn't necessary if a Gradio app has only 1 named endpoint, it does allow us to call different endpoints in a single app if they are available. If an app has unnamed API endpoints, these can also be displayed by running `.view_api(all_endpoints=True)`. + +## The "View API" Page + +As an alternative to running the `.view_api()` method, you can click on the "Use via API" link in the footer of the Gradio app, which shows us the same information, along with example usage. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/view-api.png) + +The View API page also includes an "API Recorder" that lets you interact with the Gradio UI normally and converts your interactions into the corresponding code to run with the JS Client. + + +## Making a prediction + +The simplest way to make a prediction is simply to call the `.predict()` method with the appropriate arguments: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("abidlabs/en2fr"); +const result = await app.predict("/predict", ["Hello"]); +``` + +If there are multiple parameters, then you should pass them as an array to `.predict()`, like this: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("gradio/calculator"); +const result = await app.predict("/predict", [4, "add", 5]); +``` + +For certain inputs, such as images, you should pass in a `Buffer`, `Blob` or `File` depending on what is most convenient. In node, this would be a `Buffer` or `Blob`; in a browser environment, this would be a `Blob` or `File`. + +```js +import { Client, handle_file } from "@gradio/client"; + +const response = await fetch( + "https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3" +); +const audio_file = await response.blob(); + +const app = await Client.connect("abidlabs/whisper"); +const result = await app.predict("/predict", [handle_file(audio_file)]); +``` + +## Using events + +If the API you are working with can return results over time, or you wish to access information about the status of a job, you can use the iterable interface for more flexibility. This is especially useful for iterative endpoints or generator endpoints that will produce a series of values over time as discrete responses. + +```js +import { Client } from "@gradio/client"; + +function log_result(payload) { + const { + data: [translation] + } = payload; + + console.log(`The translated result is: ${translation}`); +} + +const app = await Client.connect("abidlabs/en2fr"); +const job = app.submit("/predict", ["Hello"]); + +for await (const message of job) { + log_result(message); +} +``` + +## Status + +The event interface also allows you to get the status of the running job by instantiating the client with the `events` options passing `status` and `data` as an array: + + +```ts +import { Client } from "@gradio/client"; + +const app = await Client.connect("abidlabs/en2fr", { + events: ["status", "data"] +}); +``` + +This ensures that status messages are also reported to the client. + +`status`es are returned as an object with the following attributes: `status` (a human readbale status of the current job, `"pending" | "generating" | "complete" | "error"`), `code` (the detailed gradio code for the job), `position` (the current position of this job in the queue), `queue_size` (the total queue size), `eta` (estimated time this job will complete), `success` (a boolean representing whether the job completed successfully), and `time` ( as `Date` object detailing the time that the status was generated). + +```js +import { Client } from "@gradio/client"; + +function log_status(status) { + console.log( + `The current status for this job is: ${JSON.stringify(status, null, 2)}.` + ); +} + +const app = await Client.connect("abidlabs/en2fr", { + events: ["status", "data"] +}); +const job = app.submit("/predict", ["Hello"]); + +for await (const message of job) { + if (message.type === "status") { + log_status(message); + } +} +``` + +## Cancelling Jobs + +The job instance also has a `.cancel()` method that cancels jobs that have been queued but not started. For example, if you run: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("abidlabs/en2fr"); +const job_one = app.submit("/predict", ["Hello"]); +const job_two = app.submit("/predict", ["Friends"]); + +job_one.cancel(); +job_two.cancel(); +``` + +If the first job has started processing, then it will not be canceled but the client will no longer listen for updates (throwing away the job). If the second job has not yet started, it will be successfully canceled and removed from the queue. + +## Generator Endpoints + +Some Gradio API endpoints do not return a single value, rather they return a series of values. You can listen for these values in real time using the iterable interface: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("gradio/count_generator"); +const job = app.submit(0, [9]); + +for await (const message of job) { + console.log(message.data); +} +``` + +This will log out the values as they are generated by the endpoint. + +You can also cancel jobs that that have iterative outputs, in which case the job will finish immediately. + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("gradio/count_generator"); +const job = app.submit(0, [9]); + +for await (const message of job) { + console.log(message.data); +} + +setTimeout(() => { + job.cancel(); +}, 3000); +``` diff --git a/guides/09_gradio-clients-and-lite/03_querying-gradio-apps-with-curl.md b/guides/09_gradio-clients-and-lite/03_querying-gradio-apps-with-curl.md new file mode 100644 index 0000000..e7fdc07 --- /dev/null +++ b/guides/09_gradio-clients-and-lite/03_querying-gradio-apps-with-curl.md @@ -0,0 +1,303 @@ +# Querying Gradio Apps with Curl + +Tags: CURL, API, SPACES + +It is possible to use any Gradio app as an API using cURL, the command-line tool that is pre-installed on many operating systems. This is particularly useful if you are trying to query a Gradio app from an environment other than Python or Javascript (since specialized Gradio clients exist for both [Python](/guides/getting-started-with-the-python-client) and [Javascript](/guides/getting-started-with-the-js-client)). + +As an example, consider this Gradio demo that translates text from English to French: https://abidlabs-en2fr.hf.space/. + +Using `curl`, we can translate text programmatically. + +Here's the code to do it: + +```bash +$ curl -X POST https://abidlabs-en2fr.hf.space/call/predict -H "Content-Type: application/json" -d '{ + "data": ["Hello, my friend."] +}' + +>> {"event_id": $EVENT_ID} +``` + +```bash +$ curl -N https://abidlabs-en2fr.hf.space/call/predict/$EVENT_ID + +>> event: complete +>> data: ["Bonjour, mon ami."] +``` + + +Note: making a prediction and getting a result requires two `curl` requests: a `POST` and a `GET`. The `POST` request returns an `EVENT_ID` and prints it to the console, which is used in the second `GET` request to fetch the results. You can combine these into a single command using `awk` and `read` to parse the results of the first command and pipe into the second, like this: + +```bash +$ curl -X POST https://abidlabs-en2fr.hf.space/call/predict -H "Content-Type: application/json" -d '{ + "data": ["Hello, my friend."] +}' \ + | awk -F'"' '{ print $4}' \ + | read EVENT_ID; curl -N https://abidlabs-en2fr.hf.space/call/predict/$EVENT_ID + +>> event: complete +>> data: ["Bonjour, mon ami."] +``` + +In the rest of this Guide, we'll explain these two steps in more detail and provide additional examples of querying Gradio apps with `curl`. + + +**Prerequisites**: For this Guide, you do _not_ need to know how to build Gradio apps in great detail. However, it is helpful to have general familiarity with Gradio's concepts of input and output components. + +## Installation + +You generally don't need to install cURL, as it comes pre-installed on many operating systems. Run: + +```bash +curl --version +``` + +to confirm that `curl` is installed. If it is not already installed, you can install it by visiting https://curl.se/download.html. + + +## Step 0: Get the URL for your Gradio App + +To query a Gradio app, you'll need its full URL. This is usually just the URL that the Gradio app is hosted on, for example: https://bec81a83-5b5c-471e.gradio.live + + +**Hugging Face Spaces** + +However, if you are querying a Gradio on Hugging Face Spaces, you will need to use the URL of the embedded Gradio app, not the URL of the Space webpage. For example: + +```bash +❌ Space URL: https://huggingface.co/spaces/abidlabs/en2fr +✅ Gradio app URL: https://abidlabs-en2fr.hf.space/ +``` + +You can get the Gradio app URL by clicking the "view API" link at the bottom of the page. Or, you can right-click on the page and then click on "View Frame Source" or the equivalent in your browser to view the URL of the embedded Gradio app. + +While you can use any public Space as an API, you may get rate limited by Hugging Face if you make too many requests. For unlimited usage of a Space, simply duplicate the Space to create a private Space, +and then use it to make as many requests as you'd like! + +Note: to query private Spaces, you will need to pass in your Hugging Face (HF) token. You can get your HF token here: https://huggingface.co/settings/tokens. In this case, you will need to include an additional header in both of your `curl` calls that we'll discuss below: + +```bash +-H "Authorization: Bearer $HF_TOKEN" +``` + +Now, we are ready to make the two `curl` requests. + +## Step 1: Make a Prediction (POST) + +The first of the two `curl` requests is `POST` request that submits the input payload to the Gradio app. + +The syntax of the `POST` request is as follows: + +```bash +$ curl -X POST $URL/call/$API_NAME -H "Content-Type: application/json" -d '{ + "data": $PAYLOAD +}' +``` + +Here: + +* `$URL` is the URL of the Gradio app as obtained in Step 0 +* `$API_NAME` is the name of the API endpoint for the event that you are running. You can get the API endpoint names by clicking the "view API" link at the bottom of the page. +* `$PAYLOAD` is a valid JSON data list containing the input payload, one element for each input component. + +When you make this `POST` request successfully, you will get an event id that is printed to the terminal in this format: + +```bash +>> {"event_id": $EVENT_ID} +``` + +This `EVENT_ID` will be needed in the subsequent `curl` request to fetch the results of the prediction. + +Here are some examples of how to make the `POST` request + +**Basic Example** + +Revisiting the example at the beginning of the page, here is how to make the `POST` request for a simple Gradio application that takes in a single input text component: + +```bash +$ curl -X POST https://abidlabs-en2fr.hf.space/call/predict -H "Content-Type: application/json" -d '{ + "data": ["Hello, my friend."] +}' +``` + +**Multiple Input Components** + +This [Gradio demo](https://huggingface.co/spaces/gradio/hello_world_3) accepts three inputs: a string corresponding to the `gr.Textbox`, a boolean value corresponding to the `gr.Checkbox`, and a numerical value corresponding to the `gr.Slider`. Here is the `POST` request: + +```bash +curl -X POST https://gradio-hello-world-3.hf.space/call/predict -H "Content-Type: application/json" -d '{ + "data": ["Hello", true, 5] +}' +``` + +**Private Spaces** + +As mentioned earlier, if you are making a request to a private Space, you will need to pass in a [Hugging Face token](https://huggingface.co/settings/tokens) that has read access to the Space. The request will look like this: + +```bash +$ curl -X POST https://private-space.hf.space/call/predict -H "Content-Type: application/json" -H "Authorization: Bearer $HF_TOKEN" -d '{ + "data": ["Hello, my friend."] +}' +``` + +**Files** + +If you are using `curl` to query a Gradio application that requires file inputs, the files *need* to be provided as URLs, and The URL needs to be enclosed in a dictionary in this format: + +```bash +{"path": $URL} +``` + +Here is an example `POST` request: + +```bash +$ curl -X POST https://gradio-image-mod.hf.space/call/predict -H "Content-Type: application/json" -d '{ + "data": [{"path": "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png"}] +}' +``` + + +**Stateful Demos** + +If your Gradio demo [persists user state](/guides/interface-state) across multiple interactions (e.g. is a chatbot), you can pass in a `session_hash` alongside the `data`. Requests with the same `session_hash` are assumed to be part of the same user session. Here's how that might look: + +```bash +# These two requests will share a session + +curl -X POST https://gradio-chatinterface-random-response.hf.space/call/chat -H "Content-Type: application/json" -d '{ + "data": ["Are you sentient?"], + "session_hash": "randomsequence1234" +}' + +curl -X POST https://gradio-chatinterface-random-response.hf.space/call/chat -H "Content-Type: application/json" -d '{ + "data": ["Really?"], + "session_hash": "randomsequence1234" +}' + +# This request will be treated as a new session + +curl -X POST https://gradio-chatinterface-random-response.hf.space/call/chat -H "Content-Type: application/json" -d '{ + "data": ["Are you sentient?"], + "session_hash": "newsequence5678" +}' +``` + + + +## Step 2: GET the result + +Once you have received the `EVENT_ID` corresponding to your prediction, you can stream the results. Gradio stores these results in a least-recently-used cache in the Gradio app. By default, the cache can store 2,000 results (across all users and endpoints of the app). + +To stream the results for your prediction, make a `GET` request with the following syntax: + +```bash +$ curl -N $URL/call/$API_NAME/$EVENT_ID +``` + + +Tip: If you are fetching results from a private Space, include a header with your HF token like this: `-H "Authorization: Bearer $HF_TOKEN"` in the `GET` request. + +This should produce a stream of responses in this format: + +```bash +event: ... +data: ... +event: ... +data: ... +... +``` + +Here: `event` can be one of the following: +* `generating`: indicating an intermediate result +* `complete`: indicating that the prediction is complete and the final result +* `error`: indicating that the prediction was not completed successfully +* `heartbeat`: sent every 15 seconds to keep the request alive + +The `data` is in the same format as the input payload: valid JSON data list containing the output result, one element for each output component. + +Here are some examples of what results you should expect if a request is completed successfully: + +**Basic Example** + +Revisiting the example at the beginning of the page, we would expect the result to look like this: + +```bash +event: complete +data: ["Bonjour, mon ami."] +``` + +**Multiple Outputs** + +If your endpoint returns multiple values, they will appear as elements of the `data` list: + +```bash +event: complete +data: ["Good morning Hello. It is 5 degrees today", -15.0] +``` + +**Streaming Example** + +If your Gradio app [streams a sequence of values](/guides/streaming-outputs), then they will be streamed directly to your terminal, like this: + +```bash +event: generating +data: ["Hello, w!"] +event: generating +data: ["Hello, wo!"] +event: generating +data: ["Hello, wor!"] +event: generating +data: ["Hello, worl!"] +event: generating +data: ["Hello, world!"] +event: complete +data: ["Hello, world!"] +``` + +**File Example** + +If your Gradio app returns a file, the file will be represented as a dictionary in this format (including potentially some additional keys): + +```python +{ + "orig_name": "example.jpg", + "path": "/path/in/server.jpg", + "url": "https:/example.com/example.jpg", + "meta": {"_type": "gradio.FileData"} +} +``` + +In your terminal, it may appear like this: + +```bash +event: complete +data: [{"path": "/tmp/gradio/359933dc8d6cfe1b022f35e2c639e6e42c97a003/image.webp", "url": "https://gradio-image-mod.hf.space/c/file=/tmp/gradio/359933dc8d6cfe1b022f35e2c639e6e42c97a003/image.webp", "size": null, "orig_name": "image.webp", "mime_type": null, "is_stream": false, "meta": {"_type": "gradio.FileData"}}] +``` + +## Authentication + +What if your Gradio application has [authentication enabled](/guides/sharing-your-app#authentication)? In that case, you'll need to make an additional `POST` request with cURL to authenticate yourself before you make any queries. Here are the complete steps: + +First, login with a `POST` request supplying a valid username and password: + +```bash +curl -X POST $URL/login \ + -d "username=$USERNAME&password=$PASSWORD" \ + -c cookies.txt +``` + +If the credentials are correct, you'll get `{"success":true}` in response and the cookies will be saved in `cookies.txt`. + +Next, you'll need to include these cookies when you make the original `POST` request, like this: + +```bash +$ curl -X POST $URL/call/$API_NAME -b cookies.txt -H "Content-Type: application/json" -d '{ + "data": $PAYLOAD +}' +``` + +Finally, you'll need to `GET` the results, again supplying the cookies from the file: + +```bash +curl -N $URL/call/$API_NAME/$EVENT_ID -b cookies.txt +``` diff --git a/guides/09_gradio-clients-and-lite/04_gradio-and-llm-agents.md b/guides/09_gradio-clients-and-lite/04_gradio-and-llm-agents.md new file mode 100644 index 0000000..e79639f --- /dev/null +++ b/guides/09_gradio-clients-and-lite/04_gradio-and-llm-agents.md @@ -0,0 +1,139 @@ +# Gradio & LLM Agents 🤝 + +Large Language Models (LLMs) are very impressive but they can be made even more powerful if we could give them skills to accomplish specialized tasks. + +The [gradio_tools](https://github.com/freddyaboulton/gradio-tools) library can turn any [Gradio](https://github.com/gradio-app/gradio) application into a [tool](https://python.langchain.com/en/latest/modules/agents/tools.html) that an [agent](https://docs.langchain.com/docs/components/agents/agent) can use to complete its task. For example, an LLM could use a Gradio tool to transcribe a voice recording it finds online and then summarize it for you. Or it could use a different Gradio tool to apply OCR to a document on your Google Drive and then answer questions about it. + +This guide will show how you can use `gradio_tools` to grant your LLM Agent access to the cutting edge Gradio applications hosted in the world. Although `gradio_tools` are compatible with more than one agent framework, we will focus on [Langchain Agents](https://docs.langchain.com/docs/components/agents/) in this guide. + +## Some background + +### What are agents? + +A [LangChain agent](https://docs.langchain.com/docs/components/agents/agent) is a Large Language Model (LLM) that takes user input and reports an output based on using one of many tools at its disposal. + +### What is Gradio? + +[Gradio](https://github.com/gradio-app/gradio) is the defacto standard framework for building Machine Learning Web Applications and sharing them with the world - all with just python! 🐍 + +## gradio_tools - An end-to-end example + +To get started with `gradio_tools`, all you need to do is import and initialize your tools and pass them to the langchain agent! + +In the following example, we import the `StableDiffusionPromptGeneratorTool` to create a good prompt for stable diffusion, the +`StableDiffusionTool` to create an image with our improved prompt, the `ImageCaptioningTool` to caption the generated image, and +the `TextToVideoTool` to create a video from a prompt. + +We then tell our agent to create an image of a dog riding a skateboard, but to please improve our prompt ahead of time. We also ask +it to caption the generated image and create a video for it. The agent can decide which tool to use without us explicitly telling it. + +```python +import os + +if not os.getenv("OPENAI_API_KEY"): + raise ValueError("OPENAI_API_KEY must be set") + +from langchain.agents import initialize_agent +from langchain.llms import OpenAI +from gradio_tools import (StableDiffusionTool, ImageCaptioningTool, StableDiffusionPromptGeneratorTool, + TextToVideoTool) + +from langchain.memory import ConversationBufferMemory + +llm = OpenAI(temperature=0) +memory = ConversationBufferMemory(memory_key="chat_history") +tools = [StableDiffusionTool().langchain, ImageCaptioningTool().langchain, + StableDiffusionPromptGeneratorTool().langchain, TextToVideoTool().langchain] + + +agent = initialize_agent(tools, llm, memory=memory, agent="conversational-react-description", verbose=True) +output = agent.run(input=("Please create a photo of a dog riding a skateboard " + "but improve my prompt prior to using an image generator." + "Please caption the generated image and create a video for it using the improved prompt.")) +``` + +You'll note that we are using some pre-built tools that come with `gradio_tools`. Please see this [doc](https://github.com/freddyaboulton/gradio-tools#gradio-tools-gradio--llm-agents) for a complete list of the tools that come with `gradio_tools`. +If you would like to use a tool that's not currently in `gradio_tools`, it is very easy to add your own. That's what the next section will cover. + +## gradio_tools - creating your own tool + +The core abstraction is the `GradioTool`, which lets you define a new tool for your LLM as long as you implement a standard interface: + +```python +class GradioTool(BaseTool): + + def __init__(self, name: str, description: str, src: str) -> None: + + @abstractmethod + def create_job(self, query: str) -> Job: + pass + + @abstractmethod + def postprocess(self, output: Tuple[Any] | Any) -> str: + pass +``` + +The requirements are: + +1. The name for your tool +2. The description for your tool. This is crucial! Agents decide which tool to use based on their description. Be precise and be sure to include example of what the input and the output of the tool should look like. +3. The url or space id, e.g. `freddyaboulton/calculator`, of the Gradio application. Based on this value, `gradio_tool` will create a [gradio client](https://github.com/gradio-app/gradio/blob/main/client/python/README.md) instance to query the upstream application via API. Be sure to click the link and learn more about the gradio client library if you are not familiar with it. +4. create_job - Given a string, this method should parse that string and return a job from the client. Most times, this is as simple as passing the string to the `submit` function of the client. More info on creating jobs [here](https://github.com/gradio-app/gradio/blob/main/client/python/README.md#making-a-prediction) +5. postprocess - Given the result of the job, convert it to a string the LLM can display to the user. +6. _Optional_ - Some libraries, e.g. [MiniChain](https://github.com/srush/MiniChain/tree/main), may need some info about the underlying gradio input and output types used by the tool. By default, this will return gr.Textbox() but + if you'd like to provide more accurate info, implement the `_block_input(self, gr)` and `_block_output(self, gr)` methods of the tool. The `gr` variable is the gradio module (the result of `import gradio as gr`). It will be + automatically imported by the `GradiTool` parent class and passed to the `_block_input` and `_block_output` methods. + +And that's it! + +Once you have created your tool, open a pull request to the `gradio_tools` repo! We welcome all contributions. + +## Example tool - Stable Diffusion + +Here is the code for the StableDiffusion tool as an example: + +```python +from gradio_tool import GradioTool +import os + +class StableDiffusionTool(GradioTool): + """Tool for calling stable diffusion from llm""" + + def __init__( + self, + name="StableDiffusion", + description=( + "An image generator. Use this to generate images based on " + "text input. Input should be a description of what the image should " + "look like. The output will be a path to an image file." + ), + src="gradio-client-demos/stable-diffusion", + token=None, + ) -> None: + super().__init__(name, description, src, token) + + def create_job(self, query: str) -> Job: + return self.client.submit(query, "", 9, fn_index=1) + + def postprocess(self, output: str) -> str: + return [os.path.join(output, i) for i in os.listdir(output) if not i.endswith("json")][0] + + def _block_input(self, gr) -> "gr.components.Component": + return gr.Textbox() + + def _block_output(self, gr) -> "gr.components.Component": + return gr.Image() +``` + +Some notes on this implementation: + +1. All instances of `GradioTool` have an attribute called `client` that is a pointed to the underlying [gradio client](https://github.com/gradio-app/gradio/tree/main/client/python#gradio_client-use-a-gradio-app-as-an-api----in-3-lines-of-python). That is what you should use + in the `create_job` method. +2. `create_job` just passes the query string to the `submit` function of the client with some other parameters hardcoded, i.e. the negative prompt string and the guidance scale. We could modify our tool to also accept these values from the input string in a subsequent version. +3. The `postprocess` method simply returns the first image from the gallery of images created by the stable diffusion space. We use the `os` module to get the full path of the image. + +## Conclusion + +You now know how to extend the abilities of your LLM with the 1000s of gradio spaces running in the wild! +Again, we welcome any contributions to the [gradio_tools](https://github.com/freddyaboulton/gradio-tools) library. +We're excited to see the tools you all build! diff --git a/guides/09_gradio-clients-and-lite/07_fastapi-app-with-the-gradio-client.md b/guides/09_gradio-clients-and-lite/07_fastapi-app-with-the-gradio-client.md new file mode 100644 index 0000000..d5cb24f --- /dev/null +++ b/guides/09_gradio-clients-and-lite/07_fastapi-app-with-the-gradio-client.md @@ -0,0 +1,197 @@ +# Building a Web App with the Gradio Python Client + +Tags: CLIENT, API, WEB APP + +In this guide, we will demonstrate how to use the `gradio_client` [Python library](getting-started-with-the-python-client/), which enables developers to make requests to a Gradio app programmatically, by creating an end-to-end example web app using FastAPI. The web app we will be building is called "Acapellify," and it will allow users to upload video files as input and return a version of that video without instrumental music. It will also display a gallery of generated videos. + +**Prerequisites** + +Before we begin, make sure you are running Python 3.9 or later, and have the following libraries installed: + +- `gradio_client` +- `fastapi` +- `uvicorn` + +You can install these libraries from `pip`: + +```bash +$ pip install gradio_client fastapi uvicorn +``` + +You will also need to have ffmpeg installed. You can check to see if you already have ffmpeg by running in your terminal: + +```bash +$ ffmpeg version +``` + +Otherwise, install ffmpeg [by following these instructions](https://www.hostinger.com/tutorials/how-to-install-ffmpeg). + +## Step 1: Write the Video Processing Function + +Let's start with what seems like the most complex bit -- using machine learning to remove the music from a video. + +Luckily for us, there's an existing Space we can use to make this process easier: [https://huggingface.co/spaces/abidlabs/music-separation](https://huggingface.co/spaces/abidlabs/music-separation). This Space takes an audio file and produces two separate audio files: one with the instrumental music and one with all other sounds in the original clip. Perfect to use with our client! + +Open a new Python file, say `main.py`, and start by importing the `Client` class from `gradio_client` and connecting it to this Space: + +```py +from gradio_client import Client, handle_file + +client = Client("abidlabs/music-separation") + +def acapellify(audio_path): + result = client.predict(handle_file(audio_path), api_name="/predict") + return result[0] +``` + +That's all the code that's needed -- notice that the API endpoints returns two audio files (one without the music, and one with just the music) in a list, and so we just return the first element of the list. + +--- + +**Note**: since this is a public Space, there might be other users using this Space as well, which might result in a slow experience. You can duplicate this Space with your own [Hugging Face token](https://huggingface.co/settings/tokens) and create a private Space that only you have will have access to and bypass the queue. To do that, simply replace the first two lines above with: + +```py +from gradio_client import Client + +client = Client.duplicate("abidlabs/music-separation", token=YOUR_HF_TOKEN) +``` + +Everything else remains the same! + +--- + +Now, of course, we are working with video files, so we first need to extract the audio from the video files. For this, we will be using the `ffmpeg` library, which does a lot of heavy lifting when it comes to working with audio and video files. The most common way to use `ffmpeg` is through the command line, which we'll call via Python's `subprocess` module: + +Our video processing workflow will consist of three steps: + +1. First, we start by taking in a video filepath and extracting the audio using `ffmpeg`. +2. Then, we pass in the audio file through the `acapellify()` function above. +3. Finally, we combine the new audio with the original video to produce a final acapellified video. + +Here's the complete code in Python, which you can add to your `main.py` file: + +```python +import subprocess + +def process_video(video_path): + old_audio = os.path.basename(video_path).split(".")[0] + ".m4a" + subprocess.run(['ffmpeg', '-y', '-i', video_path, '-vn', '-acodec', 'copy', old_audio]) + + new_audio = acapellify(old_audio) + + new_video = f"acap_{video_path}" + subprocess.call(['ffmpeg', '-y', '-i', video_path, '-i', new_audio, '-map', '0:v', '-map', '1:a', '-c:v', 'copy', '-c:a', 'aac', '-strict', 'experimental', f"static/{new_video}"]) + return new_video +``` + +You can read up on [ffmpeg documentation](https://ffmpeg.org/ffmpeg.html) if you'd like to understand all of the command line parameters, as they are beyond the scope of this tutorial. + +## Step 2: Create a FastAPI app (Backend Routes) + +Next up, we'll create a simple FastAPI app. If you haven't used FastAPI before, check out [the great FastAPI docs](https://fastapi.tiangolo.com/). Otherwise, this basic template, which we add to `main.py`, will look pretty familiar: + +```python +import os +from fastapi import FastAPI, File, UploadFile, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +app = FastAPI() +os.makedirs("static", exist_ok=True) +app.mount("/static", StaticFiles(directory="static"), name="static") +templates = Jinja2Templates(directory="templates") + +videos = [] + +@app.get("/", response_class=HTMLResponse) +async def home(request: Request): + return templates.TemplateResponse( + "home.html", {"request": request, "videos": videos}) + +@app.post("/uploadvideo/") +async def upload_video(video: UploadFile = File(...)): + video_path = video.filename + with open(video_path, "wb+") as fp: + fp.write(video.file.read()) + + new_video = process_video(video.filename) + videos.append(new_video) + return RedirectResponse(url='/', status_code=303) +``` + +In this example, the FastAPI app has two routes: `/` and `/uploadvideo/`. + +The `/` route returns an HTML template that displays a gallery of all uploaded videos. + +The `/uploadvideo/` route accepts a `POST` request with an `UploadFile` object, which represents the uploaded video file. The video file is "acapellified" via the `process_video()` method, and the output video is stored in a list which stores all of the uploaded videos in memory. + +Note that this is a very basic example and if this were a production app, you will need to add more logic to handle file storage, user authentication, and security considerations. + +## Step 3: Create a FastAPI app (Frontend Template) + +Finally, we create the frontend of our web application. First, we create a folder called `templates` in the same directory as `main.py`. We then create a template, `home.html` inside the `templates` folder. Here is the resulting file structure: + +```csv +├── main.py +├── templates +│ └── home.html +``` + +Write the following as the contents of `home.html`: + +```html +<!DOCTYPE html> <html> <head> <title>Video Gallery</title> +<style> body { font-family: sans-serif; margin: 0; padding: 0; +background-color: #f5f5f5; } h1 { text-align: center; margin-top: 30px; +margin-bottom: 20px; } .gallery { display: flex; flex-wrap: wrap; +justify-content: center; gap: 20px; padding: 20px; } .video { border: 2px solid +#ccc; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); border-radius: 5px; overflow: +hidden; width: 300px; margin-bottom: 20px; } .video video { width: 100%; height: +200px; } .video p { text-align: center; margin: 10px 0; } form { margin-top: +20px; text-align: center; } input[type="file"] { display: none; } .upload-btn { +display: inline-block; background-color: #3498db; color: #fff; padding: 10px +20px; font-size: 16px; border: none; border-radius: 5px; cursor: pointer; } +.upload-btn:hover { background-color: #2980b9; } .file-name { margin-left: 10px; +} </style> </head> <body> <h1>Video Gallery</h1> {% if videos %} +<div class="gallery"> {% for video in videos %} <div class="video"> +<video controls> <source src="{{ url_for('static', path=video) }}" +type="video/mp4"> Your browser does not support the video tag. </video> +<p>{{ video }}</p> </div> {% endfor %} </div> {% else %} <p>No +videos uploaded yet.</p> {% endif %} <form action="/uploadvideo/" +method="post" enctype="multipart/form-data"> <label for="video-upload" +class="upload-btn">Choose video file</label> <input type="file" +name="video" id="video-upload"> <span class="file-name"></span> <button +type="submit" class="upload-btn">Upload</button> </form> <script> // +Display selected file name in the form const fileUpload = +document.getElementById("video-upload"); const fileName = +document.querySelector(".file-name"); fileUpload.addEventListener("change", (e) +=> { fileName.textContent = e.target.files[0].name; }); </script> </body> +</html> +``` + +## Step 4: Run your FastAPI app + +Finally, we are ready to run our FastAPI app, powered by the Gradio Python Client! + +Open up a terminal and navigate to the directory containing `main.py`. Then run the following command in the terminal: + +```bash +$ uvicorn main:app +``` + +You should see an output that looks like this: + +```csv +Loaded as API: https://abidlabs-music-separation.hf.space ✔ +INFO: Started server process [1360] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +And that's it! Start uploading videos and you'll get some "acapellified" videos in response (might take seconds to minutes to process depending on the length of your videos). Here's how the UI looks after uploading two videos: + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/acapellify.png) + +If you'd like to learn more about how to use the Gradio Python Client in your projects, [read the dedicated Guide](/guides/getting-started-with-the-python-client/). diff --git a/guides/09_gradio-clients-and-lite/08_server-mode.md b/guides/09_gradio-clients-and-lite/08_server-mode.md new file mode 100644 index 0000000..0476966 --- /dev/null +++ b/guides/09_gradio-clients-and-lite/08_server-mode.md @@ -0,0 +1,137 @@ +# Server mode + +Tags: API, MCP, FASTAPI, SERVER + +In this post, we will demonstrate how to build a completely custom frontend for your Gradio application, while still utilizing Gradio's backend, which means you still get an API server with queuing and streaming, MCP tool support, ZeroGPU support, and hosting on Hugging Face Spaces. + +To do this, you use **Server mode**: instantiate `gradio.Server` directly. The `gradio.Server` class is a FastAPI server with Gradio's API engine built in, so you get all the backend benefits with complete flexibility on what kind of frontend (e.g. a React app, a simple HTML page, or any vibe-coded frontend), if any, you'd like to launch alongside the backend server. + +## When to use `gradio.Server` + +Use `gradio.Server` instead of `gr.Blocks` when any of the following apply: + +- You want a **completely custom (potentially vibe-coded) UI** (your own HTML, React, Svelte, etc.) powered by Gradio's backend +- You want **full FastAPI control** (custom GET/POST routes, middleware, dependency injection) alongside Gradio API endpoints +- You're building a service to **host on Spaces** with or without ZeroGPU but don't need Gradio components + +If you're happy with Gradio's built-in UI components, use `gr.Blocks`, `gr.ChatInterface`, or `gr.Interface` instead. + +## Installation + +`gradio.Server` is included in the main Gradio package. If you want MCP support, install the extra: + +```bash +pip install "gradio[mcp]" +``` + +## A Minimal Example + +Here's the simplest possible Server mode app — a single API endpoint with no UI: + +```python +from gradio import Server + +app = Server() + +@app.api(name="hello") +def hello(name: str) -> str: + return f"Hello, {name}!" + +app.launch() +``` + +That's it. When you run this script, you get: + +- A Gradio API endpoint at `/gradio_api/call/hello` with queuing and SSE streaming +- Auto-generated API docs at `/gradio_api/info` +- A Python and JavaScript client that can call `/hello` by name + +You can test it with the Gradio Python client: + +```python +from gradio_client import Client + +client = Client("http://localhost:7860") +result = client.predict("World", api_name="/hello") +print(result) # "Hello, World!" +``` + +## Custom Routes + +Since `gradio.Server` inherits from FastAPI, you can add any route directly: + +```python +from gradio import Server +from fastapi.responses import HTMLResponse + +app = Server() + +@app.api(name="hello") +def hello(name: str) -> str: + return f"Hello, {name}!" + +@app.get("/", response_class=HTMLResponse) +async def homepage(): + return "

Welcome to my API

" + +@app.get("/health") +async def health(): + return {"status": "ok"} + +app.launch() +``` + +Your custom routes take priority over Gradio's default routes. For example, your `GET /` replaces Gradio's default UI page. + +You can also use all standard FastAPI features — `app.add_middleware()`, `app.include_router()`, dependency injection, exception handlers, and so on. + +## MCP Tools + +To expose your API endpoints as MCP tools, add the `@app.mcp.tool()` decorator and pass `mcp_server=True` to `launch()`: + +```python +from gradio import Server + +app = Server() + +@app.mcp.tool(name="hello") +@app.api(name="hello") +def hello(name: str) -> str: + """Greet someone by name.""" + return f"Hello, {name}!" + +app.launch(mcp_server=True) +``` + +The `@app.mcp.tool()` and `@app.api()` decorators are independent — you can have API-only endpoints or MCP-only tools. Stack both when you want a function available through both. + +## A Complete Example with the JavaScript Client + +This example combines everything: custom HTML served at `/`, Gradio API endpoints with concurrency limits, MCP tools, and a custom REST endpoint, and two connected via [the Gradio JavaScript client](/guides/getting-started-with-the-js-client). + +$code_server_app + +Run it with: + +```bash +python run.py +``` + +Then open `http://localhost:7860` in your browser. The custom HTML page uses the `@gradio/client` JavaScript library to call the Gradio API endpoints. Meanwhile, the same endpoints are available as MCP tools and through the REST API at `/gradio_api/call/add` and `/gradio_api/call/multiply`. + +Note: if your `Server` app uses ZeroGPU, you _must_ call Gradio API endpoints through `@gradio/client` from the browser. The JavaScript client forwards the Hugging Face iframe auth headers needed for ZeroGPU quota handling. + +## Concurrency and Streaming + +`app.api()` supports all of the same concurrency and streaming options as `gr.api()`: + +```python +@app.api(name="generate", concurrency_limit=2, stream_every=0.5) +async def generate(prompt: str): + for token in model.generate(prompt): + yield token +``` + +Generator functions automatically stream results via SSE, just like in a regular Gradio app. The `concurrency_limit` parameter controls how many concurrent calls to this endpoint are allowed. By default, this is set to 1, since many ML workloads that run on GPU can only support a single user at a time. However, you can increase this, or set to `None` to use FastAPI defaults, if you are e.g. calling an external API. + +For the full API reference, see the [`Server` documentation](/docs/gradio/server). diff --git a/guides/10_mcp/01_building-mcp-server-with-gradio.md b/guides/10_mcp/01_building-mcp-server-with-gradio.md new file mode 100644 index 0000000..4472f47 --- /dev/null +++ b/guides/10_mcp/01_building-mcp-server-with-gradio.md @@ -0,0 +1,431 @@ +# Building an MCP Server with Gradio + +Tags: MCP, TOOL, LLM, SERVER + +In this guide, we will describe how to launch your Gradio app so that it functions as an MCP Server. + +Punchline: it's as simple as setting `mcp_server=True` in `.launch()`. + +### Prerequisites + +If not already installed, please install Gradio with the MCP extra: + +```bash +pip install "gradio[mcp]" +``` + +This will install the necessary dependencies, including the `mcp` package. Also, you will need an LLM application that supports tool calling using the MCP protocol, such as Claude Desktop, Cursor, or Cline (these are known as "MCP Clients"). + +## What is an MCP Server? + +An MCP (Model Control Protocol) server is a standardized way to expose tools so that they can be used by LLMs. A tool can provide an LLM functionality that it does not have natively, such as the ability to generate images or calculate the prime factors of a number. + +## Example: Counting Letters in a Word + +LLMs are famously not great at counting the number of letters in a word (e.g. the number of "r"-s in "strawberry"). But what if we equip them with a tool to help? Let's start by writing a simple Gradio app that counts the number of letters in a word or phrase: + +$code_letter_counter + +Notice that we have: (1) included a detailed docstring for our function, and (2) set `mcp_server=True` in `.launch()`. This is all that's needed for your Gradio app to serve as an MCP server! Now, when you run this app, it will: + +1. Start the regular Gradio web interface +2. Start the MCP server +3. Print the MCP server URL in the console + +The MCP server will be accessible at: +``` +http://your-server:port/gradio_api/mcp/ +``` + +Gradio automatically converts the `letter_counter` function into an MCP tool that can be used by LLMs. The docstring of the function and the type hints of arguments will be used to generate the description of the tool and its parameters. The name of the function will be used as the name of your tool. Any initial values you provide to your input components (e.g. "strawberry" and "r" in the `gr.Textbox` components above) will be used as the default values if your LLM doesn't specify a value for that particular input parameter. + +Now, all you need to do is add this URL endpoint to your MCP Client (e.g. Claude Desktop, Cursor, or Cline), which typically means pasting this config in the settings: + +``` +{ + "mcpServers": { + "gradio": { + "url": "http://your-server:port/gradio_api/mcp/" + } + } +} +``` + +(By the way, you can find the exact config to copy-paste by going to the "View API" link in the footer of your Gradio app, and then clicking on "MCP"). + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/view-api-mcp.png) + +## Key features of the Gradio <> MCP Integration + +1. **Tool Conversion**: Each API endpoint in your Gradio app is automatically converted into an MCP tool with a corresponding name, description, and input schema. To view the tools and schemas, visit http://your-server:port/gradio_api/mcp/schema or go to the "View API" link in the footer of your Gradio app, and then click on "MCP". + + +2. **Environment variable support**. There are two ways to enable the MCP server functionality: + +* Using the `mcp_server` parameter, as shown above: + ```python + demo.launch(mcp_server=True) + ``` + +* Using environment variables: + ```bash + export GRADIO_MCP_SERVER=True + ``` + +3. **File Handling**: The Gradio MCP server automatically handles file data conversions, including: + - Processing image files and returning them in the correct format + - Managing temporary file storage + + By default, the Gradio MCP server accepts input images and files as full URLs ("http://..." or "https:/..."). For convenience, an additional STDIO-based MCP server is also generated, which can be used to upload files to any remote Gradio app and which returns a URL that can be used for subsequent tool calls. + +4. **Hosted MCP Servers on 󠀠🤗 Spaces**: You can publish your Gradio application for free on Hugging Face Spaces, which will allow you to have a free hosted MCP server. Here's an example of such a Space: https://huggingface.co/spaces/abidlabs/mcp-tools. Notice that you can add this config to your MCP Client to start using the tools from this Space immediately: + +``` +{ + "mcpServers": { + "gradio": { + "url": "https://abidlabs-mcp-tools.hf.space/gradio_api/mcp/" + } + } +} +``` + + + +Tip: To minimize latency and increase throughput by as much as 10 times, set queue=False in the event handlers of your Gradio app. However, this disables progress notifications so its recommended that long running events set queue=True + +## Converting an Existing Space + +If there's an existing Space that you'd like to use an MCP server, you'll need to do three things: + +1. First, [duplicate the Space](https://huggingface.co/docs/hub/en/spaces-more-ways-to-create#duplicating-a-space) if it is not your own Space. This will allow you to make changes to the app. If the Space requires a GPU, set the hardware of the duplicated Space to be same as the original Space. You can make it either a public Space or a private Space, since it is possible to use either as an MCP server, as described below. +2. Then, add docstrings to the functions that you'd like the LLM to be able to call as a tool. The docstring should be in the same format as the example code above. +3. Finally, add `mcp_server=True` in `.launch()`. + +That's it! + +## Private Spaces + +You can use either a public Space or a private Space as an MCP server. If you'd like to use a private Space as an MCP server (or a ZeroGPU Space with your own quota), then you will need to provide your [Hugging Face token](https://huggingface.co/settings/token) when you make your request. To do this, simply add it as a header in your config like this: + +``` +{ + "mcpServers": { + "gradio": { + "url": "https://abidlabs-mcp-tools.hf.space/gradio_api/mcp/", + "headers": { + "Authorization": "Bearer " + } + } + } +} +``` + +## Authentication and Credentials + +You may wish to authenticate users more precisely or let them provide other kinds of credentials or tokens in order to provide a custom experience for different users. + +Gradio allows you to access the underlying `starlette.Request` that has made the tool call, which means that you can access headers, originating IP address, or any other information that is part of the network request. To do this, simply add a parameter in your function of the type `gr.Request`, and Gradio will automatically inject the request object as the parameter. + +Here's an example: + +```py +import gradio as gr + +def echo_headers(x, request: gr.Request): + return str(dict(request.headers)) + +gr.Interface(echo_headers, "textbox", "textbox").launch(mcp_server=True) +``` + +This MCP server will simply ignore the user's input and echo back all of the headers from a user's request. One can build more complex apps using the same idea. See the [docs on `gr.Request`](https://www.gradio.app/main/docs/gradio/request) for more information (note that only the core Starlette attributes of the `gr.Request` object will be present, attributes such as Gradio's `.session_hash` will not be present). + +### Using the gr.Header class + +A common pattern in MCP server development is to use authentication headers to call services on behalf of your users. Instead of using a `gr.Request` object like in the example above, you can use a `gr.Header` argument. Gradio will automatically extract that header from the incoming request (if it exists) and pass it to your function. + +In the example below, the `X-API-Token` header is extracted from the incoming request and passed in as the `x_api_token` argument to `make_api_request_on_behalf_of_user`. + +The benefit of using `gr.Header` is that the MCP connection docs will automatically display the headers you need to supply when connecting to the server! See the image below: + +```python +import gradio as gr + +def make_api_request_on_behalf_of_user(prompt: str, x_api_token: gr.Header): + """Make a request to everyone's favorite API. + Args: + prompt: The prompt to send to the API. + Returns: + The response from the API. + Raises: + AssertionError: If the API token is not valid. + """ + return "Hello from the API" if not x_api_token else "Hello from the API with token!" + + +demo = gr.Interface( + make_api_request_on_behalf_of_user, + [ + gr.Textbox(label="Prompt"), + ], + gr.Textbox(label="Response"), +) + +demo.launch(mcp_server=True) +``` + +![MCP Header Connection Page](https://github.com/user-attachments/assets/e264eedf-a91a-476b-880d-5be0d5934134) + +### Sending Progress Updates + +The Gradio MCP server automatically sends progress updates to your MCP Client based on the queue in the Gradio application. If you'd like to send custom progress updates, you can do so using the same mechanism as you would use to display progress updates in the UI of your Gradio app: by using the `gr.Progress` class! + +Here's an example of how to do this: + +$code_mcp_progress + +[Here are the docs](https://www.gradio.app/docs/gradio/progress) for the `gr.Progress` class, which can also automatically track `tqdm` calls. + +Note: by default, progress notifications are enabled for all MCP tools, even if the corresponding Gradio functions do not include a `gr.Progress`. However, this can add some overhead to the MCP tool (typically ~500ms). To disable progress notification, you can set `queue=False` in your Gradio event handler to skip the overhead related to subscribing to the queue's progress updates. + + +## Modifying Tool Descriptions + +Gradio automatically sets the tool name based on the name of your function, and the description from the docstring of your function. But you may want to change how the description appears to your LLM. You can do this by using the `api_description` parameter in `Interface`, `ChatInterface`, or any event listener. This parameter takes three different kinds of values: + +* `None` (default): the tool description is automatically created from the docstring of the function (or its parent's docstring if it does not have a docstring but inherits from a method that does.) +* `False`: no tool description appears to the LLM. +* `str`: an arbitrary string to use as the tool description. + +In addition to modifying the tool descriptions, you can also toggle which tools appear to the LLM. You can do this by setting the `show_api` parameter, which is by default `True`. Setting it to `False` hides the endpoint from the API docs and from the MCP server. If you expose multiple tools, users of your app will also be able to toggle which tools they'd like to add to their MCP server by checking boxes in the "view MCP or API" panel. + +Here's an example that shows the `api_description` and `show_api` parameters in actions: + +$code_mcp_tools + + + +## MCP Resources and Prompts + +In addition to tools (which execute functions generally and are the default for any function exposed through the Gradio MCP integration), MCP supports two other important primitives: **resources** (for exposing data) and **prompts** (for defining reusable templates). Gradio provides decorators to easily create MCP servers with all three capabilities. + + +### Creating MCP Resources + +Use the `@gr.mcp.resource` decorator on any function to expose data through your Gradio app. Resources can be static (always available at a fixed URI) or templated (with parameters in the URI). + +$code_mcp_resources_and_prompts + +In this example: +- The `get_greeting` function is exposed as a resource with a URI template `greeting://{name}` +- When an MCP client requests `greeting://Alice`, it receives "Hello, Alice!" +- Resources can also return images and other types of files or binary data. In order to return non-text data, you should specify the `mime_type` parameter in `@gr.mcp.resource()` and return a Base64 string from your function. + +### Creating MCP Prompts + +Prompts help standardize how users interact with your tools. They're especially useful for complex workflows that require specific formatting or multiple steps. + +The `greet_user` function in the example above is decorated with `@gr.mcp.prompt()`, which: +- Makes it available as a prompt template in MCP clients +- Accepts parameters (`name` and `style`) to customize the output +- Returns a structured prompt that guides the LLM's behavior + + +## Adding MCP-Only Functions + +So far, all of our MCP tools, resources, or prompts have corresponded to event listeners in the UI. This works well for functions that directly update the UI, but may not work if you wish to expose a "pure logic" function that should return raw data (e.g. a JSON object) without directly causing a UI update. + +In order to expose such an MCP tool, you can create a pure Gradio API endpoint using `gr.api` (see [full docs here](https://www.gradio.app/main/docs/gradio/api)). Here's an example of creating an MCP tool that slices a list: + +$code_mcp_tool_only + +Note that if you use this approach, your function signature must be fully typed, including the return value, as these signature are used to determine the typing information for the MCP tool. + +## Gradio with FastMCP + +In some cases, you may decide not to use Gradio's built-in integration and instead manually create an FastMCP Server that calls a Gradio app. This approach is useful when you want to: + +- Store state / identify users between calls instead of treating every tool call completely independently +- Start the Gradio app MCP server when a tool is called (if you are running multiple Gradio apps locally and want to save memory / GPU) + +This is very doable thanks to the [Gradio Python Client](https://www.gradio.app/guides/getting-started-with-the-python-client) and the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)'s `FastMCP` class. Here's an example of creating a custom MCP server that connects to various Gradio apps hosted on [HuggingFace Spaces](https://huggingface.co/spaces) using the `stdio` protocol: + +```python +from mcp.server.fastmcp import FastMCP +from gradio_client import Client +import sys +import io +import json + +mcp = FastMCP("gradio-spaces") + +clients = {} + +def get_client(space_id: str) -> Client: + """Get or create a Gradio client for the specified space.""" + if space_id not in clients: + clients[space_id] = Client(space_id) + return clients[space_id] + + +@mcp.tool() +async def generate_image(prompt: str, space_id: str = "ysharma/SanaSprint") -> str: + """Generate an image using Flux. + + Args: + prompt: Text prompt describing the image to generate + space_id: HuggingFace Space ID to use + """ + client = get_client(space_id) + result = client.predict( + prompt=prompt, + model_size="1.6B", + seed=0, + randomize_seed=True, + width=1024, + height=1024, + guidance_scale=4.5, + num_inference_steps=2, + api_name="/infer" + ) + return result + + +@mcp.tool() +async def run_dia_tts(prompt: str, space_id: str = "ysharma/Dia-1.6B") -> str: + """Text-to-Speech Synthesis. + + Args: + prompt: Text prompt describing the conversation between speakers S1, S2 + space_id: HuggingFace Space ID to use + """ + client = get_client(space_id) + result = client.predict( + text_input=f"""{prompt}""", + audio_prompt_input=None, + max_new_tokens=3072, + cfg_scale=3, + temperature=1.3, + top_p=0.95, + cfg_filter_top_k=30, + speed_factor=0.94, + api_name="/generate_audio" + ) + return result + + +if __name__ == "__main__": + import sys + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + + mcp.run(transport='stdio') +``` + +This server exposes two tools: +1. `run_dia_tts` - Generates a conversation for the given transcript in the form of `[S1]first-sentence. [S2]second-sentence. [S1]...` +2. `generate_image` - Generates images using a fast text-to-image model + +To use this MCP Server with Claude Desktop (as MCP Client): + +1. Save the code to a file (e.g., `gradio_mcp_server.py`) +2. Install the required dependencies: `pip install mcp gradio-client` +3. Configure Claude Desktop to use your server by editing the configuration file at `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): + +```json +{ + "mcpServers": { + "gradio-spaces": { + "command": "python", + "args": [ + "/absolute/path/to/gradio_mcp_server.py" + ] + } + } +} +``` + +4. Restart Claude Desktop + +Now, when you ask Claude about generating an image or transcribing audio, it can use your Gradio-powered tools to accomplish these tasks. + + +## Troubleshooting your MCP Servers + +The MCP protocol is still in its infancy and you might see issues connecting to an MCP Server that you've built. We generally recommend using the [MCP Inspector Tool](https://github.com/modelcontextprotocol/inspector) to try connecting and debugging your MCP Server. + +Here are some things that may help: + +**1. Ensure that you've provided type hints and valid docstrings for your functions** + +As mentioned earlier, Gradio reads the docstrings for your functions and the type hints of input arguments to generate the description of the tool and parameters. A valid function and docstring looks like this (note the "Args:" block with indented parameter names underneath): + +```py +def image_orientation(image: Image.Image) -> str: + """ + Returns whether image is portrait or landscape. + + Args: + image (Image.Image): The image to check. + """ + return "Portrait" if image.height > image.width else "Landscape" +``` + +Note: You can preview the schema that is created for your MCP server by visiting the `http://your-server:port/gradio_api/mcp/schema` URL. + +**2. Try accepting input arguments as `str`** + +Some MCP Clients do not recognize parameters that are numeric or other complex types, but all of the MCP Clients that we've tested accept `str` input parameters. When in doubt, change your input parameter to be a `str` and then cast to a specific type in the function, as in this example: + +```py +def prime_factors(n: str): + """ + Compute the prime factorization of a positive integer. + + Args: + n (str): The integer to factorize. Must be greater than 1. + """ + n_int = int(n) + if n_int <= 1: + raise ValueError("Input must be an integer greater than 1.") + + factors = [] + while n_int % 2 == 0: + factors.append(2) + n_int //= 2 + + divisor = 3 + while divisor * divisor <= n_int: + while n_int % divisor == 0: + factors.append(divisor) + n_int //= divisor + divisor += 2 + + if n_int > 1: + factors.append(n_int) + + return factors +``` + +**3. Ensure that your MCP Client Supports Streamable HTTP** + +Some MCP Clients do not yet support streamable HTTP-based MCP Servers. In those cases, you can use a tool such as [mcp-remote](https://github.com/geelen/mcp-remote). First install [Node.js](https://nodejs.org/en/download/). Then, add the following to your own MCP Client config: + +``` +{ + "mcpServers": { + "gradio": { + "command": "npx", + "args": [ + "mcp-remote", + "http://your-server:port/gradio_api/mcp/" + ] + } + } +} +``` + +**4. Restart your MCP Client and MCP Server** + +Some MCP Clients require you to restart them every time you update the MCP configuration. Other times, if the connection between the MCP Client and servers breaks, you might need to restart the MCP server. If all else fails, try restarting both your MCP Client and MCP Servers! + diff --git a/guides/10_mcp/02_file-upload-mcp.md b/guides/10_mcp/02_file-upload-mcp.md new file mode 100644 index 0000000..e47741c --- /dev/null +++ b/guides/10_mcp/02_file-upload-mcp.md @@ -0,0 +1,48 @@ +# The File Upload MCP Server + +Tags: MCP, TOOL, LLM, SERVER, DOCS + +If you've tried to to use a remote Gradio MCP server that takes a file as input (image, video, audio), you've probably run into this error: + + + +The reason is that since the Gradio server is hosted on a different machine, any input files must be available via a public URL so that they can downloaded in the remote machine. + +There are many ways to host files on the internet, but they all require adding a manual step to your workflow. In the age of LLM agents, shouldn't we expect them to handle this step for you? + +In this post, we'll show how you can connect your LLM to the "File Upload" MCP server so that it can handle the file uploading for you when appropriate! + +## Using the File Upload MCP Server + +As of version 5.36.0, Gradio now comes with a built-in MCP server that can upload files to a running Gradio application. In the `View API` page of the server, you should see the following code snippet if any of the tools require file inputs: + + + +The command to start the MCP server takes two arguments: + +- The URL (or Hugging Face space id) of the gradio application to upload the files to. In this case, `http://127.0.0.1:7860`. +- The local directory on your computer with which the server is allowed to upload files from (``). For security, please make this directory as narrow as possible to prevent unintended file uploads. + +As stated in the image, you need to install [uv](https://docs.astral.sh/uv/getting-started/installation/) (a python package manager that can run python scripts) before connecting from your MCP client. + +If you have gradio installed locally and you don't want to install uv, you can replace the `uvx` command with the path to gradio binary. It should look like this: + +```json +"upload-files": { + "command": "", + "args": [ + "upload-mcp", + "http://localhost:7860/", + "/Users/freddyboulton/Pictures" + ] +} +``` + +After connecting to the upload server, your LLM agent will know when to upload files for you automatically! + + + +## Conclusion + +In this guide, we've covered how you can connect to the Upload File MCP Server so that your agent can upload files before using Gradio MCP servers. Remember to set the `` as small as possible to prevent unintended file uploads! + diff --git a/guides/10_mcp/03_building-an-mcp-client-with-gradio.md b/guides/10_mcp/03_building-an-mcp-client-with-gradio.md new file mode 100644 index 0000000..cda3e22 --- /dev/null +++ b/guides/10_mcp/03_building-an-mcp-client-with-gradio.md @@ -0,0 +1,415 @@ +# Using the Gradio Chatbot as an MCP Client + +This guide will walk you through a Model Context Protocol (MCP) Client and Server implementation with Gradio. You'll build a Gradio Chatbot that uses Anthropic's Claude API to respond to user messages, but also, as an MCP Client, generates images (by connecting to an MCP Server, which is a separate Gradio app). + + + +## What is MCP? + +The Model Context Protocol (MCP) standardizes how applications provide context to LLMs. It allows Claude to interact with external tools, like image generators, file systems, or APIs, etc. + +## Prerequisites + +- Python 3.10+ +- An Anthropic API key +- Basic understanding of Python programming + +## Setup + +First, install the required packages: + +```bash +pip install gradio anthropic mcp +``` + +Create a `.env` file in your project directory and add your Anthropic API key: + +``` +ANTHROPIC_API_KEY=your_api_key_here +``` + +## Part 1: Building the MCP Server + +The server provides tools that Claude can use. In this example, we'll create a server that generates images through [a HuggingFace space](https://huggingface.co/spaces/ysharma/SanaSprint). + +Create a file named `gradio_mcp_server.py`: + +```python +from mcp.server.fastmcp import FastMCP +import json +import sys +import io +import time +from gradio_client import Client + +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + +mcp = FastMCP("huggingface_spaces_image_display") + +@mcp.tool() +async def generate_image(prompt: str, width: int = 512, height: int = 512) -> str: + """Generate an image using SanaSprint model. + + Args: + prompt: Text prompt describing the image to generate + width: Image width (default: 512) + height: Image height (default: 512) + """ + client = Client("https://ysharma-sanasprint.hf.space/") + + try: + result = client.predict( + prompt, + "0.6B", + 0, + True, + width, + height, + 4.0, + 2, + api_name="/infer" + ) + + if isinstance(result, list) and len(result) >= 1: + image_data = result[0] + if isinstance(image_data, dict) and "url" in image_data: + return json.dumps({ + "type": "image", + "url": image_data["url"], + "message": f"Generated image for prompt: {prompt}" + }) + + return json.dumps({ + "type": "error", + "message": "Failed to generate image" + }) + + except Exception as e: + return json.dumps({ + "type": "error", + "message": f"Error generating image: {str(e)}" + }) + +if __name__ == "__main__": + mcp.run(transport='stdio') +``` + +### What this server does: + +1. It creates an MCP server that exposes a `generate_image` tool +2. The tool connects to the SanaSprint model hosted on HuggingFace Spaces +3. It handles the asynchronous nature of image generation by polling for results +4. When an image is ready, it returns the URL in a structured JSON format + +## Part 2: Building the MCP Client with Gradio + +Now let's create a Gradio chat interface as MCP Client that connects Claude to our MCP server. + +Create a file named `app.py`: + +```python +import asyncio +import os +import json +from typing import List, Dict, Any, Union +from contextlib import AsyncExitStack + +import gradio as gr +from gradio.components.chatbot import ChatMessage +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client +from anthropic import Anthropic +from dotenv import load_dotenv + +load_dotenv() + +loop = asyncio.new_event_loop() +asyncio.set_event_loop(loop) + +class MCPClientWrapper: + def __init__(self): + self.session = None + self.exit_stack = None + self.anthropic = Anthropic() + self.tools = [] + + def connect(self, server_path: str) -> str: + return loop.run_until_complete(self._connect(server_path)) + + async def _connect(self, server_path: str) -> str: + if self.exit_stack: + await self.exit_stack.aclose() + + self.exit_stack = AsyncExitStack() + + is_python = server_path.endswith('.py') + command = "python" if is_python else "node" + + server_params = StdioServerParameters( + command=command, + args=[server_path], + env={"PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1"} + ) + + stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) + self.stdio, self.write = stdio_transport + + self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) + await self.session.initialize() + + response = await self.session.list_tools() + self.tools = [{ + "name": tool.name, + "description": tool.description, + "input_schema": tool.inputSchema + } for tool in response.tools] + + tool_names = [tool["name"] for tool in self.tools] + return f"Connected to MCP server. Available tools: {', '.join(tool_names)}" + + def process_message(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]]) -> tuple: + if not self.session: + return history + [ + {"role": "user", "content": message}, + {"role": "assistant", "content": "Please connect to an MCP server first."} + ], gr.Textbox(value="") + + new_messages = loop.run_until_complete(self._process_query(message, history)) + return history + [{"role": "user", "content": message}] + new_messages, gr.Textbox(value="") + + async def _process_query(self, message: str, history: List[Union[Dict[str, Any], ChatMessage]]): + claude_messages = [] + for msg in history: + if isinstance(msg, ChatMessage): + role, content = msg.role, msg.content + else: + role, content = msg.get("role"), msg.get("content") + + if role in ["user", "assistant", "system"]: + claude_messages.append({"role": role, "content": content}) + + claude_messages.append({"role": "user", "content": message}) + + response = self.anthropic.messages.create( + model="claude-3-5-sonnet-20241022", + max_tokens=1000, + messages=claude_messages, + tools=self.tools + ) + + result_messages = [] + + for content in response.content: + if content.type == 'text': + result_messages.append({ + "role": "assistant", + "content": content.text + }) + + elif content.type == 'tool_use': + tool_name = content.name + tool_args = content.input + + result_messages.append({ + "role": "assistant", + "content": f"I'll use the {tool_name} tool to help answer your question.", + "metadata": { + "title": f"Using tool: {tool_name}", + "log": f"Parameters: {json.dumps(tool_args, ensure_ascii=True)}", + "status": "pending", + "id": f"tool_call_{tool_name}" + } + }) + + result_messages.append({ + "role": "assistant", + "content": "```json\n" + json.dumps(tool_args, indent=2, ensure_ascii=True) + "\n```", + "metadata": { + "parent_id": f"tool_call_{tool_name}", + "id": f"params_{tool_name}", + "title": "Tool Parameters" + } + }) + + result = await self.session.call_tool(tool_name, tool_args) + + if result_messages and "metadata" in result_messages[-2]: + result_messages[-2]["metadata"]["status"] = "done" + + result_messages.append({ + "role": "assistant", + "content": "Here are the results from the tool:", + "metadata": { + "title": f"Tool Result for {tool_name}", + "status": "done", + "id": f"result_{tool_name}" + } + }) + + result_content = result.content + if isinstance(result_content, list): + result_content = "\n".join(str(item) for item in result_content) + + try: + result_json = json.loads(result_content) + if isinstance(result_json, dict) and "type" in result_json: + if result_json["type"] == "image" and "url" in result_json: + result_messages.append({ + "role": "assistant", + "content": {"path": result_json["url"], "alt_text": result_json.get("message", "Generated image")}, + "metadata": { + "parent_id": f"result_{tool_name}", + "id": f"image_{tool_name}", + "title": "Generated Image" + } + }) + else: + result_messages.append({ + "role": "assistant", + "content": "```\n" + result_content + "\n```", + "metadata": { + "parent_id": f"result_{tool_name}", + "id": f"raw_result_{tool_name}", + "title": "Raw Output" + } + }) + except: + result_messages.append({ + "role": "assistant", + "content": "```\n" + result_content + "\n```", + "metadata": { + "parent_id": f"result_{tool_name}", + "id": f"raw_result_{tool_name}", + "title": "Raw Output" + } + }) + + claude_messages.append({"role": "user", "content": f"Tool result for {tool_name}: {result_content}"}) + next_response = self.anthropic.messages.create( + model="claude-3-5-sonnet-20241022", + max_tokens=1000, + messages=claude_messages, + ) + + if next_response.content and next_response.content[0].type == 'text': + result_messages.append({ + "role": "assistant", + "content": next_response.content[0].text + }) + + return result_messages + +client = MCPClientWrapper() + +def gradio_interface(): + with gr.Blocks(title="MCP Weather Client") as demo: + gr.Markdown("# MCP Weather Assistant") + gr.Markdown("Connect to your MCP weather server and chat with the assistant") + + with gr.Row(equal_height=True): + with gr.Column(scale=4): + server_path = gr.Textbox( + label="Server Script Path", + placeholder="Enter path to server script (e.g., weather.py)", + value="gradio_mcp_server.py" + ) + with gr.Column(scale=1): + connect_btn = gr.Button("Connect") + + status = gr.Textbox(label="Connection Status", interactive=False) + + chatbot = gr.Chatbot( + value=[], + height=500, + show_copy_button=True, + avatar_images=("👤", "🤖") + ) + + with gr.Row(equal_height=True): + msg = gr.Textbox( + label="Your Question", + placeholder="Ask about weather or alerts (e.g., What's the weather in New York?)", + scale=4 + ) + clear_btn = gr.Button("Clear Chat", scale=1) + + connect_btn.click(client.connect, inputs=server_path, outputs=status) + msg.submit(client.process_message, [msg, chatbot], [chatbot, msg]) + clear_btn.click(lambda: [], None, chatbot) + + return demo + +if __name__ == "__main__": + if not os.getenv("ANTHROPIC_API_KEY"): + print("Warning: ANTHROPIC_API_KEY not found in environment. Please set it in your .env file.") + + interface = gradio_interface() + interface.launch(debug=True) +``` + +### What this MCP Client does: + +- Creates a friendly Gradio chat interface for user interaction +- Connects to the MCP server you specify +- Handles conversation history and message formatting +- Makes call to Claude API with tool definitions +- Processes tool usage requests from Claude +- Displays images and other tool outputs in the chat +- Sends tool results back to Claude for interpretation + +## Running the Application + +To run your MCP application: + +- Start a terminal window and run the MCP Client: + ```bash + python app.py + ``` +- Open the Gradio interface at the URL shown (typically http://127.0.0.1:7860) +- In the Gradio interface, you'll see a field for the MCP Server path. It should default to `gradio_mcp_server.py`. +- Click "Connect" to establish the connection to the MCP server. +- You should see a message indicating the server connection was successful. + +## Example Usage + +Now you can chat with Claude and it will be able to generate images based on your descriptions. + +Try prompts like: +- "Can you generate an image of a mountain landscape at sunset?" +- "Create an image of a cool tabby cat" +- "Generate a picture of a panda wearing sunglasses" + +Claude will recognize these as image generation requests and automatically use the `generate_image` tool from your MCP server. + + +## How it Works + +Here's the high-level flow of what happens during a chat session: + +1. Your prompt enters the Gradio interface +2. The client forwards your prompt to Claude +3. Claude analyzes the prompt and decides to use the `generate_image` tool +4. The client sends the tool call to the MCP server +5. The server calls the external image generation API +6. The image URL is returned to the client +7. The client sends the image URL back to Claude +8. Claude provides a response that references the generated image +9. The Gradio chat interface displays both Claude's response and the image + + +## Next Steps + +Now that you have a working MCP system, here are some ideas to extend it: + +- Add more tools to your server +- Improve error handling +- Add private Huggingface Spaces with authentication for secure tool access +- Create custom tools that connect to your own APIs or services +- Implement streaming responses for better user experience + +## Conclusion + +Congratulations! You've successfully built an MCP Client and Server that allows Claude to generate images based on text prompts. This is just the beginning of what you can do with Gradio and MCP. This guide enables you to build complex AI applications that can use Claude or any other powerful LLM to interact with virtually any external tool or service. + +Read our other Guide on using [Gradio apps as MCP Servers](./building-mcp-server-with-gradio). diff --git a/guides/10_mcp/04_using-docs-mcp.md b/guides/10_mcp/04_using-docs-mcp.md new file mode 100644 index 0000000..07f0cfd --- /dev/null +++ b/guides/10_mcp/04_using-docs-mcp.md @@ -0,0 +1,83 @@ +# Using the Gradio Docs MCP Server + +Tags: MCP, TOOL, LLM, SERVER, DOCS + +In this guide, we will describe how to use the official Gradio Docs MCP Server. + +### Prerequisites + +You will need an LLM application that supports tool calling using the MCP protocol, such as Claude Desktop, Cursor, or Cline (these are known as "MCP Clients"). + +## Why an MCP Server? + +If you're using LLMs in your workflow, adding this server will augment them with just the right context on gradio - which makes your experience a lot faster and smoother. + + + +The server is running on Spaces and was launched entirely using Gradio, you can see all the code [here](https://huggingface.co/spaces/gradio/docs-mcp). For more on building an mcp server with gradio, see the [previous guide](./building-an-mcp-client-with-gradio). + +## Installing in the Clients + +For clients that support streamable HTTP (e.g. Cursor, Windsurf, Cline), simply add the following configuration to your MCP config: + +```json +{ + "mcpServers": { + "gradio": { + "url": "https://gradio-docs-mcp.hf.space/gradio_api/mcp/" + } + } +} +``` + +We've included step-by-step instructions for Cursor below, but you can consult the docs for Windsurf [here](https://docs.windsurf.com/windsurf/mcp), and Cline [here](https://docs.cline.bot/mcp-servers/configuring-mcp-servers) which are similar to set up. + + + +### Cursor + +1. Make sure you're using the latest version of Cursor, and go to Cursor > Settings > Cursor Settings > MCP +2. Click on '+ Add new global MCP server' +3. Copy paste this json into the file that opens and then save it. +```json +{ + "mcpServers": { + "gradio": { + "url": "https://gradio-docs-mcp.hf.space/gradio_api/mcp/" + } + } +} +``` +4. That's it! You should see the tools load and the status go green in the settings page. You may have to click the refresh icon or wait a few seconds. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/cursor-mcp.png) + +### Claude Desktop + +1. Since Claude Desktop only supports stdio, you will need to [install Node.js](https://nodejs.org/en/download/) to get this to work. +2. Make sure you're using the latest version of Claude Desktop, and go to Claude > Settings > Developer > Edit Config +3. Open the file with your favorite editor and copy paste this json, then save the file. +```json +{ + "mcpServers": { + "gradio": { + "command": "npx", + "args": [ + "mcp-remote", + "https://gradio-docs-mcp.hf.space/gradio_api/mcp/" + ] + } + } +} +``` +4. Quit and re-open Claude Desktop, and you should be good to go. You should see it loaded in the Search and Tools icon or on the developer settings page. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/claude-desktop-mcp.gif) + +## Tools + +There are currently only two tools in the server: `gradio_docs_mcp_load_gradio_docs` and `gradio_docs_mcp_search_gradio_docs`. + +1. `gradio_docs_mcp_load_gradio_docs`: This tool takes no arguments and will load an /llms.txt style summary of Gradio's latest, full documentation. Very useful context the LLM can parse before answering questions or generating code. + +2. `gradio_docs_mcp_search_gradio_docs`: This tool takes a query as an argument and will run embedding search on Gradio's docs, guides, and demos to return the most useful context for the LLM to parse. \ No newline at end of file diff --git a/guides/10_mcp/05_building-chatgpt-apps-with-gradio.md b/guides/10_mcp/05_building-chatgpt-apps-with-gradio.md new file mode 100644 index 0000000..a75893d --- /dev/null +++ b/guides/10_mcp/05_building-chatgpt-apps-with-gradio.md @@ -0,0 +1,210 @@ +# Building ChatGPT Apps with Gradio and Apps SDK + +[Apps in ChatGPT](https://openai.com/index/introducing-apps-in-chatgpt/) are a great way to let users try your machine learning models or other kinds of apps entirely by chatting in familiar chat application. OpenAI has released the [Apps SDK](https://developers.openai.com/apps-sdk/quickstart) for developers to build complete applications, but you can use Gradio to build ChatGPT apps very quickly, based off of your Gradio MCP server. We will also see how Gradio's built-in [share links](https://www.gradio.app/guides/sharing-your-app#sharing-demos) make it especially easy to iterate on your ChatGPT app! + +### Introduction + +Building a ChatGPT app requires doing two things: + +* Building a Gradio MCP server with at least one tool exposed. If you're not already familiar with building a Gradio MCP server, we recommend reading [this guide first](https://www.gradio.app/guides/building-mcp-server-with-gradio). + +* Building a custom UI with HTML, JavaScript, and CSS that will be displayed when your tool is called, an exposing that as an MCP resource. + +We will walk through the steps in more detail below. + +### Prerequisites + +* You will need to enable "developer mode" in ChatGPT under Settings → Apps & Connectors → Advanced settings in ChatGPT. This currently requires a paid ChatGPT account. +* You need to have `gradio>=6.0` installed with the `mcp` add-on: + +```bash +pip install --upgrade gradio[mcp] +``` + +Now, let's walk through two examples of how you can build build ChatGPT apps with Gradio. + +### Example 1: Letter Counter App + +The first example is an ChatGPT app that counts the occurrence of letters in a word and displays a card with the word and specified letters highlighted, like this: + + + + +So how do we build this? You can find the complete code for the letter counter app [in a single file here](https://github.com/gradio-app/gradio/blob/main/demo/mcp_letter_counter_app/run.py), or follow the steps below: + +1. Start by writing your Python function. In our case, the function is simply a letter counter: + +```py +def letter_counter(word: str, letter: str) -> int: + """ + Count the number of letters in a word or phrase. + + Parameters: + word (str): The word or phrase to count the letters of. + letter (str): The letter to count the occurrences of. + """ + return word.count(letter) +``` + +2. Then, wrap your Python function with a Gradio UI, something along these lines: + +```py +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + word = gr.Textbox(label="Word") + letter = gr.Textbox(label="Letter") + btn = gr.Button("Count Letters") + with gr.Column(): + count = gr.Number(label="Count") + + btn.click(letter_counter, inputs=[word, letter], outputs=count) +``` + +3. Now, launch your Gradio app with the MCP server enabled, i.e. with `mcp_server=True` + +```py + demo.launch(mcp_server=True) +``` + +As covered in [earlier guides](https://www.gradio.app/guides/building-mcp-server-with-gradio), you will now be able to test the tool using any MCP Client, such as the MCP Inspector tool. Test it and confirm that it behaves as you expect. + +4. Create a UI for your ChatGPT app and expose it as a resource. This part requires writing some frontend code and may be unfamiliar at first, but a few examples will help you create an app that works well for your use case. In our case, we'll create a card with HTML, Javascript, and CSS. Inside the card, we'll display the word presented by the user, highlighting each occurrence of the specified letter. Note that we access the user's tool input using `window.openai?.toolInput?.word` and `window.openai?.toolInput?.letter`. The `window.openai` object is automatically inserted by ChatGPT with the data from the user's tool call. This is what the complete function looks like: + +```py +@gr.mcp.resource("ui://widget/app.html", mime_type="text/html+skybridge") +def app_html(): + visual = """ +
+ + """ + return visual +``` + +Note that we've provided a URI for the `gr.mcp.resource` at `ui://widget/app.html`. This is arbitrary, but we'll need to use the same URI later on. We also need to specify the mimetype of the resource to be `mime_type="text/html+skybridge"`. Finally, note that we attached an event listener in the JavaScript for "openai:set_globals", which is generally a good practice as it allows the widget to update whenever a new tool call is triggered. + +5. Create an event in your Gradio app corresponding to the resource function. This is necessary because your Gradio app only picks up MCP tools, resources, prompts, etc. if they are associated with a Gradio event. Typically, the convention is to simply display the code for your MCP resource in a `gr.Code` component, e.g. like this: + +```py + html = gr.Code(language="html", max_lines=20) + + # ... the rest of your Gradio app + + btn.click(app_html, outputs=html) +``` + +6. Add `_meta` attributes to your MCP tool. We need to connect the MCP tool that we created to the UI that we created for our app. We can do this by adding this decorator to our MCP tool function: + +```py +@gr.mcp.tool( + _meta={ + "openai/outputTemplate": "ui://widget/app.html", + "openai/resultCanProduceWidget": True, + "openai/widgetAccessible": True, + } +) +``` + +The key thing to observe is that the `"openai/outputTemplate"` must match the URI of the MCP resource that we created earlier. + +7. Relaunch your Gradio app with `share=True`. This will make it very easy to test within ChatGPT. Note the MCP server URL that is printed to your terminal, e.g. `https://2e879c6066d729b11b.gradio.live/gradio_api/mcp/`. + +```py + demo.launch(share=True, mcp_server=True) +``` + +This will print a public URL that your Gradio app will be running on. + +8. Now, navigate to ChatGPT (https://chat.com/). As mentioned earlier, you need to enable "developer mode" in ChatGPT under Settings → Apps & Connectors → Advanced settings in ChatGPT. Then, navigate to Settings → Apps & Connectors and click the "Create" button. Give your connector a name, a description (optional), and paste in the MCP server URL that was printed to your terminal. Choose "No authentication" and create. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/letter-counter-setup.png) + +And that's it! Once the Connector has been created, you can start prompting it by saying something like, "Use @letter-counter to count the number of r's in Gradio." + +### Example 2: An Image Brightener + +Next, let's see a more complex ChatGPT app for image enhancement. The ChatGPT app includes a "Brighten" button that lets the user call the tool directly from the app UI. + + + +Here's the complete code for this app: + +$code_mcp_image_app + +We won't break down the code in as much detail since many of the pieces are the same. But note the following differences from the earlier example: + +* Calling tools from the widget: The app uses `window.openai.callTool()` to invoke the MCP tool directly from a button click, without requiring ChatGPT to call it: + +```javascript +const result = await window.openai.callTool('power_law_image', { + input_path: imageEl.src +}); +``` + +* Parsing tool call results: The result from `callTool()` contains a `content` array that needs to be parsed to extract data: + +```javascript +function extractImageUrl(data) { + if (data?.content) { + for (const item of data.content) { + if (item.type === 'text' && item.text?.startsWith('Image URL: ')) { + return item.text.substring('Image URL: '.length).trim(); + } + } + } +} +``` + +* Updating UI based on tool results: After calling the tool, the app immediately updates the displayed image with the new result: + +```javascript +const newUrl = extractImageUrl(result); +if (newUrl) imageEl.src = newUrl; +``` + +With these examples, you've seen how to build both simple reactive widgets and more advanced interactive apps that can call tools directly from the UI. By combining Gradio's MCP server capabilities with the OpenAI Apps SDK, it's time to start create richer ChatGPT integrations that enhance the conversational experience with custom visualizations and user interactions! diff --git a/guides/11_other-tutorials/01_using-hugging-face-integrations.md b/guides/11_other-tutorials/01_using-hugging-face-integrations.md new file mode 100644 index 0000000..3616b3d --- /dev/null +++ b/guides/11_other-tutorials/01_using-hugging-face-integrations.md @@ -0,0 +1,134 @@ +# Using Hugging Face Integrations + +Related spaces: https://huggingface.co/spaces/gradio/en2es +Tags: HUB, SPACES, EMBED + +Contributed by Omar Sanseviero 🦙 + +## Introduction + +The Hugging Face Hub is a central platform that has hundreds of thousands of [models](https://huggingface.co/models), [datasets](https://huggingface.co/datasets) and [demos](https://huggingface.co/spaces) (also known as Spaces). + +Gradio has multiple features that make it extremely easy to leverage existing models and Spaces on the Hub. This guide walks through these features. + + +## Demos with the Hugging Face Inference Endpoints + +Hugging Face has a service called [Serverless Inference Endpoints](https://huggingface.co/docs/api-inference/index), which allows you to send HTTP requests to models on the Hub. The API includes a generous free tier, and you can switch to [dedicated Inference Endpoints](https://huggingface.co/inference-endpoints/dedicated) when you want to use it in production. Gradio integrates directly with Serverless Inference Endpoints so that you can create a demo simply by specifying a model's name (e.g. `Helsinki-NLP/opus-mt-en-es`), like this: + +```python +import gradio as gr + +demo = gr.load("Helsinki-NLP/opus-mt-en-es", src="models") + +demo.launch() +``` + +For any Hugging Face model supported in Inference Endpoints, Gradio automatically infers the expected input and output and make the underlying server calls, so you don't have to worry about defining the prediction function. + +Notice that we just put specify the model name and state that the `src` should be `models` (Hugging Face's Model Hub). There is no need to install any dependencies (except `gradio`) since you are not loading the model on your computer. + +You might notice that the first inference takes a little bit longer. This happens since the Inference Endpoints is loading the model in the server. You get some benefits afterward: + +- The inference will be much faster. +- The server caches your requests. +- You get built-in automatic scaling. + +## Hosting your Gradio demos on Spaces + +[Hugging Face Spaces](https://hf.co/spaces) allows anyone to host their Gradio demos freely, and uploading your Gradio demos take a couple of minutes. You can head to [hf.co/new-space](https://huggingface.co/new-space), select the Gradio SDK, create an `app.py` file, and voila! You have a demo you can share with anyone else. To learn more, read [this guide how to host on Hugging Face Spaces using the website](https://huggingface.co/blog/gradio-spaces). + +Alternatively, you can create a Space programmatically, making use of the [huggingface_hub client library](https://huggingface.co/docs/huggingface_hub/index) library. Here's an example: + +```python +from huggingface_hub import ( + create_repo, + get_full_repo_name, + upload_file, +) +create_repo(name=target_space_name, token=hf_token, repo_type="space", space_sdk="gradio") +repo_name = get_full_repo_name(model_id=target_space_name, token=hf_token) +file_url = upload_file( + path_or_fileobj="file.txt", + path_in_repo="app.py", + repo_id=repo_name, + repo_type="space", + token=hf_token, +) +``` + +Here, `create_repo` creates a gradio repo with the target name under a specific account using that account's Write Token. `repo_name` gets the full repo name of the related repo. Finally `upload_file` uploads a file inside the repo with the name `app.py`. + + +## Loading demos from Spaces + +You can also use and remix existing Gradio demos on Hugging Face Spaces. For example, you could take two existing Gradio demos on Spaces and put them as separate tabs and create a new demo. You can run this new demo locally, or upload it to Spaces, allowing endless possibilities to remix and create new demos! + +Here's an example that does exactly that: + +```python +import gradio as gr + +with gr.Blocks() as demo: + with gr.Tab("Translate to Spanish"): + gr.load("gradio/en2es", src="spaces") + with gr.Tab("Translate to French"): + gr.load("abidlabs/en2fr", src="spaces") + +demo.launch() +``` + +Notice that we use `gr.load()`, the same method we used to load models using Inference Endpoints. However, here we specify that the `src` is `spaces` (Hugging Face Spaces). + +Note: loading a Space in this way may result in slight differences from the original Space. In particular, any attributes that apply to the entire Blocks, such as the theme or custom CSS/JS, will not be loaded. You can copy these properties from the Space you are loading into your own `Blocks` object. + +## Demos with the `Pipeline` in `transformers` + +Hugging Face's popular `transformers` library has a very easy-to-use abstraction, [`pipeline()`](https://huggingface.co/docs/transformers/v4.16.2/en/main_classes/pipelines#transformers.pipeline) that handles most of the complex code to offer a simple API for common tasks. By specifying the task and an (optional) model, you can build a demo around an existing model with few lines of Python: + +```python +import gradio as gr + +from transformers import pipeline + +pipe = pipeline("translation", model="Helsinki-NLP/opus-mt-en-es") + +def predict(text): + return pipe(text)[0]["translation_text"] + +demo = gr.Interface( + fn=predict, + inputs='text', + outputs='text', +) + +demo.launch() +``` + +But `gradio` actually makes it even easier to convert a `pipeline` to a demo, simply by using the `gradio.Interface.from_pipeline` methods, which skips the need to specify the input and output components: + +```python +from transformers import pipeline +import gradio as gr + +pipe = pipeline("translation", model="Helsinki-NLP/opus-mt-en-es") + +demo = gr.Interface.from_pipeline(pipe) +demo.launch() +``` + +The previous code produces the following interface, which you can try right here in your browser: + + + + +## Recap + +That's it! Let's recap the various ways Gradio and Hugging Face work together: + +1. You can build a demo around Inference Endpoints without having to load the model, by using `gr.load()`. +2. You host your Gradio demo on Hugging Face Spaces, either using the GUI or entirely in Python. +3. You can load demos from Hugging Face Spaces to remix and create new Gradio demos using `gr.load()`. +4. You can convert a `transformers` pipeline into a Gradio demo using `from_pipeline()`. + +🤗 diff --git a/guides/11_other-tutorials/Gradio-and-Comet.md b/guides/11_other-tutorials/Gradio-and-Comet.md new file mode 100644 index 0000000..cc809bb --- /dev/null +++ b/guides/11_other-tutorials/Gradio-and-Comet.md @@ -0,0 +1,270 @@ +# Using Gradio and Comet + +Tags: COMET, SPACES +Contributed by the Comet team + +## Introduction + +In this guide we will demonstrate some of the ways you can use Gradio with Comet. We will cover the basics of using Comet with Gradio and show you some of the ways that you can leverage Gradio's advanced features such as [Embedding with iFrames](https://www.gradio.app/guides/sharing-your-app/#embedding-with-iframes) and [State](https://www.gradio.app/docs/#state) to build some amazing model evaluation workflows. + +Here is a list of the topics covered in this guide. + +1. Logging Gradio UI's to your Comet Experiments +2. Embedding Gradio Applications directly into your Comet Projects +3. Embedding Hugging Face Spaces directly into your Comet Projects +4. Logging Model Inferences from your Gradio Application to Comet + +## What is Comet? + +[Comet](https://www.comet.com?utm_source=gradio&utm_medium=referral&utm_campaign=gradio-integration&utm_content=gradio-docs) is an MLOps Platform that is designed to help Data Scientists and Teams build better models faster! Comet provides tooling to Track, Explain, Manage, and Monitor your models in a single place! It works with Jupyter Notebooks and Scripts and most importantly it's 100% free! + +## Setup + +First, install the dependencies needed to run these examples + +```shell +pip install comet_ml torch torchvision transformers gradio shap requests Pillow +``` + +Next, you will need to [sign up for a Comet Account](https://www.comet.com/signup?utm_source=gradio&utm_medium=referral&utm_campaign=gradio-integration&utm_content=gradio-docs). Once you have your account set up, [grab your API Key](https://www.comet.com/docs/v2/guides/getting-started/quickstart/#get-an-api-key?utm_source=gradio&utm_medium=referral&utm_campaign=gradio-integration&utm_content=gradio-docs) and configure your Comet credentials + +If you're running these examples as a script, you can either export your credentials as environment variables + +```shell +export COMET_API_KEY="" +export COMET_WORKSPACE="" +export COMET_PROJECT_NAME="" +``` + +or set them in a `.comet.config` file in your working directory. You file should be formatted in the following way. + +```shell +[comet] +api_key= +workspace= +project_name= +``` + +If you are using the provided Colab Notebooks to run these examples, please run the cell with the following snippet before starting the Gradio UI. Running this cell allows you to interactively add your API key to the notebook. + +```python +import comet_ml +comet_ml.init() +``` + +## 1. Logging Gradio UI's to your Comet Experiments + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/comet-ml/comet-examples/blob/master/integrations/model-evaluation/gradio/notebooks/Gradio_and_Comet.ipynb) + +In this example, we will go over how to log your Gradio Applications to Comet and interact with them using the Gradio Custom Panel. + +Let's start by building a simple Image Classification example using `resnet18`. + +```python +import comet_ml + +import requests +import torch +from PIL import Image +from torchvision import transforms + +torch.hub.download_url_to_file("https://github.com/pytorch/hub/raw/master/images/dog.jpg", "dog.jpg") + +if torch.cuda.is_available(): + device = "cuda" +else: + device = "cpu" + +model = torch.hub.load("pytorch/vision:v0.6.0", "resnet18", pretrained=True).eval() +model = model.to(device) + +# Download human-readable labels for ImageNet. +response = requests.get("https://git.io/JJkYN") +labels = response.text.split("\n") + + +def predict(inp): + inp = Image.fromarray(inp.astype("uint8"), "RGB") + inp = transforms.ToTensor()(inp).unsqueeze(0) + with torch.no_grad(): + prediction = torch.nn.functional.softmax(model(inp.to(device))[0], dim=0) + return {labels[i]: float(prediction[i]) for i in range(1000)} + + +inputs = gr.Image() +outputs = gr.Label(num_top_classes=3) + +io = gr.Interface( + fn=predict, inputs=inputs, outputs=outputs, examples=["dog.jpg"] +) +io.launch(inline=False, share=True) + +experiment = comet_ml.Experiment() +experiment.add_tag("image-classifier") + +io.integrate(comet_ml=experiment) +``` + +The last line in this snippet will log the URL of the Gradio Application to your Comet Experiment. You can find the URL in the Text Tab of your Experiment. + + + +Add the Gradio Panel to your Experiment to interact with your application. + + + +## 2. Embedding Gradio Applications directly into your Comet Projects + + + +If you are permanently hosting your Gradio application, you can embed the UI using the Gradio Panel Extended custom Panel. + +Go to your Comet Project page, and head over to the Panels tab. Click the `+ Add` button to bring up the Panels search page. + +adding-panels + +Next, search for Gradio Panel Extended in the Public Panels section and click `Add`. + +gradio-panel-extended + +Once you have added your Panel, click `Edit` to access to the Panel Options page and paste in the URL of your Gradio application. + +![Edit-Gradio-Panel-Options](https://user-images.githubusercontent.com/7529846/214573001-23814b5a-ca65-4ace-a8a5-b27cdda70f7a.gif) + +Edit-Gradio-Panel-URL + +## 3. Embedding Hugging Face Spaces directly into your Comet Projects + + + +You can also embed Gradio Applications that are hosted on Hugging Faces Spaces into your Comet Projects using the Hugging Face Spaces Panel. + +Go to your Comet Project page, and head over to the Panels tab. Click the `+ Add` button to bring up the Panels search page. Next, search for the Hugging Face Spaces Panel in the Public Panels section and click `Add`. + +huggingface-spaces-panel + +Once you have added your Panel, click Edit to access to the Panel Options page and paste in the path of your Hugging Face Space e.g. `pytorch/ResNet` + +Edit-HF-Space + +## 4. Logging Model Inferences to Comet + + + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/comet-ml/comet-examples/blob/master/integrations/model-evaluation/gradio/notebooks/Logging_Model_Inferences_with_Comet_and_Gradio.ipynb) + +In the previous examples, we demonstrated the various ways in which you can interact with a Gradio application through the Comet UI. Additionally, you can also log model inferences, such as SHAP plots, from your Gradio application to Comet. + +In the following snippet, we're going to log inferences from a Text Generation model. We can persist an Experiment across multiple inference calls using Gradio's [State](https://www.gradio.app/docs/#state) object. This will allow you to log multiple inferences from a model to a single Experiment. + +```python +import comet_ml +import gradio as gr +import shap +import torch +from transformers import AutoModelForCausalLM, AutoTokenizer + +if torch.cuda.is_available(): + device = "cuda" +else: + device = "cpu" + +MODEL_NAME = "gpt2" + +model = AutoModelForCausalLM.from_pretrained(MODEL_NAME) + +# set model decoder to true +model.config.is_decoder = True +# set text-generation params under task_specific_params +model.config.task_specific_params["text-generation"] = { + "do_sample": True, + "max_length": 50, + "temperature": 0.7, + "top_k": 50, + "no_repeat_ngram_size": 2, +} +model = model.to(device) + +tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) +explainer = shap.Explainer(model, tokenizer) + + +def start_experiment(): + """Returns an APIExperiment object that is thread safe + and can be used to log inferences to a single Experiment + """ + try: + api = comet_ml.API() + workspace = api.get_default_workspace() + project_name = comet_ml.config.get_config()["comet.project_name"] + + experiment = comet_ml.APIExperiment( + workspace=workspace, project_name=project_name + ) + experiment.log_other("Created from", "gradio-inference") + + message = f"Started Experiment: [{experiment.name}]({experiment.url})" + + return (experiment, message) + + except Exception as e: + return None, None + + +def predict(text, state, message): + experiment = state + + shap_values = explainer([text]) + plot = shap.plots.text(shap_values, display=False) + + if experiment is not None: + experiment.log_other("message", message) + experiment.log_html(plot) + + return plot + + +with gr.Blocks() as demo: + start_experiment_btn = gr.Button("Start New Experiment") + experiment_status = gr.Markdown() + + # Log a message to the Experiment to provide more context + experiment_message = gr.Textbox(label="Experiment Message") + experiment = gr.State() + + input_text = gr.Textbox(label="Input Text", lines=5, interactive=True) + submit_btn = gr.Button("Submit") + + output = gr.HTML(interactive=True) + + start_experiment_btn.click( + start_experiment, outputs=[experiment, experiment_status] + ) + submit_btn.click( + predict, inputs=[input_text, experiment, experiment_message], outputs=[output] + ) +``` + +Inferences from this snippet will be saved in the HTML tab of your experiment. + + + +## Conclusion + +We hope you found this guide useful and that it provides some inspiration to help you build awesome model evaluation workflows with Comet and Gradio. + +## How to contribute Gradio demos on HF spaces on the Comet organization + +- Create an account on Hugging Face [here](https://huggingface.co/join). +- Add Gradio Demo under your username, see this [course](https://huggingface.co/course/chapter9/4?fw=pt) for setting up Gradio Demo on Hugging Face. +- Request to join the Comet organization [here](https://huggingface.co/Comet). + +## Additional Resources + +- [Comet Documentation](https://www.comet.com/docs/v2/?utm_source=gradio&utm_medium=referral&utm_campaign=gradio-integration&utm_content=gradio-docs) diff --git a/guides/11_other-tutorials/Gradio-and-ONNX-on-Hugging-Face.md b/guides/11_other-tutorials/Gradio-and-ONNX-on-Hugging-Face.md new file mode 100644 index 0000000..9893285 --- /dev/null +++ b/guides/11_other-tutorials/Gradio-and-ONNX-on-Hugging-Face.md @@ -0,0 +1,140 @@ +# Gradio and ONNX on Hugging Face + +Related spaces: https://huggingface.co/spaces/onnx/EfficientNet-Lite4 +Tags: ONNX, SPACES +Contributed by Gradio and the ONNX team + +## Introduction + +In this Guide, we'll walk you through: + +- Introduction of ONNX, ONNX model zoo, Gradio, and Hugging Face Spaces +- How to setup a Gradio demo for EfficientNet-Lite4 +- How to contribute your own Gradio demos for the ONNX organization on Hugging Face + +Here's an [example](https://onnx-efficientnet-lite4.hf.space/) of an ONNX model. + +## What is the ONNX Model Zoo? + +Open Neural Network Exchange ([ONNX](https://onnx.ai/)) is an open standard format for representing machine learning models. ONNX is supported by a community of partners who have implemented it in many frameworks and tools. For example, if you have trained a model in TensorFlow or PyTorch, you can convert it to ONNX easily, and from there run it on a variety of devices using an engine/compiler like ONNX Runtime. + +The [ONNX Model Zoo](https://github.com/onnx/models) is a collection of pre-trained, state-of-the-art models in the ONNX format contributed by community members. Accompanying each model are Jupyter notebooks for model training and running inference with the trained model. The notebooks are written in Python and include links to the training dataset as well as references to the original paper that describes the model architecture. + +## What are Hugging Face Spaces & Gradio? + +### Gradio + +Gradio lets users demo their machine learning models as a web app all in python code. Gradio wraps a python function into a user interface and the demos can be launched inside jupyter notebooks, colab notebooks, as well as embedded in your own website and hosted on Hugging Face Spaces for free. + +Get started [here](https://gradio.app/getting_started) + +### Hugging Face Spaces + +Hugging Face Spaces is a free hosting option for Gradio demos. Spaces comes with 3 SDK options: Gradio, Streamlit and Static HTML demos. Spaces can be public or private and the workflow is similar to github repos. There are over 2000+ spaces currently on Hugging Face. Learn more about spaces [here](https://huggingface.co/spaces/launch). + +### Hugging Face Models + +Hugging Face Model Hub also supports ONNX models and ONNX models can be filtered through the [ONNX tag](https://huggingface.co/models?library=onnx&sort=downloads) + +## How did Hugging Face help the ONNX Model Zoo? + +There are a lot of Jupyter notebooks in the ONNX Model Zoo for users to test models. Previously, users needed to download the models themselves and run those notebooks locally for testing. With Hugging Face, the testing process can be much simpler and more user-friendly. Users can easily try certain ONNX Model Zoo model on Hugging Face Spaces and run a quick demo powered by Gradio with ONNX Runtime, all on cloud without downloading anything locally. Note, there are various runtimes for ONNX, e.g., [ONNX Runtime](https://github.com/microsoft/onnxruntime), [MXNet](https://github.com/apache/incubator-mxnet). + +## What is the role of ONNX Runtime? + +ONNX Runtime is a cross-platform inference and training machine-learning accelerator. It makes live Gradio demos with ONNX Model Zoo model on Hugging Face possible. + +ONNX Runtime inference can enable faster customer experiences and lower costs, supporting models from deep learning frameworks such as PyTorch and TensorFlow/Keras as well as classical machine learning libraries such as scikit-learn, LightGBM, XGBoost, etc. ONNX Runtime is compatible with different hardware, drivers, and operating systems, and provides optimal performance by leveraging hardware accelerators where applicable alongside graph optimizations and transforms. For more information please see the [official website](https://onnxruntime.ai/). + +## Setting up a Gradio Demo for EfficientNet-Lite4 + +EfficientNet-Lite 4 is the largest variant and most accurate of the set of EfficientNet-Lite models. It is an integer-only quantized model that produces the highest accuracy of all of the EfficientNet models. It achieves 80.4% ImageNet top-1 accuracy, while still running in real-time (e.g. 30ms/image) on a Pixel 4 CPU. To learn more read the [model card](https://github.com/onnx/models/tree/main/vision/classification/efficientnet-lite4) + +Here we walk through setting up a example demo for EfficientNet-Lite4 using Gradio + +First we import our dependencies and download and load the efficientnet-lite4 model from the onnx model zoo. Then load the labels from the labels_map.txt file. We then setup our preprocessing functions, load the model for inference, and setup the inference function. Finally, the inference function is wrapped into a gradio interface for a user to interact with. See the full code below. + +```python +import numpy as np +import math +import matplotlib.pyplot as plt +import cv2 +import json +import gradio as gr +from huggingface_hub import hf_hub_download +from onnx import hub +import onnxruntime as ort + +# loads ONNX model from ONNX Model Zoo +model = hub.load("efficientnet-lite4") +# loads the labels text file +labels = json.load(open("labels_map.txt", "r")) + +# sets image file dimensions to 224x224 by resizing and cropping image from center +def pre_process_edgetpu(img, dims): + output_height, output_width, _ = dims + img = resize_with_aspectratio(img, output_height, output_width, inter_pol=cv2.INTER_LINEAR) + img = center_crop(img, output_height, output_width) + img = np.asarray(img, dtype='float32') + # converts jpg pixel value from [0 - 255] to float array [-1.0 - 1.0] + img -= [127.0, 127.0, 127.0] + img /= [128.0, 128.0, 128.0] + return img + +# resizes the image with a proportional scale +def resize_with_aspectratio(img, out_height, out_width, scale=87.5, inter_pol=cv2.INTER_LINEAR): + height, width, _ = img.shape + new_height = int(100. * out_height / scale) + new_width = int(100. * out_width / scale) + if height > width: + w = new_width + h = int(new_height * height / width) + else: + h = new_height + w = int(new_width * width / height) + img = cv2.resize(img, (w, h), interpolation=inter_pol) + return img + +# crops the image around the center based on given height and width +def center_crop(img, out_height, out_width): + height, width, _ = img.shape + left = int((width - out_width) / 2) + right = int((width + out_width) / 2) + top = int((height - out_height) / 2) + bottom = int((height + out_height) / 2) + img = img[top:bottom, left:right] + return img + + +sess = ort.InferenceSession(model) + +def inference(img): + img = cv2.imread(img) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + img = pre_process_edgetpu(img, (224, 224, 3)) + + img_batch = np.expand_dims(img, axis=0) + + results = sess.run(["Softmax:0"], {"images:0": img_batch})[0] + result = reversed(results[0].argsort()[-5:]) + resultdic = {} + for r in result: + resultdic[labels[str(r)]] = float(results[0][r]) + return resultdic + +title = "EfficientNet-Lite4" +description = "EfficientNet-Lite 4 is the largest variant and most accurate of the set of EfficientNet-Lite model. It is an integer-only quantized model that produces the highest accuracy of all of the EfficientNet models. It achieves 80.4% ImageNet top-1 accuracy, while still running in real-time (e.g. 30ms/image) on a Pixel 4 CPU." +examples = [['catonnx.jpg']] +gr.Interface(inference, gr.Image(type="filepath"), "label", title=title, description=description, examples=examples).launch() +``` + +## How to contribute Gradio demos on HF spaces using ONNX models + +- Add model to the [onnx model zoo](https://github.com/onnx/models/blob/main/.github/PULL_REQUEST_TEMPLATE.md) +- Create an account on Hugging Face [here](https://huggingface.co/join). +- See list of models left to add to ONNX organization, please refer to the table with the [Models list](https://github.com/onnx/models#models) +- Add Gradio Demo under your username, see this [blog post](https://huggingface.co/blog/gradio-spaces) for setting up Gradio Demo on Hugging Face. +- Request to join ONNX Organization [here](https://huggingface.co/onnx). +- Once approved transfer model from your username to ONNX organization +- Add a badge for model in model table, see examples in [Models list](https://github.com/onnx/models#models) diff --git a/guides/11_other-tutorials/Gradio-and-Wandb-Integration.md b/guides/11_other-tutorials/Gradio-and-Wandb-Integration.md new file mode 100644 index 0000000..0c3d233 --- /dev/null +++ b/guides/11_other-tutorials/Gradio-and-Wandb-Integration.md @@ -0,0 +1,276 @@ +# Gradio and W&B Integration + +Related spaces: https://huggingface.co/spaces/akhaliq/JoJoGAN +Tags: WANDB, SPACES +Contributed by Gradio team + +## Introduction + +In this Guide, we'll walk you through: + +- Introduction of Gradio, and Hugging Face Spaces, and Wandb +- How to setup a Gradio demo using the Wandb integration for JoJoGAN +- How to contribute your own Gradio demos after tracking your experiments on wandb to the Wandb organization on Hugging Face + + +## What is Wandb? + +Weights and Biases (W&B) allows data scientists and machine learning scientists to track their machine learning experiments at every stage, from training to production. Any metric can be aggregated over samples and shown in panels in a customizable and searchable dashboard, like below: + +Screen Shot 2022-08-01 at 5 54 59 PM + +## What are Hugging Face Spaces & Gradio? + +### Gradio + +Gradio lets users demo their machine learning models as a web app, all in a few lines of Python. Gradio wraps any Python function (such as a machine learning model's inference function) into a user interface and the demos can be launched inside jupyter notebooks, colab notebooks, as well as embedded in your own website and hosted on Hugging Face Spaces for free. + +Get started [here](https://gradio.app/getting_started) + +### Hugging Face Spaces + +Hugging Face Spaces is a free hosting option for Gradio demos. Spaces comes with 3 SDK options: Gradio, Streamlit and Static HTML demos. Spaces can be public or private and the workflow is similar to github repos. There are over 2000+ spaces currently on Hugging Face. Learn more about spaces [here](https://huggingface.co/spaces/launch). + +## Setting up a Gradio Demo for JoJoGAN + +Now, let's walk you through how to do this on your own. We'll make the assumption that you're new to W&B and Gradio for the purposes of this tutorial. + +Let's get started! + +1. Create a W&B account + + Follow [these quick instructions](https://app.wandb.ai/login) to create your free account if you don’t have one already. It shouldn't take more than a couple minutes. Once you're done (or if you've already got an account), next, we'll run a quick colab. + +2. Open Colab Install Gradio and W&B + + We'll be following along with the colab provided in the JoJoGAN repo with some minor modifications to use Wandb and Gradio more effectively. + + [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mchong6/JoJoGAN/blob/main/stylize.ipynb) + + Install Gradio and Wandb at the top: + + ```sh + pip install gradio wandb + ``` + +3. Finetune StyleGAN and W&B experiment tracking + + This next step will open a W&B dashboard to track your experiments and a gradio panel showing pretrained models to choose from a drop down menu from a Gradio Demo hosted on Huggingface Spaces. Here's the code you need for that: + + ```python + alpha = 1.0 + alpha = 1-alpha + + preserve_color = True + num_iter = 100 + log_interval = 50 + + samples = [] + column_names = ["Reference (y)", "Style Code(w)", "Real Face Image(x)"] + + wandb.init(project="JoJoGAN") + config = wandb.config + config.num_iter = num_iter + config.preserve_color = preserve_color + wandb.log( + {"Style reference": [wandb.Image(transforms.ToPILImage()(target_im))]}, + step=0) + + # load discriminator for perceptual loss + discriminator = Discriminator(1024, 2).eval().to(device) + ckpt = torch.load('models/stylegan2-ffhq-config-f.pt', map_location=lambda storage, loc: storage) + discriminator.load_state_dict(ckpt["d"], strict=False) + + # reset generator + del generator + generator = deepcopy(original_generator) + + g_optim = optim.Adam(generator.parameters(), lr=2e-3, betas=(0, 0.99)) + + # Which layers to swap for generating a family of plausible real images -> fake image + if preserve_color: + id_swap = [9,11,15,16,17] + else: + id_swap = list(range(7, generator.n_latent)) + + for idx in tqdm(range(num_iter)): + mean_w = generator.get_latent(torch.randn([latents.size(0), latent_dim]).to(device)).unsqueeze(1).repeat(1, generator.n_latent, 1) + in_latent = latents.clone() + in_latent[:, id_swap] = alpha*latents[:, id_swap] + (1-alpha)*mean_w[:, id_swap] + + img = generator(in_latent, input_is_latent=True) + + with torch.no_grad(): + real_feat = discriminator(targets) + fake_feat = discriminator(img) + + loss = sum([F.l1_loss(a, b) for a, b in zip(fake_feat, real_feat)])/len(fake_feat) + + wandb.log({"loss": loss}, step=idx) + if idx % log_interval == 0: + generator.eval() + my_sample = generator(my_w, input_is_latent=True) + generator.train() + my_sample = transforms.ToPILImage()(utils.make_grid(my_sample, normalize=True, range=(-1, 1))) + wandb.log( + {"Current stylization": [wandb.Image(my_sample)]}, + step=idx) + table_data = [ + wandb.Image(transforms.ToPILImage()(target_im)), + wandb.Image(img), + wandb.Image(my_sample), + ] + samples.append(table_data) + + g_optim.zero_grad() + loss.backward() + g_optim.step() + + out_table = wandb.Table(data=samples, columns=column_names) + wandb.log({"Current Samples": out_table}) + ``` +4. Save, Download, and Load Model + + Here's how to save and download your model. + + ```python + from PIL import Image + import torch + torch.backends.cudnn.benchmark = True + from torchvision import transforms, utils + from util import * + import math + import random + import numpy as np + from torch import nn, autograd, optim + from torch.nn import functional as F + from tqdm import tqdm + import lpips + from model import * + from e4e_projection import projection as e4e_projection + + from copy import deepcopy + import imageio + + import os + import sys + import torchvision.transforms as transforms + from argparse import Namespace + from e4e.models.psp import pSp + from util import * + from huggingface_hub import hf_hub_download + from google.colab import files + + torch.save({"g": generator.state_dict()}, "your-model-name.pt") + + files.download('your-model-name.pt') + + latent_dim = 512 + device="cuda" + model_path_s = hf_hub_download(repo_id="akhaliq/jojogan-stylegan2-ffhq-config-f", filename="stylegan2-ffhq-config-f.pt") + original_generator = Generator(1024, latent_dim, 8, 2).to(device) + ckpt = torch.load(model_path_s, map_location=lambda storage, loc: storage) + original_generator.load_state_dict(ckpt["g_ema"], strict=False) + mean_latent = original_generator.mean_latent(10000) + + generator = deepcopy(original_generator) + + ckpt = torch.load("/content/JoJoGAN/your-model-name.pt", map_location=lambda storage, loc: storage) + generator.load_state_dict(ckpt["g"], strict=False) + generator.eval() + + plt.rcParams['figure.dpi'] = 150 + + transform = transforms.Compose( + [ + transforms.Resize((1024, 1024)), + transforms.ToTensor(), + transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), + ] + ) + + def inference(img): + img.save('out.jpg') + aligned_face = align_face('out.jpg') + + my_w = e4e_projection(aligned_face, "out.pt", device).unsqueeze(0) + with torch.no_grad(): + my_sample = generator(my_w, input_is_latent=True) + + npimage = my_sample[0].cpu().permute(1, 2, 0).detach().numpy() + imageio.imwrite('filename.jpeg', npimage) + return 'filename.jpeg' + ```` + +5. Build a Gradio Demo + + ```python + import gradio as gr + + title = "JoJoGAN" + description = "Gradio Demo for JoJoGAN: One Shot Face Stylization. To use it, simply upload your image, or click one of the examples to load them. Read more at the links below." + + demo = gr.Interface( + inference, + gr.Image(type="pil"), + gr.Image(type="file"), + title=title, + description=description + ) + + demo.launch(share=True) + ``` + +6. Integrate Gradio into your W&B Dashboard + + The last step—integrating your Gradio demo with your W&B dashboard—is just one extra line: + + ```python + demo.integrate(wandb=wandb) + ``` + + Once you call integrate, a demo will be created and you can integrate it into your dashboard or report. + + Outside of W&B with Web components, using the `gradio-app` tags, anyone can embed Gradio demos on HF spaces directly into their blogs, websites, documentation, etc.: + + ```html + + ``` + +7. (Optional) Embed W&B plots in your Gradio App + + It's also possible to embed W&B plots within Gradio apps. To do so, you can create a W&B Report of your plots and + embed them within your Gradio app within a `gr.HTML` block. + + The Report will need to be public and you will need to wrap the URL within an iFrame like this: + + ```python + import gradio as gr + + def wandb_report(url): + iframe = f' +
+ +Gradio comes with a set of prebuilt themes which you can load from `gr.themes.*`. These are: + + +* `gr.themes.Base()` - the `"base"` theme sets the primary color to blue but otherwise has minimal styling, making it particularly useful as a base for creating new, custom themes. +* `gr.themes.Default()` - the `"default"` Gradio 5 theme, with a vibrant orange primary color and gray secondary color. +* `gr.themes.Origin()` - the `"origin"` theme is most similar to Gradio 4 styling. Colors, especially in light mode, are more subdued than the Gradio 5 default theme. +* `gr.themes.Citrus()` - the `"citrus"` theme uses a yellow primary color, highlights form elements that are in focus, and includes fun 3D effects when buttons are clicked. +* `gr.themes.Monochrome()` - the `"monochrome"` theme uses a black primary and white secondary color, and uses serif-style fonts, giving the appearance of a black-and-white newspaper. +* `gr.themes.Soft()` - the `"soft"` theme uses a purple primary color and white secondary color. It also increases the border radius around buttons and form elements and highlights labels. +* `gr.themes.Glass()` - the `"glass"` theme has a blue primary color and a transclucent gray secondary color. The theme also uses vertical gradients to create a glassy effect. +* `gr.themes.Ocean()` - the `"ocean"` theme has a blue-green primary color and gray secondary color. The theme also uses horizontal gradients, especially for buttons and some form elements. + + +Each of these themes set values for hundreds of CSS variables. You can use prebuilt themes as a starting point for your own custom themes, or you can create your own themes from scratch. Let's take a look at each approach. + +## Using the Theme Builder + +The easiest way to build a theme is using the Theme Builder. To launch the Theme Builder locally, run the following code: + +```python +import gradio as gr + +gr.themes.builder() +``` + +$demo_theme_builder + +You can use the Theme Builder running on Spaces above, though it runs much faster when you launch it locally via `gr.themes.builder()`. + +As you edit the values in the Theme Builder, the app will preview updates in real time. You can download the code to generate the theme you've created so you can use it in any Gradio app. + +In the rest of the guide, we will cover building themes programmatically. + +## Extending Themes via the Constructor + +Although each theme has hundreds of CSS variables, the values for most these variables are drawn from 8 core variables which can be set through the constructor of each prebuilt theme. Modifying these 8 arguments allows you to quickly change the look and feel of your app. + +### Core Colors + +The first 3 constructor arguments set the colors of the theme and are `gradio.themes.Color` objects. Internally, these Color objects hold brightness values for the palette of a single hue, ranging from 50, 100, 200..., 800, 900, 950. Other CSS variables are derived from these 3 colors. + +The 3 color constructor arguments are: + +- `primary_hue`: This is the color draws attention in your theme. In the default theme, this is set to `gradio.themes.colors.orange`. +- `secondary_hue`: This is the color that is used for secondary elements in your theme. In the default theme, this is set to `gradio.themes.colors.blue`. +- `neutral_hue`: This is the color that is used for text and other neutral elements in your theme. In the default theme, this is set to `gradio.themes.colors.gray`. + +You could modify these values using their string shortcuts, such as + +```python +with gr.Blocks() as demo: + ... # your code here +demo.launch(theme=gr.themes.Default(primary_hue="red", secondary_hue="pink")) + ... +``` + +or you could use the `Color` objects directly, like this: + +```python +with gr.Blocks() as demo: + ... # your code here +demo.launch(theme=gr.themes.Default(primary_hue=gr.themes.colors.red, secondary_hue=gr.themes.colors.pink)) +``` + +
+ +
+ +Predefined colors are: + +- `slate` +- `gray` +- `zinc` +- `neutral` +- `stone` +- `red` +- `orange` +- `amber` +- `yellow` +- `lime` +- `green` +- `emerald` +- `teal` +- `cyan` +- `sky` +- `blue` +- `indigo` +- `violet` +- `purple` +- `fuchsia` +- `pink` +- `rose` + +You could also create your own custom `Color` objects and pass them in. + +### Core Sizing + +The next 3 constructor arguments set the sizing of the theme and are `gradio.themes.Size` objects. Internally, these Size objects hold pixel size values that range from `xxs` to `xxl`. Other CSS variables are derived from these 3 sizes. + +- `spacing_size`: This sets the padding within and spacing between elements. In the default theme, this is set to `gradio.themes.sizes.spacing_md`. +- `radius_size`: This sets the roundedness of corners of elements. In the default theme, this is set to `gradio.themes.sizes.radius_md`. +- `text_size`: This sets the font size of text. In the default theme, this is set to `gradio.themes.sizes.text_md`. + +You could modify these values using their string shortcuts, such as + +```python +with gr.Blocks() as demo: + ... # your code here +demo.launch(theme=gr.themes.Default(spacing_size="sm", radius_size="none")) + ... +``` + +or you could use the `Size` objects directly, like this: + +```python +with gr.Blocks() as demo: + ... # your code here +demo.launch(theme=gr.themes.Default(spacing_size=gr.themes.sizes.spacing_sm, radius_size=gr.themes.sizes.radius_none)) + ... +``` + +
+ +
+ +The predefined size objects are: + +- `radius_none` +- `radius_sm` +- `radius_md` +- `radius_lg` +- `spacing_sm` +- `spacing_md` +- `spacing_lg` +- `text_sm` +- `text_md` +- `text_lg` + +You could also create your own custom `Size` objects and pass them in. + +### Core Fonts + +The final 2 constructor arguments set the fonts of the theme. You can pass a list of fonts to each of these arguments to specify fallbacks. If you provide a string, it will be loaded as a system font. If you provide a `gradio.themes.GoogleFont`, the font will be loaded from Google Fonts. + +- `font`: This sets the primary font of the theme. In the default theme, this is set to `gradio.themes.GoogleFont("IBM Plex Sans")`. +- `font_mono`: This sets the monospace font of the theme. In the default theme, this is set to `gradio.themes.GoogleFont("IBM Plex Mono")`. + +You could modify these values such as the following: + +```python +with gr.Blocks() as demo: + ... # your code here +demo.launch(theme=gr.themes.Default(font=[gr.themes.GoogleFont("Inconsolata"), "Arial", "sans-serif"])) + ... +``` + +
+ +
+ +### Custom CSS + +For styling beyond what theme variables provide, you can add custom CSS via the `custom_css` attribute. This CSS is bundled with the theme, so it will be included when you upload or download themes from the Hub. + +```python +theme = gr.themes.Default() +theme.custom_css = """ +button.primary { + background: linear-gradient(135deg, var(--primary-400), var(--primary-600)); + transition: transform 0.15s ease, box-shadow 0.15s ease; +} +button.primary:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px color-mix(in srgb, var(--primary-500) 40%, transparent); +} +""" + +with gr.Blocks(theme=theme) as demo: + gr.Textbox(label="Input") + gr.Button("Submit", variant="primary") +demo.launch() +``` + +## Extending Themes via `.set()` + +You can also modify the values of CSS variables after the theme has been loaded. To do so, use the `.set()` method of the theme object to get access to the CSS variables. For example: + +```python +theme = gr.themes.Default(primary_hue="blue").set( + loader_color="#FF0000", + slider_color="#FF0000", +) + +with gr.Blocks() as demo: + ... # your code here +demo.launch(theme=theme) +``` + +In the example above, we've set the `loader_color` and `slider_color` variables to `#FF0000`, despite the overall `primary_color` using the blue color palette. You can set any CSS variable that is defined in the theme in this manner. + +Your IDE type hinting should help you navigate these variables. Since there are so many CSS variables, let's take a look at how these variables are named and organized. + +### CSS Variable Naming Conventions + +CSS variable names can get quite long, like `button_primary_background_fill_hover_dark`! However they follow a common naming convention that makes it easy to understand what they do and to find the variable you're looking for. Separated by underscores, the variable name is made up of: + +1. The target element, such as `button`, `slider`, or `block`. +2. The target element type or sub-element, such as `button_primary`, or `block_label`. +3. The property, such as `button_primary_background_fill`, or `block_label_border_width`. +4. Any relevant state, such as `button_primary_background_fill_hover`. +5. If the value is different in dark mode, the suffix `_dark`. For example, `input_border_color_focus_dark`. + +Of course, many CSS variable names are shorter than this, such as `table_border_color`, or `input_shadow`. + +### CSS Variable Organization + +Though there are hundreds of CSS variables, they do not all have to have individual values. They draw their values by referencing a set of core variables and referencing each other. This allows us to only have to modify a few variables to change the look and feel of the entire theme, while also getting finer control of individual elements that we may want to modify. + +#### Referencing Core Variables + +To reference one of the core constructor variables, precede the variable name with an asterisk. To reference a core color, use the `*primary_`, `*secondary_`, or `*neutral_` prefix, followed by the brightness value. For example: + +```python +theme = gr.themes.Default(primary_hue="blue").set( + button_primary_background_fill="*primary_200", + button_primary_background_fill_hover="*primary_300", +) +``` + +In the example above, we've set the `button_primary_background_fill` and `button_primary_background_fill_hover` variables to `*primary_200` and `*primary_300`. These variables will be set to the 200 and 300 brightness values of the blue primary color palette, respectively. + +Similarly, to reference a core size, use the `*spacing_`, `*radius_`, or `*text_` prefix, followed by the size value. For example: + +```python +theme = gr.themes.Default(radius_size="md").set( + button_primary_border_radius="*radius_xl", +) +``` + +In the example above, we've set the `button_primary_border_radius` variable to `*radius_xl`. This variable will be set to the `xl` setting of the medium radius size range. + +#### Referencing Other Variables + +Variables can also reference each other. For example, look at the example below: + +```python +theme = gr.themes.Default().set( + button_primary_background_fill="#FF0000", + button_primary_background_fill_hover="#FF0000", + button_primary_border="#FF0000", +) +``` + +Having to set these values to a common color is a bit tedious. Instead, we can reference the `button_primary_background_fill` variable in the `button_primary_background_fill_hover` and `button_primary_border` variables, using a `*` prefix. + +```python +theme = gr.themes.Default().set( + button_primary_background_fill="#FF0000", + button_primary_background_fill_hover="*button_primary_background_fill", + button_primary_border="*button_primary_background_fill", +) +``` + +Now, if we change the `button_primary_background_fill` variable, the `button_primary_background_fill_hover` and `button_primary_border` variables will automatically update as well. + +This is particularly useful if you intend to share your theme - it makes it easy to modify the theme without having to change every variable. + +Note that dark mode variables automatically reference each other. For example: + +```python +theme = gr.themes.Default().set( + button_primary_background_fill="#FF0000", + button_primary_background_fill_dark="#AAAAAA", + button_primary_border="*button_primary_background_fill", + button_primary_border_dark="*button_primary_background_fill_dark", +) +``` + +`button_primary_border_dark` will draw its value from `button_primary_background_fill_dark`, because dark mode always draw from the dark version of the variable. + +## CSS Variables Reference + +For a full list of all available CSS variables, see the [CSS Variables Reference](/main/guides/css-variables-reference). + +## Creating a Full Theme + +Let's say you want to create a theme from scratch! We'll go through it step by step - you can also see the source of prebuilt themes in the gradio source repo for reference - [here's the source](https://github.com/gradio-app/gradio/blob/main/gradio/themes/monochrome.py) for the Monochrome theme. + +Our new theme class will inherit from `gradio.themes.Base`, a theme that sets a lot of convenient defaults. Let's make a simple demo that creates a dummy theme called Seafoam, and make a simple app that uses it. + +$code_theme_new_step_1 + +
+ +
+ +The Base theme is very barebones, and uses `gr.themes.Blue` as it primary color - you'll note the primary button and the loading animation are both blue as a result. Let's change the defaults core arguments of our app. We'll overwrite the constructor and pass new defaults for the core constructor arguments. + +We'll use `gr.themes.Emerald` as our primary color, and set secondary and neutral hues to `gr.themes.Blue`. We'll make our text larger using `text_lg`. We'll use `Quicksand` as our default font, loaded from Google Fonts. + +$code_theme_new_step_2 + +
+ +
+ +See how the primary button and the loading animation are now green? These CSS variables are tied to the `primary_hue` variable. + +Let's modify the theme a bit more directly. We'll call the `set()` method to overwrite CSS variable values explicitly. We can use any CSS logic, and reference our core constructor arguments using the `*` prefix. + +$code_theme_new_step_3 + +
+ +
+ +Look how fun our theme looks now! With just a few variable changes, our theme looks completely different. + +You may find it helpful to explore the [source code of the other prebuilt themes](https://github.com/gradio-app/gradio/blob/main/gradio/themes) to see how they modified the base theme. You can also find your browser's Inspector useful to select elements from the UI and see what CSS variables are being used in the styles panel. + +## Sharing Themes + +Once you have created a theme, you can upload it to the HuggingFace Hub to let others view it, use it, and build off of it! + +### Uploading a Theme + +There are two ways to upload a theme, via the theme class instance or the command line. We will cover both of them with the previously created `seafoam` theme. + +- Via the class instance + +Each theme instance has a method called `push_to_hub` we can use to upload a theme to the HuggingFace hub. + +```python +seafoam.push_to_hub(repo_name="seafoam", + version="0.0.1", + token="") +``` + +- Via the command line + +First save the theme to disk + +```python +seafoam.dump(filename="seafoam.json") +``` + +Then use the `upload_theme` command: + +```bash +upload_theme\ +"seafoam.json"\ +"seafoam"\ +--version "0.0.1"\ +--token "" +``` + +In order to upload a theme, you must have a HuggingFace account and pass your [Access Token](https://huggingface.co/docs/huggingface_hub/quick-start#login) +as the `token` argument. However, if you log in via the [HuggingFace command line](https://huggingface.co/docs/huggingface_hub/quick-start#login) (which comes installed with `gradio`), +you can omit the `token` argument. + +The `version` argument lets you specify a valid [semantic version](https://www.geeksforgeeks.org/introduction-semantic-versioning/) string for your theme. +That way your users are able to specify which version of your theme they want to use in their apps. This also lets you publish updates to your theme without worrying +about changing how previously created apps look. The `version` argument is optional. If omitted, the next patch version is automatically applied. + +### Theme Previews + +By calling `push_to_hub` or `upload_theme`, the theme assets will be stored in a [HuggingFace space](https://huggingface.co/docs/hub/spaces-overview). + +For example, the theme preview for the calm seafoam theme is here: [calm seafoam preview](https://huggingface.co/spaces/shivalikasingh/calm_seafoam). + +
+ +
+ +### Discovering Themes + +The [Theme Gallery](https://gradio.app/themes/gallery) on the Gradio website shows all official and community themes. You can search, filter by official or community themes, and preview each theme's colors, fonts, and live demo. + +Community themes are sourced from the [gradio/theme-gallery](https://huggingface.co/datasets/gradio/theme-gallery) dataset on HuggingFace. To add your theme to the gallery, open a Pull Request to that dataset with your theme's metadata in `manifest.json`. + +### Downloading + +To use a theme from the hub, use the `from_hub` method on the `ThemeClass` and pass it to your app: + +```python +my_theme = gr.Theme.from_hub("gradio/seafoam") + +with gr.Blocks() as demo: + ... # your code here +demo.launch(theme=my_theme) +``` + +You can also pass the theme string directly to the `launch()` method of `Blocks` or `Interface` (e.g. `demo.launch(theme="gradio/seafoam")`) + +You can pin your app to an upstream theme version by using semantic versioning expressions. + +For example, the following would ensure the theme we load from the `seafoam` repo was between versions `0.0.1` and `0.1.0`: + +```python +with gr.Blocks() as demo: + ... # your code here +demo.launch(theme="gradio/seafoam@>=0.0.1,<0.1.0") + .... +``` + +### Version Compatibility + +When you save a theme with `theme.dump()` or upload it with `theme.push_to_hub()`, the current Gradio version is automatically added. When someone loads a theme created with a different major or minor version of Gradio, they'll see a warning: + +``` +UserWarning: This theme was created for Gradio 5.0.0, but you are using Gradio 5.1.0. Some styles may not work as expected. +``` + +This helps prevent unexpected styling issues when themes rely on CSS variables that may have changed between Gradio versions. Patch version differences (e.g. 5.0.0 vs 5.0.1) will not trigger a warning. + +Enjoy creating your own themes! If you make one you're proud of, please share it with the world by uploading it to the hub! +If you tag us on [Twitter](https://twitter.com/gradio) we can give your theme a shout out! + + diff --git a/guides/11_other-tutorials/understanding-gradio-share-links.md b/guides/11_other-tutorials/understanding-gradio-share-links.md new file mode 100644 index 0000000..91a8a57 --- /dev/null +++ b/guides/11_other-tutorials/understanding-gradio-share-links.md @@ -0,0 +1,76 @@ +# Share Links and Share Servers + +You may already know that you can share any Gradio app that you build by setting `share=True` in the `.launch()` method. In other words, if you do: + +```py +import gradio as gr + +with gr.Blocks() as demo: + ... + +demo.launch(share=True) +``` + +This creates a publicly accessible **share link** (which looks like: `https://xxxxx.gradio.live`) to your Gradio application immediately, letting you share your app with anyone (while keeping the code and model running in your local environment). The link is created on Gradio's **share server**, which does not host your Gradio app, but instead creates a _tunnel_ to your locally-running Gradio app. + +This is particlarly useful when you are prototyping and want to get immediate feedback on your machine learning app, without having to deal with the hassle of hosting or deploying your application. + + + +At any given time, more than 5,000 Gradio apps are being shared through share links. But how is this link created, and how can you create your own share server? Read on! + +### Fast Reverse Proxy (FRP) + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/frp-gradio-diagram.svg) + +Gradio share links are powered by Fast Reverse Proxy (FRP), an [open-source tunneling solution](https://github.com/huggingface/frp). Here's how it works: + +When you create a Gradio app with `share=True`, the FRP Client is automatically downloaded to your local machine (if not already installed). This client establishes a secure TLS tunnel to Gradio's Share Server, which hosts the FRP Server component capable of handling thousands of simultaneous connections. + +Once the tunnel is established, Gradio's Share Server exposes your locally-running application to the internet under a unique domain in the format `xxxxx.gradio.live`. This entire process happens in the background, when you launch a Gradio app with `share=True`. + +Next, we'll dive deeper into both the FRP Client and FRP Server, as they are used in Gradio. + +### FRP Client + +We use a [modified version of the FRP Client](https://github.com/huggingface/frp/tree/tls/client), which runs on your machine. We package binaries for the most common operating systems, and the FRP Client for your system is downloaded the first time you create a share link on your machine. + +**Code**: +* The complete Go code for the client can be found [in this directory](https://github.com/huggingface/frp/tree/tls/client). +* We use this [Make script](https://github.com/huggingface/frp/blob/tls/Makefile) to package the Go code into binaries for each operating system. + +**Troubleshooting**: Some antivirus programs (notably Windows Defender) block the download of the FRP Client. In this case, you'll see a message with details on how to install the file manually, something like: + +``` +Could not create share link. Missing file: /Users/.../frpc_darwin_arm64_v0.3. + +Please check your internet connection. This can happen if your antivirus software blocks the download of this file. You can install manually by following these steps: + +1. Download this file: https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_darwin_arm64 +2. Rename the downloaded file to: frpc_darwin_arm64_v0.3 +3. Move the file to this location: /Users/... +``` + +If this does not work, you may need to [whitelist this file with your antivirus](https://www.makeuseof.com/how-to-whitelist-files-windows-defender/) in order to use the share links. + +### FRP Server + +Gradio runs a share server, which is a modified version of the FRP server. This server handles the public-facing side of the tunnel, receiving incoming connections from the internet and routing them to the appropriate FRP client running on your local machine. + +The official Gradio share server is hosted at `gradio.live`, and we make our best effort to keep it running reliably at all times. This is the server that's used by default when you set `share=True` in your Gradio applications. You can check the current operational status of the official Gradio share server at [https://status.gradio.app/](https://status.gradio.app/). + +If you prefer, you can also host your own FRP server. This gives you complete control over the tunneling infrastructure and can be useful for enterprise deployments or situations where you need custom domains or additional security measures, or if you want to avoid the 72 hour timeout that is in place for links created through Gradio's official share server. Here are the instructions for running your own [Gradio Share Server](https://github.com/huggingface/frp?tab=readme-ov-file#why-run-your-own-share-server). + + +**Code**: +* The complete Go code for the client can be found [in this directory](https://github.com/huggingface/frp/tree/dev/server). +* The Dockerfile to launch [the FRP Server](https://github.com/huggingface/frp/blob/dev/dockerfiles/Dockerfile-for-frps) can be found here. + +**Troubleshooting**: Gradio's Share Server may occasionally go down, despite our best effort to keep it running. If the [status page](https://status.gradio.app/) shows that the Gradio server is down, we'll work on fixing it, no need to create an issue! + + + + + diff --git a/guides/11_other-tutorials/using-flagging.md b/guides/11_other-tutorials/using-flagging.md new file mode 100644 index 0000000..0cdf9a1 --- /dev/null +++ b/guides/11_other-tutorials/using-flagging.md @@ -0,0 +1,153 @@ +# Using Flagging + +Related spaces: https://huggingface.co/spaces/gradio/calculator-flagging-crowdsourced, https://huggingface.co/spaces/gradio/calculator-flagging-options, https://huggingface.co/spaces/gradio/calculator-flagging-basic +Tags: FLAGGING, DATA + +## Introduction + +When you demo a machine learning model, you might want to collect data from users who try the model, particularly data points in which the model is not behaving as expected. Capturing these "hard" data points is valuable because it allows you to improve your machine learning model and make it more reliable and robust. + +Gradio simplifies the collection of this data by including a **Flag** button with every `Interface`. This allows a user or tester to easily send data back to the machine where the demo is running. In this Guide, we discuss more about how to use the flagging feature, both with `gradio.Interface` as well as with `gradio.Blocks`. + +## The **Flag** button in `gradio.Interface` + +Flagging with Gradio's `Interface` is especially easy. By default, underneath the output components, there is a button marked **Flag**. When a user testing your model sees input with interesting output, they can click the flag button to send the input and output data back to the machine where the demo is running. The sample is saved to a CSV log file (by default). If the demo involves images, audio, video, or other types of files, these are saved separately in a parallel directory and the paths to these files are saved in the CSV file. + +There are [four parameters](https://gradio.app/docs/interface#initialization) in `gradio.Interface` that control how flagging works. We will go over them in greater detail. + +- `flagging_mode`: this parameter can be set to either `"manual"` (default), `"auto"`, or `"never"`. + - `manual`: users will see a button to flag, and samples are only flagged when the button is clicked. + - `auto`: users will not see a button to flag, but every sample will be flagged automatically. + - `never`: users will not see a button to flag, and no sample will be flagged. +- `flagging_options`: this parameter can be either `None` (default) or a list of strings. + - If `None`, then the user simply clicks on the **Flag** button and no additional options are shown. + - If a list of strings are provided, then the user sees several buttons, corresponding to each of the strings that are provided. For example, if the value of this parameter is `["Incorrect", "Ambiguous"]`, then buttons labeled **Flag as Incorrect** and **Flag as Ambiguous** appear. This only applies if `flagging_mode` is `"manual"`. + - The chosen option is then logged along with the input and output. +- `flagging_dir`: this parameter takes a string. + - It represents what to name the directory where flagged data is stored. +- `flagging_callback`: this parameter takes an instance of a subclass of the `FlaggingCallback` class + - Using this parameter allows you to write custom code that gets run when the flag button is clicked + - By default, this is set to an instance of `gr.JSONLogger` + +## What happens to flagged data? + +Within the directory provided by the `flagging_dir` argument, a JSON file will log the flagged data. + +Here's an example: The code below creates the calculator interface embedded below it: + +```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": + return num1 / num2 + + +iface = gr.Interface( + calculator, + ["number", gr.Radio(["add", "subtract", "multiply", "divide"]), "number"], + "number", + flagging_mode="manual" +) + +iface.launch() +``` + + + +When you click the flag button above, the directory where the interface was launched will include a new flagged subfolder, with a csv file inside it. This csv file includes all the data that was flagged. + +```directory ++-- flagged/ +| +-- logs.csv +``` + +_flagged/logs.csv_ + +```csv +num1,operation,num2,Output,timestamp +5,add,7,12,2022-01-31 11:40:51.093412 +6,subtract,1.5,4.5,2022-01-31 03:25:32.023542 +``` + +If the interface involves file data, such as for Image and Audio components, folders will be created to store those flagged data as well. For example an `image` input to `image` output interface will create the following structure. + +```directory ++-- flagged/ +| +-- logs.csv +| +-- image/ +| | +-- 0.png +| | +-- 1.png +| +-- Output/ +| | +-- 0.png +| | +-- 1.png +``` + +_flagged/logs.csv_ + +```csv +im,Output timestamp +im/0.png,Output/0.png,2022-02-04 19:49:58.026963 +im/1.png,Output/1.png,2022-02-02 10:40:51.093412 +``` + +If you wish for the user to provide a reason for flagging, you can pass a list of strings to the `flagging_options` argument of Interface. Users will have to select one of these choices when flagging, and the option will be saved as an additional column to the CSV. + +If we go back to the calculator example, the following code will create the interface embedded below it. + +```python +iface = gr.Interface( + calculator, + ["number", gr.Radio(["add", "subtract", "multiply", "divide"]), "number"], + "number", + flagging_mode="manual", + flagging_options=["wrong sign", "off by one", "other"] +) + +iface.launch() +``` + + + +When users click the flag button, the csv file will now include a column indicating the selected option. + +_flagged/logs.csv_ + +```csv +num1,operation,num2,Output,flag,timestamp +5,add,7,-12,wrong sign,2022-02-04 11:40:51.093412 +6,subtract,1.5,3.5,off by one,2022-02-04 11:42:32.062512 +``` + +## Flagging with Blocks + +What about if you are using `gradio.Blocks`? On one hand, you have even more flexibility +with Blocks -- you can write whatever Python code you want to run when a button is clicked, +and assign that using the built-in events in Blocks. + +At the same time, you might want to use an existing `FlaggingCallback` to avoid writing extra code. +This requires two steps: + +1. You have to run your callback's `.setup()` somewhere in the code prior to the + first time you flag data +2. When the flagging button is clicked, then you trigger the callback's `.flag()` method, + making sure to collect the arguments correctly and disabling the typical preprocessing. + +Here is an example with an image sepia filter Blocks demo that lets you flag +data using the default `CSVLogger`: + +$code_blocks_flag +$demo_blocks_flag + +## Privacy + +Important Note: please make sure your users understand when the data they submit is being saved, and what you plan on doing with it. This is especially important when you use `flagging_mode=auto` (when all of the data submitted through the demo is being flagged) + +### That's all! Happy building :) diff --git a/guides/11_other-tutorials/using-gradio-for-tabular-workflows.md b/guides/11_other-tutorials/using-gradio-for-tabular-workflows.md new file mode 100644 index 0000000..c7da703 --- /dev/null +++ b/guides/11_other-tutorials/using-gradio-for-tabular-workflows.md @@ -0,0 +1,103 @@ +# Using Gradio for Tabular Data Science Workflows + +Related spaces: https://huggingface.co/spaces/scikit-learn/gradio-skops-integration, https://huggingface.co/spaces/scikit-learn/tabular-playground, https://huggingface.co/spaces/merve/gradio-analysis-dashboard + +## Introduction + +Tabular data science is the most widely used domain of machine learning, with problems ranging from customer segmentation to churn prediction. Throughout various stages of the tabular data science workflow, communicating your work to stakeholders or clients can be cumbersome; which prevents data scientists from focusing on what matters, such as data analysis and model building. Data scientists can end up spending hours building a dashboard that takes in dataframe and returning plots, or returning a prediction or plot of clusters in a dataset. In this guide, we'll go through how to use `gradio` to improve your data science workflows. We will also talk about how to use `gradio` and [skops](https://skops.readthedocs.io/en/stable/) to build interfaces with only one line of code! + +### Prerequisites + +Make sure you have the `gradio` Python package already [installed](/getting_started). + +## Let's Create a Simple Interface! + +We will take a look at how we can create a simple UI that predicts failures based on product information. + +```python +import gradio as gr +import pandas as pd +import joblib +import datasets + + +inputs = [gr.Dataframe(row_count = (2, "dynamic"), col_count=(4,"dynamic"), label="Input Data", interactive=1)] + +outputs = [gr.Dataframe(row_count = (2, "dynamic"), col_count=(1, "fixed"), label="Predictions", headers=["Failures"])] + +model = joblib.load("model.pkl") + +# we will give our dataframe as example +df = datasets.load_dataset("merve/supersoaker-failures") +df = df["train"].to_pandas() + +def infer(input_dataframe): + return pd.DataFrame(model.predict(input_dataframe)) + +gr.Interface(fn = infer, inputs = inputs, outputs = outputs, examples = [[df.head(2)]]).launch() +``` + +Let's break down above code. + +- `fn`: the inference function that takes input dataframe and returns predictions. +- `inputs`: the component we take our input with. We define our input as dataframe with 2 rows and 4 columns, which initially will look like an empty dataframe with the aforementioned shape. When the `row_count` is set to `dynamic`, you don't have to rely on the dataset you're inputting to pre-defined component. +- `outputs`: The dataframe component that stores outputs. This UI can take single or multiple samples to infer, and returns 0 or 1 for each sample in one column, so we give `row_count` as 2 and `col_count` as 1 above. `headers` is a list made of header names for dataframe. +- `examples`: You can either pass the input by dragging and dropping a CSV file, or a pandas DataFrame through examples, which headers will be automatically taken by the interface. + +We will now create an example for a minimal data visualization dashboard. You can find a more comprehensive version in the related Spaces. + + + +```python +import gradio as gr +import pandas as pd +import datasets +import seaborn as sns +import matplotlib.pyplot as plt + +df = datasets.load_dataset("merve/supersoaker-failures") +df = df["train"].to_pandas() +df.dropna(axis=0, inplace=True) + +def plot(df): + plt.scatter(df.measurement_13, df.measurement_15, c = df.loading,alpha=0.5) + plt.savefig("scatter.png") + df['failure'].value_counts().plot(kind='bar') + plt.savefig("bar.png") + sns.heatmap(df.select_dtypes(include="number").corr()) + plt.savefig("corr.png") + plots = ["corr.png","scatter.png", "bar.png"] + return plots + +inputs = [gr.Dataframe(label="Supersoaker Production Data")] +outputs = [gr.Gallery(label="Profiling Dashboard", columns=(1,3))] + +gr.Interface(plot, inputs=inputs, outputs=outputs, examples=[df.head(100)], title="Supersoaker Failures Analysis Dashboard").launch() +``` + + + +We will use the same dataset we used to train our model, but we will make a dashboard to visualize it this time. + +- `fn`: The function that will create plots based on data. +- `inputs`: We use the same `Dataframe` component we used above. +- `outputs`: The `Gallery` component is used to keep our visualizations. +- `examples`: We will have the dataset itself as the example. + +## Easily load tabular data interfaces with one line of code using skops + +`skops` is a library built on top of `huggingface_hub` and `sklearn`. With the recent `gradio` integration of `skops`, you can build tabular data interfaces with one line of code! + +```python +import gradio as gr + +# title and description are optional +title = "Supersoaker Defective Product Prediction" +description = "This model predicts Supersoaker production line failures. Drag and drop any slice from dataset or edit values as you wish in below dataframe component." + +gr.load("huggingface/scikit-learn/tabular-playground", title=title, description=description).launch() +``` + + + +`sklearn` models pushed to Hugging Face Hub using `skops` include a `config.json` file that contains an example input with column names, the task being solved (that can either be `tabular-classification` or `tabular-regression`). From the task type, `gradio` constructs the `Interface` and consumes column names and the example input to build it. You can [refer to skops documentation on hosting models on Hub](https://skops.readthedocs.io/en/latest/auto_examples/plot_hf_hub.html#sphx-glr-auto-examples-plot-hf-hub-py) to learn how to push your models to Hub using `skops`. diff --git a/guides/11_other-tutorials/using-gradio-in-other-programming-languages.md b/guides/11_other-tutorials/using-gradio-in-other-programming-languages.md new file mode 100644 index 0000000..6be3ab3 --- /dev/null +++ b/guides/11_other-tutorials/using-gradio-in-other-programming-languages.md @@ -0,0 +1,176 @@ +# Using Gradio in Other Programming Languages + +The core `gradio` library is a Python library. But you can also use `gradio` to create UIs around programs written in other languages, thanks to Python's ability to interface with external processes. Using Python's `subprocess` module, you can call programs written in C++, Rust, or virtually any other language, allowing `gradio` to become a flexible UI layer for non-Python applications. + +In this post, we'll walk through how to integrate `gradio` with C++ and Rust, using Python's `subprocess` module to invoke code written in these languages. We'll also discuss how to use Gradio with R, which is even easier, thanks to the [reticulate](https://rstudio.github.io/reticulate/) R package, which makes it possible to install and import Python modules in R. + +## Using Gradio with C++ + +Let’s start with a simple example of integrating a C++ program into a Gradio app. Suppose we have the following C++ program that adds two numbers: + +```cpp +// add.cpp +#include + +int main() { + double a, b; + std::cin >> a >> b; + std::cout << a + b << std::endl; + return 0; +} +``` + +This program reads two numbers from standard input, adds them, and outputs the result. + +We can build a Gradio interface around this C++ program using Python's `subprocess` module. Here’s the corresponding Python code: + +```python +import gradio as gr +import subprocess + +def add_numbers(a, b): + process = subprocess.Popen( + ['./add'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + output, error = process.communicate(input=f"{a} {b}\n".encode()) + + if error: + return f"Error: {error.decode()}" + return float(output.decode().strip()) + +demo = gr.Interface( + fn=add_numbers, + inputs=[gr.Number(label="Number 1"), gr.Number(label="Number 2")], + outputs=gr.Textbox(label="Result") +) + +demo.launch() +``` + +Here, `subprocess.Popen` is used to execute the compiled C++ program (`add`), pass the input values, and capture the output. You can compile the C++ program by running: + +```bash +g++ -o add add.cpp +``` + +This example shows how easy it is to call C++ from Python using `subprocess` and build a Gradio interface around it. + +## Using Gradio with Rust + +Now, let’s move to another example: calling a Rust program to apply a sepia filter to an image. The Rust code could look something like this: + +```rust +// sepia.rs +extern crate image; + +use image::{GenericImageView, ImageBuffer, Rgba}; + +fn sepia_filter(input: &str, output: &str) { + let img = image::open(input).unwrap(); + let (width, height) = img.dimensions(); + let mut img_buf = ImageBuffer::new(width, height); + + for (x, y, pixel) in img.pixels() { + let (r, g, b, a) = (pixel[0] as f32, pixel[1] as f32, pixel[2] as f32, pixel[3]); + let tr = (0.393 * r + 0.769 * g + 0.189 * b).min(255.0); + let tg = (0.349 * r + 0.686 * g + 0.168 * b).min(255.0); + let tb = (0.272 * r + 0.534 * g + 0.131 * b).min(255.0); + img_buf.put_pixel(x, y, Rgba([tr as u8, tg as u8, tb as u8, a])); + } + + img_buf.save(output).unwrap(); +} + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() != 3 { + eprintln!("Usage: sepia "); + return; + } + sepia_filter(&args[1], &args[2]); +} +``` + +This Rust program applies a sepia filter to an image. It takes two command-line arguments: the input image path and the output image path. You can compile this program using: + +```bash +cargo build --release +``` + +Now, we can call this Rust program from Python and use Gradio to build the interface: + +```python +import gradio as gr +import subprocess + +def apply_sepia(input_path): + output_path = "output.png" + + process = subprocess.Popen( + ['./target/release/sepia', input_path, output_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + process.wait() + + return output_path + +demo = gr.Interface( + fn=apply_sepia, + inputs=gr.Image(type="filepath", label="Input Image"), + outputs=gr.Image(label="Sepia Image") +) + +demo.launch() +``` + +Here, when a user uploads an image and clicks submit, Gradio calls the Rust binary (`sepia`) to process the image, and returns the sepia-filtered output to Gradio. + +This setup showcases how you can integrate performance-critical or specialized code written in Rust into a Gradio interface. + +## Using Gradio with R (via `reticulate`) + +Integrating Gradio with R is particularly straightforward thanks to the `reticulate` package, which allows you to run Python code directly in R. Let’s walk through an example of using Gradio in R. + +**Installation** + +First, you need to install the `reticulate` package in R: + +```r +install.packages("reticulate") +``` + + +Once installed, you can use the package to run Gradio directly from within an R script. + + +```r +library(reticulate) + +py_install("gradio", pip = TRUE) + +gr <- import("gradio") # import gradio as gr +``` + +**Building a Gradio Application** + +With gradio installed and imported, we now have access to gradio's app building methods. Let's build a simple app for an R function that returns a greeting + +```r +greeting <- \(name) paste("Hello", name) + +app <- gr$Interface( + fn = greeting, + inputs = gr$Text(label = "Name"), + outputs = gr$Text(label = "Greeting"), + title = "Hello! 😃 👋" +) + +app$launch(server_name = "localhost", + server_port = as.integer(3000)) +``` + +Credit to [@IfeanyiIdiaye](https://github.com/Ifeanyi55) for contributing this section. You can see more examples [here](https://github.com/Ifeanyi55/Gradio-in-R/tree/main/Code), including using Gradio Blocks to build a machine learning application in R. diff --git a/guides/11_other-tutorials/wrapping-layouts.md b/guides/11_other-tutorials/wrapping-layouts.md new file mode 100644 index 0000000..6d8e444 --- /dev/null +++ b/guides/11_other-tutorials/wrapping-layouts.md @@ -0,0 +1,178 @@ +# Wrapping Layouts + +Tags: LAYOUTS + +## Introduction + +Gradio features [blocks](https://www.gradio.app/docs/blocks) to easily layout applications. To use this feature, you need to stack or nest layout components and create a hierarchy with them. This isn't difficult to implement and maintain for small projects, but after the project gets more complex, this component hierarchy becomes difficult to maintain and reuse. + +In this guide, we are going to explore how we can wrap the layout classes to create more maintainable and easy-to-read applications without sacrificing flexibility. + +## Example + +We are going to follow the implementation from this Huggingface Space example: + + + + +## Implementation + +The wrapping utility has two important classes. The first one is the ```LayoutBase``` class and the other one is the ```Application``` class. + +We are going to look at the ```render``` and ```attach_event``` functions of them for brevity. You can look at the full implementation from [the example code](https://huggingface.co/spaces/WoWoWoWololo/wrapping-layouts/blob/main/app.py). + +So let's start with the ```LayoutBase``` class. + +### LayoutBase Class + +1. Render Function + + Let's look at the ```render``` function in the ```LayoutBase``` class: + +```python +# other LayoutBase implementations + +def render(self) -> None: + with self.main_layout: + for renderable in self.renderables: + renderable.render() + + self.main_layout.render() +``` +This is a little confusing at first but if you consider the default implementation you can understand it easily. +Let's look at an example: + +In the default implementation, this is what we're doing: + +```python +with Row(): + left_textbox = Textbox(value="left_textbox") + right_textbox = Textbox(value="right_textbox") +``` + +Now, pay attention to the Textbox variables. These variables' ```render``` parameter is true by default. So as we use the ```with``` syntax and create these variables, they are calling the ```render``` function under the ```with``` syntax. + +We know the render function is called in the constructor with the implementation from the ```gradio.blocks.Block``` class: + +```python +class Block: + # constructor parameters are omitted for brevity + def __init__(self, ...): + # other assign functions + + if render: + self.render() +``` + +So our implementation looks like this: + +```python +# self.main_layout -> Row() +with self.main_layout: + left_textbox.render() + right_textbox.render() +``` + +What this means is by calling the components' render functions under the ```with``` syntax, we are actually simulating the default implementation. + +So now let's consider two nested ```with```s to see how the outer one works. For this, let's expand our example with the ```Tab``` component: + +```python +with Tab(): + with Row(): + first_textbox = Textbox(value="first_textbox") + second_textbox = Textbox(value="second_textbox") +``` + +Pay attention to the Row and Tab components this time. We have created the Textbox variables above and added them to Row with the ```with``` syntax. Now we need to add the Row component to the Tab component. You can see that the Row component is created with default parameters, so its render parameter is true, that's why the render function is going to be executed under the Tab component's ```with``` syntax. + +To mimic this implementation, we need to call the ```render``` function of the ```main_layout``` variable after the ```with``` syntax of the ```main_layout``` variable. + +So the implementation looks like this: + +```python +with tab_main_layout: + with row_main_layout: + first_textbox.render() + second_textbox.render() + + row_main_layout.render() + +tab_main_layout.render() +``` + +The default implementation and our implementation are the same, but we are using the render function ourselves. So it requires a little work. + +Now, let's take a look at the ```attach_event``` function. + +2. Attach Event Function + + The function is left as not implemented because it is specific to the class, so each class has to implement its `attach_event` function. + +```python + # other LayoutBase implementations + + def attach_event(self, block_dict: Dict[str, Block]) -> None: + raise NotImplementedError +``` + +Check out the ```block_dict``` variable in the ```Application``` class's ```attach_event``` function. + +### Application Class + +1. Render Function + +```python + # other Application implementations + + def _render(self): + with self.app: + for child in self.children: + child.render() + + self.app.render() +``` + +From the explanation of the ```LayoutBase``` class's ```render``` function, we can understand the ```child.render``` part. + +So let's look at the bottom part, why are we calling the ```app``` variable's ```render``` function? It's important to call this function because if we look at the implementation in the ```gradio.blocks.Blocks``` class, we can see that it is adding the components and event functions into the root component. To put it another way, it is creating and structuring the gradio application. + +2. Attach Event Function + + Let's see how we can attach events to components: + +```python + # other Application implementations + + def _attach_event(self): + block_dict: Dict[str, Block] = {} + + for child in self.children: + block_dict.update(child.global_children_dict) + + with self.app: + for child in self.children: + try: + child.attach_event(block_dict=block_dict) + except NotImplementedError: + print(f"{child.name}'s attach_event is not implemented") +``` + +You can see why the ```global_children_list``` is used in the ```LayoutBase``` class from the example code. With this, all the components in the application are gathered into one dictionary, so the component can access all the components with their names. + +The ```with``` syntax is used here again to attach events to components. If we look at the ```__exit__``` function in the ```gradio.blocks.Blocks``` class, we can see that it is calling the ```attach_load_events``` function which is used for setting event triggers to components. So we have to use the ```with``` syntax to trigger the ```__exit__``` function. + +Of course, we can call ```attach_load_events``` without using the ```with``` syntax, but the function needs a ```Context.root_block```, and it is set in the ```__enter__``` function. So we used the ```with``` syntax here rather than calling the function ourselves. + +## Conclusion + +In this guide, we saw + +- How we can wrap the layouts +- How components are rendered +- How we can structure our application with wrapped layout classes + +Because the classes used in this guide are used for demonstration purposes, they may still not be totally optimized or modular. But that would make the guide much longer! + +I hope this guide helps you gain another view of the layout classes and gives you an idea about how you can use them for your needs. See the full implementation of our example [here](https://huggingface.co/spaces/WoWoWoWololo/wrapping-layouts/blob/main/app.py). diff --git a/guides/CONTRIBUTING.md b/guides/CONTRIBUTING.md new file mode 100644 index 0000000..d43e06d --- /dev/null +++ b/guides/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Contributing a Guide + +Want to help teach Gradio? Consider contributing a Guide! 🤗 + +Broadly speaking, there are two types of guides: + +- **Use cases**: guides that cover step-by-step how to build a particular type of machine learning demo or app using Gradio. Here's an example: [_Creating a Chatbot_](https://github.com/gradio-app/gradio/blob/master/guides/creating_a_chatbot.md) +- **Feature explanation**: guides that describe in detail a particular feature of Gradio. Here's an example: [_Using Flagging_](https://github.com/gradio-app/gradio/blob/master/guides/using_flagging.md) + +We encourage you to submit either type of Guide! (Looking for ideas? We may also have open [issues](https://github.com/gradio-app/gradio/issues?q=is%3Aopen+is%3Aissue+label%3Aguides) where users have asked for guides on particular topics) + +## Guide Structure + +As you can see with the previous examples, Guides are standard markdown documents. They usually: + +- start with an Introduction section describing the topic +- include subheadings to make articles easy to navigate +- include real code snippets that make it easy to follow along and implement the Guide +- include embedded Gradio demos to make them more interactive and provide immediate demonstrations of the topic being discussed. These Gradio demos are hosted on [Hugging Face Spaces](https://huggingface.co/spaces) and are embedded using the standard \ tag. + +## How to Contribute a Guide + +1. Clone or fork this `gradio` repo +2. Add a new markdown document with a descriptive title to the `/guides` folder +3. Write your Guide in standard markdown! Embed Gradio demos wherever helpful +4. Add a list of `related_spaces` at the top of the markdown document (see the previously linked Guides for how to do this) +5. Add 3 `tags` at the top of the markdown document to help users find your guide (again, see the previously linked Guides for how to do this) +6. Open a PR to have your guide reviewed + +That's it! We're looking forward to reading your Guide 🥳 diff --git a/guides/assets/access_token.png b/guides/assets/access_token.png new file mode 100644 index 0000000..7e7d8da Binary files /dev/null and b/guides/assets/access_token.png differ diff --git a/guides/assets/annotated.png b/guides/assets/annotated.png new file mode 100644 index 0000000..e8db15f Binary files /dev/null and b/guides/assets/annotated.png differ diff --git a/guides/assets/art-critic.mp4 b/guides/assets/art-critic.mp4 new file mode 100644 index 0000000..8261a2a Binary files /dev/null and b/guides/assets/art-critic.mp4 differ diff --git a/guides/assets/dataflow.svg b/guides/assets/dataflow.svg new file mode 100644 index 0000000..43e7e86 --- /dev/null +++ b/guides/assets/dataflow.svg @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/guides/assets/embed_this_space.png b/guides/assets/embed_this_space.png new file mode 100644 index 0000000..8b37e80 Binary files /dev/null and b/guides/assets/embed_this_space.png differ diff --git a/guides/assets/flagging-callback-hf.png b/guides/assets/flagging-callback-hf.png new file mode 100644 index 0000000..5e049fb Binary files /dev/null and b/guides/assets/flagging-callback-hf.png differ diff --git a/guides/assets/hf_demo.mp4 b/guides/assets/hf_demo.mp4 new file mode 100644 index 0000000..deb6d28 Binary files /dev/null and b/guides/assets/hf_demo.mp4 differ diff --git a/guides/assets/logo.png b/guides/assets/logo.png new file mode 100644 index 0000000..e465f2b Binary files /dev/null and b/guides/assets/logo.png differ diff --git a/guides/assets/secrets.png b/guides/assets/secrets.png new file mode 100644 index 0000000..b14d9d1 Binary files /dev/null and b/guides/assets/secrets.png differ diff --git a/guides/assets/share_icon.png b/guides/assets/share_icon.png new file mode 100644 index 0000000..4822451 Binary files /dev/null and b/guides/assets/share_icon.png differ diff --git a/guides/assets/sharing.svg b/guides/assets/sharing.svg new file mode 100644 index 0000000..334e0d0 --- /dev/null +++ b/guides/assets/sharing.svg @@ -0,0 +1,487 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + lion + lion + HOST + REMOTE USERS + diff --git a/guides/assets/use_via_api.png b/guides/assets/use_via_api.png new file mode 100644 index 0000000..1e17e8f Binary files /dev/null and b/guides/assets/use_via_api.png differ diff --git a/guides/cn/01_getting-started/01_quickstart.md b/guides/cn/01_getting-started/01_quickstart.md new file mode 100644 index 0000000..9a2a9ab --- /dev/null +++ b/guides/cn/01_getting-started/01_quickstart.md @@ -0,0 +1,118 @@ +# 快速开始 + +**先决条件**:Gradio 需要 Python 3.10 或更高版本,就是这样! + +## Gradio 是做什么的? + +与他人分享您的机器学习模型、API 或数据科学流程的*最佳方式之一*是创建一个**交互式应用程序**,让您的用户或同事可以在他们的浏览器中尝试演示。 + +Gradio 允许您**使用 Python 构建演示并共享这些演示**。通常只需几行代码!那么我们开始吧。 + +## Hello, World + +要通过一个简单的“Hello, World”示例运行 Gradio,请遵循以下三个步骤: + +1. 使用 pip 安装 Gradio: + +```bash +pip install gradio +``` + +2. 将下面的代码作为 Python 脚本运行或在 Jupyter Notebook 中运行(或者 [Google Colab](https://colab.research.google.com/drive/18ODkJvyxHutTN0P5APWyGFO_xwNcgHDZ?usp=sharing)): + +$code_hello_world + +我们将导入的名称缩短为 `gr`,以便以后在使用 Gradio 的代码中更容易理解。这是一种广泛采用的约定,您应该遵循,以便与您的代码一起工作的任何人都可以轻松理解。 + +3. 在 Jupyter Notebook 中,该演示将自动显示;如果从脚本运行,则会在浏览器中弹出,网址为 [http://localhost:7860](http://localhost:7860): + +$demo_hello_world + +在本地开发时,如果您想将代码作为 Python 脚本运行,您可以使用 Gradio CLI 以**重载模式**启动应用程序,这将提供无缝和快速的开发。了解有关[自动重载指南](https://gradio.app/developing-faster-with-reload-mode/)中重新加载的更多信息。 + +```bash +gradio app.py +``` + +注意:您也可以运行 `python app.py`,但它不会提供自动重新加载机制。 + +## `Interface` 类 + +您会注意到为了创建演示,我们创建了一个 `gr.Interface`。`Interface` 类可以将任何 Python 函数与用户界面配对。在上面的示例中,我们看到了一个简单的基于文本的函数,但该函数可以是任何内容,从音乐生成器到税款计算器再到预训练的机器学习模型的预测函数。 + +`Interface` 类的核心是使用三个必需参数进行初始化: + +- `fn`:要在其周围包装 UI 的函数 +- `inputs`:用于输入的组件(例如 `"text"`、`"image"` 或 `"audio"`) +- `outputs`:用于输出的组件(例如 `"text"`、`"image"` 或 `"label"`) + +让我们更详细地了解用于提供输入和输出的组件。 + +## 组件属性 (Components Attributes) + +我们在前面的示例中看到了一些简单的 `Textbox` 组件,但是如果您想更改 UI 组件的外观或行为怎么办? + +假设您想自定义输入文本字段 - 例如,您希望它更大并具有文本占位符。如果我们使用实际的 `Textbox` 类而不是使用字符串快捷方式,您可以通过组件属性获得更多的自定义功能。 + +$code_hello_world_2 +$demo_hello_world_2 + +## 多个输入和输出组件 + +假设您有一个更复杂的函数,具有多个输入和输出。在下面的示例中,我们定义了一个接受字符串、布尔值和数字,并返回字符串和数字的函数。请看一下如何传递输入和输出组件的列表。 + +$code_hello_world_3 +$demo_hello_world_3 + +只需将组件包装在列表中。`inputs` 列表中的每个组件对应函数的一个参数,顺序相同。`outputs` 列表中的每个组件对应函数返回的一个值,同样是顺序。 + +## 图像示例 + +Gradio 支持许多类型的组件,例如 `Image`、`DataFrame`、`Video` 或 `Label`。让我们尝试一个图像到图像的函数,以了解这些组件的感觉! + +$code_sepia_filter +$demo_sepia_filter + +使用 `Image` 组件作为输入时,您的函数将接收到一个形状为`(高度,宽度,3)` 的 NumPy 数组,其中最后一个维度表示 RGB 值。我们还将返回一个图像,形式为 NumPy 数组。 + +您还可以使用 `type=` 关键字参数设置组件使用的数据类型。例如,如果您希望函数接受图像文件路径而不是 NumPy 数组,输入 `Image` 组件可以写成: + +```python +gr.Image(type="filepath") +``` + +还要注意,我们的输入 `Image` 组件附带有一个编辑按钮🖉,允许裁剪和缩放图像。通过这种方式操作图像可以帮助揭示机器学习模型中的偏见或隐藏的缺陷! + +您可以在[Gradio 文档](https://gradio.app/docs)中阅读有关许多组件以及如何使用它们的更多信息。 + +## Blocks:更灵活和可控 + +Gradio 提供了两个类来构建应用程序: + +1. **Interface**,提供了用于创建演示的高级抽象,我们到目前为止一直在讨论。 + +2. **Blocks**,用于以更灵活的布局和数据流设计 Web 应用程序的低级 API。Blocks 允许您执行诸如特性多个数据流和演示,控制组件在页面上的出现位置,处理复杂的数据流(例如,输出可以作为其他函数的输入),并基于用户交互更新组件的属性 / 可见性等操作 - 仍然全部使用 Python。如果您需要这种可定制性,请尝试使用 `Blocks`! + +## Hello, Blocks + +让我们看一个简单的示例。请注意,此处的 API 与 `Interface` 不同。 + +$code_hello_blocks +$demo_hello_blocks + +需要注意的事项: + +- `Blocks` 可以使用 `with` 子句创建,此子句中创建的任何组件都会自动添加到应用程序中。 +- 组件以按创建顺序垂直放置在应用程序中。(稍后我们将介绍自定义布局!) +- 创建了一个 `Button`,然后在此按钮上添加了一个 `click` 事件监听器。对于这个 API,应该很熟悉!与 `Interface` 类似,`click` 方法接受一个 Python 函数、输入组件和输出组件。 + +## 更复杂的应用 + +下面是一个应用程序,以让您对 `Blocks` 可以实现的更多内容有所了解: + +$code_blocks_flipper +$demo_blocks_flipper + +这里有更多的东西!在[building with blocks](https://gradio.app/building_with_blocks)部分中,我们将介绍如何创建像这样的复杂的 `Blocks` 应用程序。 + +恭喜,您已经熟悉了 Gradio 的基础知识! 🥳 转到我们的[下一个指南](https://gradio.app/key_features)了解更多关于 Gradio 的主要功能。 diff --git a/guides/cn/01_getting-started/02_key-features.md b/guides/cn/01_getting-started/02_key-features.md new file mode 100644 index 0000000..ce48081 --- /dev/null +++ b/guides/cn/01_getting-started/02_key-features.md @@ -0,0 +1,266 @@ +# 主要特点 + +让我们来介绍一下 Gradio 最受欢迎的一些功能!这里是 Gradio 的主要特点: + +1. [添加示例输入](#example-inputs) +2. [传递自定义错误消息](#errors) +3. [添加描述内容](#descriptive-content) +4. [设置旗标](#flagging) +5. [预处理和后处理](#preprocessing-and-postprocessing) +6. [样式化演示](#styling) +7. [排队用户](#queuing) +8. [迭代输出](#iterative-outputs) +9. [进度条](#progress-bars) +10. [批处理函数](#batch-functions) +11. [在协作笔记本上运行](#colab-notebooks) + +## 示例输入 + +您可以提供用户可以轻松加载到 "Interface" 中的示例数据。这对于演示模型期望的输入类型以及演示数据集和模型一起探索的方式非常有帮助。要加载示例数据,您可以将嵌套列表提供给 Interface 构造函数的 `examples=` 关键字参数。外部列表中的每个子列表表示一个数据样本,子列表中的每个元素表示每个输入组件的输入。有关每个组件的示例数据格式在[Docs](https://gradio.app/docs/components)中有说明。 + +$code_calculator +$demo_calculator + +您可以将大型数据集加载到示例中,通过 Gradio 浏览和与数据集进行交互。示例将自动分页(可以通过 Interface 的 `examples_per_page` 参数进行配置)。 + +继续了解示例,请参阅[更多示例](https://gradio.app/more-on-examples)指南。 + +## 错误 + +您希望向用户传递自定义错误消息。为此,with `gr.Error("custom message")` 来显示错误消息。如果在上面的计算器示例中尝试除以零,将显示自定义错误消息的弹出模态窗口。了解有关错误的更多信息,请参阅[文档](https://gradio.app/docs/error)。 + +## 描述性内容 + +在前面的示例中,您可能已经注意到 Interface 构造函数中的 `title=` 和 `description=` 关键字参数,帮助用户了解您的应用程序。 + +Interface 构造函数中有三个参数用于指定此内容应放置在哪里: + +- `title`:接受文本,并可以将其显示在界面的顶部,也将成为页面标题。 +- `description`:接受文本、Markdown 或 HTML,并将其放置在标题正下方。 +- `article`:也接受文本、Markdown 或 HTML,并将其放置在界面下方。 + +![annotated](/assets/guides/annotated.png) + +如果您使用的是 `Blocks` API,则可以 with `gr.Markdown(...)` 或 `gr.HTML(...)` 组件在任何位置插入文本、Markdown 或 HTML,其中描述性内容位于 `Component` 构造函数内部。 + +另一个有用的关键字参数是 `label=`,它存在于每个 `Component` 中。这修改了每个 `Component` 顶部的标签文本。还可以为诸如 `Textbox` 或 `Radio` 之类的表单元素添加 `info=` 关键字参数,以提供有关其用法的进一步信息。 + +```python +gr.Number(label='年龄', info='以年为单位,必须大于0') +``` + +## 旗标 + +默认情况下,"Interface" 将有一个 "Flag" 按钮。当用户测试您的 `Interface` 时,如果看到有趣的输出,例如错误或意外的模型行为,他们可以将输入标记为您进行查看。在由 `Interface` 构造函数的 `flagging_dir=` 参数提供的目录中,将记录标记的输入到一个 CSV 文件中。如果界面涉及文件数据,例如图像和音频组件,将创建文件夹来存储这些标记的数据。 + +例如,对于上面显示的计算器界面,我们将在下面的旗标目录中存储标记的数据: + +```directory ++-- calculator.py ++-- flagged/ +| +-- logs.csv +``` + +_flagged/logs.csv_ + +```csv +num1,operation,num2,Output +5,add,7,12 +6,subtract,1.5,4.5 +``` + +与早期显示的冷色界面相对应,我们将在下面的旗标目录中存储标记的数据: + +```directory ++-- sepia.py ++-- flagged/ +| +-- logs.csv +| +-- im/ +| | +-- 0.png +| | +-- 1.png +| +-- Output/ +| | +-- 0.png +| | +-- 1.png +``` + +_flagged/logs.csv_ + +```csv +im,Output +im/0.png,Output/0.png +im/1.png,Output/1.png +``` + +如果您希望用户提供旗标原因,可以将字符串列表传递给 Interface 的 `flagging_options` 参数。用户在进行旗标时必须选择其中一个字符串,这将作为附加列保存到 CSV 中。 + +## 预处理和后处理 (Preprocessing and Postprocessing) + +![annotated](/assets/img/dataflow.svg) + +如您所见,Gradio 包括可以处理各种不同数据类型的组件,例如图像、音频和视频。大多数组件都可以用作输入或输出。 + +当组件用作输入时,Gradio 自动处理*预处理*,将数据从用户浏览器发送的类型(例如网络摄像头快照的 base64 表示)转换为您的函数可以接受的形式(例如 `numpy` 数组)。 + +同样,当组件用作输出时,Gradio 自动处理*后处理*,将数据从函数返回的形式(例如图像路径列表)转换为可以在用户浏览器中显示的形式(例如以 base64 格式显示图像的 `Gallery`)。 + +您可以使用构建图像组件时的参数控制*预处理*。例如,如果您使用以下参数实例化 `Image` 组件,它将将图像转换为 `PIL` 类型,并将其重塑为`(100, 100)`,而不管提交时的原始大小如何: + +```py +img = gr.Image(width=100, height=100, type="pil") +``` + +相反,这里我们保留图像的原始大小,但在将其转换为 numpy 数组之前反转颜色: + +```py +img = gr.Image(invert_colors=True, type="numpy") +``` + +后处理要容易得多!Gradio 自动识别返回数据的格式(例如 `Image` 是 `numpy` 数组还是 `str` 文件路径?),并将其后处理为可以由浏览器显示的格式。 + +请查看[文档](https://gradio.app/docs),了解每个组件的所有与预处理相关的参数。 + +## 样式 (Styling) + +Gradio 主题是自定义应用程序外观和感觉的最简单方法。您可以选择多种主题或创建自己的主题。要这样做,请将 `theme=` 参数传递给 `Interface` 构造函数。例如: + +```python +demo = gr.Interface(..., theme=gr.themes.Monochrome()) +``` + +Gradio 带有一组预先构建的主题,您可以从 `gr.themes.*` 加载。您可以扩展这些主题或从头开始创建自己的主题 - 有关更多详细信息,请参阅[主题指南](https://gradio.app/theming-guide)。 + +要增加额外的样式能力,您可以 with `css=` 关键字将任何 CSS 传递给您的应用程序。 +Gradio 应用程序的基类是 `gradio-container`,因此以下是一个更改 Gradio 应用程序背景颜色的示例: + +```python +with `gr.Interface(css=".gradio-container {background-color: red}") as demo: + ... +``` + +## 队列 (Queuing) + +如果您的应用程序预计会有大量流量,请 with `queue()` 方法来控制处理速率。这将排队处理调用,因此一次只处理一定数量的请求。队列使用 Websockets,还可以防止网络超时,因此如果您的函数的推理时间很长(> 1 分钟),应使用队列。 + +with `Interface`: + +```python +demo = gr.Interface(...).queue() +demo.launch() +``` + +with `Blocks`: + +```python +with gr.Blocks() as demo: + #... +demo.queue() +demo.launch() +``` + +您可以通过以下方式控制一次处理的请求数量: + +```python +demo.queue(default_concurrency_limit=3) +``` + +查看有关配置其他队列参数的[队列文档](/docs/#queue)。 + +在 Blocks 中指定仅对某些函数进行排队: + +```python +with gr.Blocks() as demo2: + num1 = gr.Number() + num2 = gr.Number() + output = gr.Number() + gr.Button("Add").click( + lambda a, b: a + b, [num1, num2], output) + gr.Button("Multiply").click( + lambda a, b: a * b, [num1, num2], output, queue=True) +demo2.launch() +``` + +## 迭代输出 (Iterative Outputs) + +在某些情况下,您可能需要传输一系列输出而不是一次显示单个输出。例如,您可能有一个图像生成模型,希望显示生成的每个步骤的图像,直到最终图像。或者您可能有一个聊天机器人,它逐字逐句地流式传输响应,而不是一次返回全部响应。 + +在这种情况下,您可以将**生成器**函数提供给 Gradio,而不是常规函数。在 Python 中创建生成器非常简单:函数不应该有一个单独的 `return` 值,而是应该 with `yield` 连续返回一系列值。通常,`yield` 语句放置在某种循环中。下面是一个简单示例,生成器只是简单计数到给定数字: + +```python +def my_generator(x): + for i in range(x): + yield i +``` + +您以与常规函数相同的方式将生成器提供给 Gradio。例如,这是一个(虚拟的)图像生成模型,它在输出图像之前生成数个步骤的噪音: + +$code_fake_diffusion +$demo_fake_diffusion + +请注意,我们在迭代器中添加了 `time.sleep(1)`,以创建步骤之间的人工暂停,以便您可以观察迭代器的步骤(在真实的图像生成模型中,这可能是不必要的)。 + +将生成器提供给 Gradio **需要**在底层 Interface 或 Blocks 中启用队列(请参阅上面的队列部分)。 + +## 进度条 + +Gradio 支持创建自定义进度条,以便您可以自定义和控制向用户显示的进度更新。要启用此功能,只需为方法添加一个默认值为 `gr.Progress` 实例的参数即可。然后,您可以直接调用此实例并传入 0 到 1 之间的浮点数来更新进度级别,或者 with `Progress` 实例的 `tqdm()` 方法来跟踪可迭代对象上的进度,如下所示。必须启用队列以进行进度更新。 + +$code_progress_simple +$demo_progress_simple + +如果您 with `tqdm` 库,并且希望从函数内部的任何 `tqdm.tqdm` 自动报告进度更新,请将默认参数设置为 `gr.Progress(track_tqdm=True)`! + +## 批处理函数 (Batch Functions) + +Gradio 支持传递*批处理*函数。批处理函数只是接受输入列表并返回预测列表的函数。 + +例如,这是一个批处理函数,它接受两个输入列表(一个单词列表和一个整数列表),并返回修剪过的单词列表作为输出: + +```python +import time + +def trim_words(words, lens): + trimmed_words = [] + time.sleep(5) + for w, l in zip(words, lens): + trimmed_words.append(w[:int(l)]) + return [trimmed_words] + for w, l in zip(words, lens): +``` + +使用批处理函数的优点是,如果启用了队列,Gradio 服务器可以自动*批处理*传入的请求并并行处理它们,从而可能加快演示速度。以下是 Gradio 代码的示例(请注意 `batch=True` 和 `max_batch_size=16` - 这两个参数都可以传递给事件触发器或 `Interface` 类) + +with `Interface`: + +```python +demo = gr.Interface(trim_words, ["textbox", "number"], ["output"], + batch=True, max_batch_size=16) +demo.queue() +demo.launch() +``` + +with `Blocks`: + +```python +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + word = gr.Textbox(label="word") + leng = gr.Number(label="leng") + output = gr.Textbox(label="Output") + with gr.Row(): + run = gr.Button() + + event = run.click(trim_words, [word, leng], output, batch=True, max_batch_size=16) + +demo.queue() +demo.launch() +``` + +在上面的示例中,可以并行处理 16 个请求(总推理时间为 5 秒),而不是分别处理每个请求(总推理时间为 80 秒)。许多 Hugging Face 的 `transformers` 和 `diffusers` 模型在 Gradio 的批处理模式下自然工作:这是[使用批处理生成图像的示例演示](https://github.com/gradio-app/gradio/blob/main/demo/diffusers_with_batching/run.py) + +注意:使用 Gradio 的批处理函数 **requires** 在底层 Interface 或 Blocks 中启用队列(请参阅上面的队列部分)。 + +## Gradio 笔记本 (Colab Notebooks) + +Gradio 可以在任何运行 Python 的地方运行,包括本地 Jupyter 笔记本和协作笔记本,如[Google Colab](https://colab.research.google.com/)。对于本地 Jupyter 笔记本和 Google Colab 笔记本,Gradio 在本地服务器上运行,您可以在浏览器中与之交互。(注意:对于 Google Colab,这是通过[服务工作器隧道](https://github.com/tensorflow/tensorboard/blob/master/docs/design/colab_integration.md)实现的,您的浏览器需要启用 cookies。)对于其他远程笔记本,Gradio 也将在服务器上运行,但您需要使用[SSH 隧道](https://coderwall.com/p/ohk6cg/remote-access-to-ipython-notebooks-via-ssh)在本地浏览器中查看应用程序。通常,更简单的选择是使用 Gradio 内置的公共链接,[在下一篇指南中讨论](/sharing-your-app/#sharing-demos)。 diff --git a/guides/cn/01_getting-started/03_sharing-your-app.md b/guides/cn/01_getting-started/03_sharing-your-app.md new file mode 100644 index 0000000..3947aba --- /dev/null +++ b/guides/cn/01_getting-started/03_sharing-your-app.md @@ -0,0 +1,212 @@ +# 分享您的应用 + +如何分享您的 Gradio 应用: + +1. [使用 share 参数分享演示](#sharing-demos) +2. [在 HF Spaces 上托管](#hosting-on-hf-spaces) +3. [嵌入托管的空间](#embedding-hosted-spaces) +4. [使用 Web 组件嵌入](#embedding-with-web-components) +5. [使用 API 页面](#api-page) +6. [在页面上添加身份验证](#authentication) +7. [访问网络请求](#accessing-the-network-request-directly) +8. [在 FastAPI 中挂载](#mounting-within-another-fastapi-app) +9. [安全性](#security-and-file-access) + +## 分享演示 + +通过在 `launch()` 方法中设置 `share=True`,可以轻松公开分享 Gradio 演示。就像这样: + +```python +demo.launch(share=True) +``` + +这将生成一个公开的可分享链接,您可以将其发送给任何人!当您发送此链接时,对方用户可以在其浏览器中尝试模型。因为处理过程发生在您的设备上(只要您的设备保持开启!),您不必担心任何打包依赖项的问题。一个分享链接通常看起来像这样:**XXXXX.gradio.app**。尽管链接是通过 Gradio URL 提供的,但我们只是您本地服务器的代理,并不会存储通过您的应用发送的任何数据。 + +但请记住,这些链接可以被公开访问,这意味着任何人都可以使用您的模型进行预测!因此,请确保不要通过您编写的函数公开任何敏感信息,也不要允许在您的设备上进行任何关键更改。如果您设置 `share=False`(默认值,在 colab 笔记本中除外),则只创建一个本地链接,可以通过[端口转发](https://www.ssh.com/ssh/tunneling/example)与特定用户共享。 + + + +分享链接在 72 小时后过期。 + +## 在 HF Spaces 上托管 + +如果您想在互联网上获得您的 Gradio 演示的永久链接,请使用 Hugging Face Spaces。 [Hugging Face Spaces](http://huggingface.co/spaces/) 提供了免费托管您的机器学习模型的基础设施! + +在您创建了一个免费的 Hugging Face 账户后,有三种方法可以将您的 Gradio 应用部署到 Hugging Face Spaces: + +1. 从终端:在应用目录中运行 `gradio deploy`。CLI 将收集一些基本元数据,然后启动您的应用。要更新您的空间,可以重新运行此命令或启用 Github Actions 选项,在 `git push` 时自动更新 Spaces。 +2. 从浏览器:将包含 Gradio 模型和所有相关文件的文件夹拖放到 [此处](https://huggingface.co/new-space)。 +3. 将 Spaces 与您的 Git 存储库连接,Spaces 将从那里拉取 Gradio 应用。有关更多信息,请参阅 [此指南如何在 Hugging Face Spaces 上托管](https://huggingface.co/blog/gradio-spaces)。 + + + +## 嵌入托管的空间 + +一旦您将应用托管在 Hugging Face Spaces(或您自己的服务器上),您可能希望将演示嵌入到不同的网站上,例如您的博客或个人作品集。嵌入交互式演示使人们可以在他们的浏览器中尝试您构建的机器学习模型,而无需下载或安装任何内容!最好的部分是,您甚至可以将交互式演示嵌入到静态网站中,例如 GitHub 页面。 + +有两种方法可以嵌入您的 Gradio 演示。您可以在 Hugging Face Space 页面的“嵌入此空间”下拉选项中直接找到这两个选项的快速链接: + +![嵌入此空间下拉选项](/assets/guides/embed_this_space.png) + +### 使用 Web 组件嵌入 + +与 IFrames 相比,Web 组件通常为用户提供更好的体验。Web 组件进行延迟加载,这意味着它们不会减慢您网站的加载时间,并且它们会根据 Gradio 应用的大小自动调整其高度。 + +要使用 Web 组件嵌入: + +1. 通过在您的网站中添加以下脚本来导入 gradio JS 库(在 URL 中替换{GRADIO_VERSION}为您使用的 Gradio 库的版本)。 + + ```html + + <script type="module" + src="https://gradio.s3-us-west-2.amazonaws.com/{GRADIO_VERSION}/gradio.js"> + </script> + ``` + +2. 在您想放置应用的位置添加 + `html +<gradio-app src="https://$your_space_host.hf.space"></gradio-app> + ` + 元素。将 `src=` 属性设置为您的 Space 的嵌入 URL,您可以在“嵌入此空间”按钮中找到。例如: + + ```html + + <gradio-app src="https://abidlabs-pytorch-image-classifier.hf.space"></gradio-app> + ``` + + + +您可以在 Gradio 首页 上查看 Web 组件的示例。 + +您还可以使用传递给 `` 标签的属性来自定义 Web 组件的外观和行为: + +- `src`:如前所述,`src` 属性链接到您想要嵌入的托管 Gradio 演示的 URL +- `space`:一个可选的缩写,如果您的 Gradio 演示托管在 Hugging Face Space 上。接受 `username/space_name` 而不是完整的 URL。示例:`gradio/Echocardiogram-Segmentation`。如果提供了此属性,则不需要提供 `src`。 +- `control_page_title`:一个布尔值,指定是否将 html 标题设置为 Gradio 应用的标题(默认为 `"false"`) +- `initial_height`:加载 Gradio 应用时 Web 组件的初始高度(默认为 `"300px"`)。请注意,最终高度是根据 Gradio 应用的大小设置的。 +- `container`:是否显示边框框架和有关 Space 托管位置的信息(默认为 `"true"`) +- `info`:是否仅显示有关 Space 托管位置的信息在嵌入的应用程序下方(默认为 `"true"`) +- `autoscroll`:在预测完成后是否自动滚动到输出(默认为 `"false"`) +- `eager`:在页面加载时是否立即加载 Gradio 应用(默认为 `"false"`) +- `theme_mode`:是否使用 `dark`,`light` 或默认的 `system` 主题模式(默认为 `"system"`) + +以下是使用这些属性创建一个懒加载且初始高度为 0px 的 Gradio 应用的示例。 + +```html +<gradio-app space="gradio/Echocardiogram-Segmentation" eager="true" +initial_height="0px"></gradio-app> +``` + +_ 注意:Gradio 的 CSS 永远不会影响嵌入页面,但嵌入页面可以影响嵌入的 Gradio 应用的样式。请确保父页面中的任何 CSS 不是如此通用,以至于它也可能适用于嵌入的 Gradio 应用并导致样式破裂。例如,元素选择器如 `header { ... }` 和 `footer { ... }` 最可能引起问题。_ + +### 使用 IFrames 嵌入 + +如果您无法向网站添加 javascript(例如),则可以改为使用 IFrames 进行嵌入,请添加以下元素: + +```html +<iframe src="https://$your_space_host.hf.space"></iframe> +``` + +同样,您可以在“嵌入此空间”按钮中找到您的 Space 的嵌入 URL 的 `src=` 属性。 + +注意:如果您使用 IFrames,您可能希望添加一个固定的 `height` 属性,并设置 `style="border:0;"` 以去除边框。此外,如果您的应用程序需要诸如访问摄像头或麦克风之类的权限,您还需要使用 `allow` 属性提供它们。 + +## API 页面 + +$demo_hello_world + +如果您点击并打开上面的空间,您会在应用的页脚看到一个“通过 API 使用”链接。 + +![通过 API 使用](/assets/guides/use_via_api.png) + +这是一个文档页面,记录了用户可以使用的 REST API 来查询“Interface”函数。`Blocks` 应用程序也可以生成 API 页面,但必须为每个事件监听器显式命名 API,例如: + +```python +btn.click(add, [num1, num2], output, api_name="addition") +``` + +这将记录自动生成的 API 页面的端点 `/api/addition/`。 + +_注意_:对于启用了[队列功能](https://gradio.app/key-features#queuing)的 Gradio 应用程序,如果用户向您的 API 端点发出 POST 请求,他们可以绕过队列。要禁用此行为,请在 `queue()` 方法中设置 `api_open=False`。 + +## 鉴权 + +您可能希望在您的应用程序前面放置一个鉴权页面,以限制谁可以打开您的应用程序。使用 `launch()` 方法中的 `auth=` 关键字参数,您可以提供一个包含用户名和密码的元组,或者一个可接受的用户名 / 密码元组列表;以下是一个为单个名为“admin”的用户提供基于密码的身份验证的示例: + +```python +demo.launch(auth=("admin", "pass1234")) +``` + +对于更复杂的身份验证处理,您甚至可以传递一个以用户名和密码作为参数的函数,并返回 True 以允许身份验证,否则返回 False。这可用于访问第三方身份验证服务等其他功能。 + +以下是一个接受任何用户名和密码相同的登录的函数示例: + +```python +def same_auth(username, password): + return username == password +demo.launch(auth=same_auth) +``` + +为了使身份验证正常工作,必须在浏览器中启用第三方 Cookie。 +默认情况下,Safari、Chrome 隐私模式不会启用此功能。 + +## 直接访问网络请求 + +当用户向您的应用程序进行预测时,您可能需要底层的网络请求,以获取请求标头(例如用于高级身份验证)、记录客户端的 IP 地址或其他原因。Gradio 支持与 FastAPI 类似的方式:只需添加一个类型提示为 `gr.Request` 的函数参数,Gradio 将将网络请求作为该参数传递进来。以下是一个示例: + +```python +import gradio as gr + +def echo(name, request: gr.Request): + if request: + print("Request headers dictionary:", request.headers) + print("IP address:", request.client.host) + return name + +io = gr.Interface(echo, "textbox", "textbox").launch() +``` + +注意:如果直接调用函数而不是通过 UI(例如在缓存示例时),则 `request` 将为 `None`。您应该明确处理此情况,以确保您的应用程序不会抛出任何错误。这就是为什么我们有显式检查 `if request`。 + +## 嵌入到另一个 FastAPI 应用程序中 + +在某些情况下,您可能已经有一个现有的 FastAPI 应用程序,并且您想要为 Gradio 演示添加一个路径。 +您可以使用 `gradio.mount_gradio_app()` 来轻松实现此目的。 + +以下是一个完整的示例: + +$code_custom_path + +请注意,此方法还允许您在自定义路径上运行 Gradio 应用程序(例如上面的 `http://localhost:8000/gradio`)。 + +## 安全性和文件访问 + +与他人共享 Gradio 应用程序(通过 Spaces、您自己的服务器或临时共享链接进行托管)将主机机器上的某些文件**暴露**给您的 Gradio 应用程序的用户。 + +特别是,Gradio 应用程序允许用户访问以下三类文件: + +- **与 Gradio 脚本所在目录(或子目录)中的文件相同。** 例如,如果您的 Gradio 脚本的路径是 `/home/usr/scripts/project/app.py`,并且您从 `/home/usr/scripts/project/` 启动它,则共享 Gradio 应用程序的用户将能够访问 `/home/usr/scripts/project/` 中的任何文件。这样做是为了您可以在 Gradio 应用程序中轻松引用这些文件(例如应用程序的“示例”)。 + +- **Gradio 创建的临时文件。** 这些是由 Gradio 作为运行您的预测函数的一部分创建的文件。例如,如果您的预测函数返回一个视频文件,则 Gradio 将该视频保存到临时文件中,然后将临时文件的路径发送到前端。您可以通过设置环境变量 `GRADIO_TEMP_DIR` 为绝对路径(例如 `/home/usr/scripts/project/temp/`)来自定义 Gradio 创建的临时文件的位置。 + +- **通过 `launch()` 中的 `allowed_paths` 参数允许的文件。** 此参数允许您传递一个包含其他目录或确切文件路径的列表,以允许用户访问它们。(默认情况下,此参数为空列表)。 + +Gradio**不允许**访问以下内容: + +- **点文件**(其名称以 '.' 开头的任何文件)或其名称以 '.' 开头的任何目录中的任何文件。 + +- **通过 `launch()` 中的 `blocked_paths` 参数允许的文件。** 您可以将其他目录或确切文件路径的列表传递给 `launch()` 中的 `blocked_paths` 参数。此参数优先于 Gradio 默认或 `allowed_paths` 允许的文件。 + +- **主机机器上的任何其他路径**。用户不应能够访问主机上的其他任意路径。 + +请确保您正在运行最新版本的 `gradio`,以使这些安全设置生效。 diff --git a/guides/cn/02_building-interfaces/01_interface-state.md b/guides/cn/02_building-interfaces/01_interface-state.md new file mode 100644 index 0000000..855e34b --- /dev/null +++ b/guides/cn/02_building-interfaces/01_interface-state.md @@ -0,0 +1,28 @@ +# 接口状态 (Interface State) + +本指南介绍了 Gradio 中如何处理状态。了解全局状态和会话状态的区别,以及如何同时使用它们。 + +## 全局状态 (Global State) + +您的函数可能使用超出单个函数调用的持久性数据。如果数据是所有函数调用和所有用户都可访问的内容,您可以在函数调用外部创建一个变量,并在函数内部访问它。例如,您可能会在函数外部加载一个大模型,并在函数内部使用它,以便每个函数调用都不需要重新加载模型。 + +$code_score_tracker + +在上面的代码中,'scores' 数组在所有用户之间共享。如果多个用户访问此演示,他们的得分将全部添加到同一列表中,并且返回的前 3 个得分将从此共享引用中收集。 + +## 全局状态 (Global State) + +Gradio 支持的另一种数据持久性是会话状态,其中数据在页面会话中的多个提交之间持久存在。但是,不同用户之间的数据*不*共享。要将数据存储在会话状态中,需要执行以下三个步骤: + +1. 将额外的参数传递给您的函数,表示接口的状态。 +2. 在函数的末尾,作为额外的返回值返回状态的更新值。 +3. 在创建界面时添加 `'state'` 输入和 `'state'` 输出组件。 + +聊天机器人就是需要会话状态的一个例子 - 您希望访问用户之前的提交,但不能将聊天记录存储在全局变量中,因为这样聊天记录会在不同用户之间混乱。 + +$code_chatbot_dialogpt +$demo_chatbot_dialogpt + +请注意,在每个页面中,状态在提交之间保持不变,但是如果在另一个标签中加载此演示(或刷新页面),演示将不共享聊天记录。 + +`state` 的默认值为 None。如果您将默认值传递给函数的状态参数,则该默认值将用作状态的默认值。`Interface` 类仅支持单个输入和输出状态变量,但可以是具有多个元素的列表。对于更复杂的用例,您可以使用 Blocks,[它支持多个 `State` 变量](/state_in_blocks/)。 diff --git a/guides/cn/02_building-interfaces/02_reactive-interfaces.md b/guides/cn/02_building-interfaces/02_reactive-interfaces.md new file mode 100644 index 0000000..9cd7daa --- /dev/null +++ b/guides/cn/02_building-interfaces/02_reactive-interfaces.md @@ -0,0 +1,22 @@ +# 反应式界面 (Reactive Interfaces) + +本指南介绍了如何使 Gradio 界面自动刷新或连续流式传输数据。 + +## 实时界面 (Live Interfaces) + +您可以通过在界面中设置 `live=True` 来使界面自动刷新。现在,只要用户输入发生变化,界面就会重新计算。 + +$code_calculator_live +$demo_calculator_live + +注意,因为界面在更改时会自动重新提交,所以没有提交按钮。 + +## 流式组件 (Streaming Components) + +某些组件具有“流式”模式,比如麦克风模式下的 `Audio` 组件或网络摄像头模式下的 `Image` 组件。流式传输意味着数据会持续发送到后端,并且 `Interface` 函数会持续重新运行。 + +当在 `gr.Interface(live=True)` 中同时使用 `gr.Audio(sources=['microphone'])` 和 `gr.Audio(sources=['microphone'], streaming=True)` 时,两者的区别在于第一个 `Component` 会在用户停止录制时自动提交数据并运行 `Interface` 函数,而第二个 `Component` 会在录制过程中持续发送数据并运行 `Interface` 函数。 + +以下是从网络摄像头实时流式传输图像的示例代码。 + +$code_stream_frames diff --git a/guides/cn/02_building-interfaces/03_more-on-examples.md b/guides/cn/02_building-interfaces/03_more-on-examples.md new file mode 100644 index 0000000..9d1407b --- /dev/null +++ b/guides/cn/02_building-interfaces/03_more-on-examples.md @@ -0,0 +1,41 @@ +# 更多示例 (More on Examples) + +本指南介绍了有关示例的更多内容:从目录中加载示例,提供部分示例和缓存。如果你对示例还不熟悉,请查看 [关键特性](../key-features/#example-inputs) 指南中的介绍。 + +## 提供示例 (Providing Examples) + +正如 [关键特性](../key-features/#example-inputs) 指南中所介绍的,向接口添加示例就像提供一个列表的列表给 `examples` 关键字参数一样简单。 +每个子列表都是一个数据样本,其中每个元素对应于预测函数的一个输入。 +输入必须按照与预测函数期望的顺序排序。 + +如果你的接口只有一个输入组件,那么可以将示例提供为常规列表,而不是列表的列表。 + +### 从目录加载示例 (Loading Examples from a Directory) + +你还可以指定一个包含示例的目录路径。如果你的接口只接受单个文件类型的输入(例如图像分类器),你只需将目录文件路径传递给 `examples=` 参数,`Interface` 将加载目录中的图像作为示例。 +对于多个输入,该目录必须包含一个带有示例值的 log.csv 文件。 +在计算器演示的上下文中,我们可以设置 `examples='/demo/calculator/examples'` ,在该目录中包含以下 `log.csv` 文件: +contain a log.csv file with the example values. +In the context of the calculator demo, we can set `examples='/demo/calculator/examples'` and in that directory we include the following `log.csv` file: + +```csv +num,operation,num2 +5,"add",3 +4,"divide",2 +5,"multiply",3 +``` + +当浏览标记数据时,这将非常有用。只需指向标记目录,`Interface` 将从标记数据加载示例。 + +### 提供部分示例 + +有时你的应用程序有许多输入组件,但你只想为其中的一部分提供示例。为了在示例中排除某些输入,对于那些特定输入对应的所有数据样本都传递 `None`。 + +## 示例缓存 (Caching examples) + +你可能希望为用户提供一些模型的缓存示例,以便他们可以快速尝试,以防您的模型运行时间较长。 +如果 `cache_examples=True` ,当你调用 `launch()` 方法时,`Interface` 将运行所有示例,并保存输出。这些数据将保存在一个名为 `gradio_cached_examples` 的目录中。 + +每当用户点击示例时,输出将自动填充到应用程序中,使用来自该缓存目录的数据,而不是实际运行函数。这对于用户可以快速尝试您的模型而不增加任何负载是非常有用的! + +请记住一旦生成了缓存,它将不会在以后的启动中更新。如果示例或函数逻辑发生更改,请删除缓存文件夹以清除缓存并使用另一个 `launch()` 重新构建它。 diff --git a/guides/cn/02_building-interfaces/04_four-kinds-of-interfaces.md b/guides/cn/02_building-interfaces/04_four-kinds-of-interfaces.md new file mode 100644 index 0000000..150d5c4 --- /dev/null +++ b/guides/cn/02_building-interfaces/04_four-kinds-of-interfaces.md @@ -0,0 +1,44 @@ +# Gradio 界面的 4 种类型 + +到目前为止,我们一直假设构建 Gradio 演示需要同时具备输入和输出。但对于机器学习演示来说,并不总是如此:例如,*无条件图像生成模型*不需要任何输入,但会生成一张图像作为输出。 + +事实证明,`gradio.Interface` 类实际上可以处理 4 种不同类型的演示: + +1. **Standard demos 标准演示**:同时具有独立的输入和输出(例如图像分类器或语音转文本模型) +2. **Output-only demos 仅输出演示**:不接受任何输入,但会产生输出(例如无条件图像生成模型) +3. **Input-only demos 仅输入演示**:不产生任何输出,但会接受某种形式的输入(例如保存您上传到外部持久数据库的图像的演示) +4. **Unified demos 统一演示**:同时具有输入和输出组件,但这些组件是*相同的*。这意味着生成的输出将覆盖输入(例如文本自动完成模型) + +根据演示类型的不同,用户界面(UI)会有略微不同的外观: + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/interfaces4.png) + +我们来看一下如何使用 `Interface` 类构建每种类型的演示,以及示例: + +## 标准演示 (Standard demos) + +要创建具有输入和输出组件的演示,只需在 `Interface()` 中设置 `inputs` 和 `outputs` 参数的值。以下是一个简单图像滤镜的示例演示: + +$code_sepia_filter +$demo_sepia_filter + +## 仅输出演示 (Output-only demos) + +那么仅包含输出的演示呢?为了构建这样的演示,只需将 `Interface()` 中的 `inputs` 参数值设置为 `None`。以下是模拟图像生成模型的示例演示: + +$code_fake_gan_no_input +$demo_fake_gan_no_input + +## 仅输入演示 (Input-only demos) + +同样地,要创建仅包含输入的演示,将 `Interface()` 中的 `outputs` 参数值设置为 `None`。以下是将任何上传的图像保存到磁盘的示例演示: + +$code_save_file_no_output +$demo_save_file_no_output + +## 统一演示 (Unified demos) + +这种演示将单个组件同时作为输入和输出。只需将 `Interface()` 中的 `inputs` 和 `outputs` 参数值设置为相同的组件即可创建此演示。以下是文本生成模型的示例演示: + +$code_unified_demo_text_generation +$demo_unified_demo_text_generation diff --git a/guides/cn/03_building-with-blocks/01_blocks-and-event-listeners.md b/guides/cn/03_building-with-blocks/01_blocks-and-event-listeners.md new file mode 100644 index 0000000..211edae --- /dev/null +++ b/guides/cn/03_building-with-blocks/01_blocks-and-event-listeners.md @@ -0,0 +1,157 @@ +# 区块和事件监听器 (Blocks and Event Listeners) + +我们在[快速入门](https://gradio.app/blocks-and-event-listeners)中简要介绍了区块。让我们深入探讨一下。本指南将涵盖区块的结构、事件监听器及其类型、连续运行事件、更新配置以及使用字典与列表。 + +## 区块结构 (Blocks Structure) + +请查看下面的演示。 + +$code_hello_blocks +$demo_hello_blocks + +- 首先,注意 `with gr.Blocks() as demo:` 子句。区块应用程序代码将被包含在该子句中。 +- 接下来是组件。这些组件是在 `Interface` 中使用的相同组件。但是,与将组件传递给某个构造函数不同,组件在 `with` 子句内创建时会自动添加到区块中。 +- 最后,`click()` 事件监听器。事件监听器定义了应用程序内的数据流。在上面的示例中,监听器将两个文本框相互关联。文本框 `name` 作为输入,文本框 `output` 作为 `greet` 方法的输出。当单击按钮 `greet_btn` 时触发此数据流。与界面类似,事件监听器可以具有多个输入或输出。 + +## 事件监听器与交互性 (Event Listeners and Interactivity) + +在上面的示例中,您会注意到可以编辑文本框 `name`,但无法编辑文本框 `output`。这是因为作为事件监听器的任何组件都具有交互性。然而,由于文本框 `output` 仅作为输出,它没有交互性。您可以使用 `interactive=` 关键字参数直接配置组件的交互性。 + +```python +output = gr.Textbox(label="输出", interactive=True) +``` + +## 事件监听器的类型 (Types of Event Listeners) + +请查看下面的演示: + +$code_blocks_hello +$demo_blocks_hello + +`welcome` 函数不是由点击触发的,而是由在文本框 `inp` 中输入文字触发的。这是由于 `change()` 事件监听器。不同的组件支持不同的事件监听器。例如,`Video` 组件支持一个 `play()` 事件监听器,当用户按下播放按钮时触发。有关每个组件的事件监听器,请参见[文档](http://gradio.app/docs/components)。 + +## 多个数据流 (Multiple Data Flows) + +区块应用程序不像界面那样限制于单个数据流。请查看下面的演示: + +$code_reversible_flow +$demo_reversible_flow + +请注意,`num1` 可以充当 `num2` 的输入,反之亦然!随着应用程序变得更加复杂,您将能够连接各种组件的多个数据流。 + +下面是一个 " 多步骤 " 示例,其中一个模型的输出(语音到文本模型)被传递给下一个模型(情感分类器)。 + +$code_blocks_speech_text_sentiment +$demo_blocks_speech_text_sentiment + +## 函数输入列表与集合 (Function Input List vs Set) + +到目前为止,您看到的事件监听器都只有一个输入组件。如果您希望有多个输入组件将数据传递给函数,有两种选项可供函数接受输入组件值: + +1. 作为参数列表,或 +2. 作为以组件为键的单个值字典 + +让我们分别看一个例子: +$code_calculator_list_and_dict + +`add()` 和 `sub()` 都将 `a` 和 `b` 作为输入。然而,这些监听器之间的语法不同。 + +1. 对于 `add_btn` 监听器,我们将输入作为列表传递。函数 `add()` 将每个输入作为参数。`a` 的值映射到参数 `num1`,`b` 的值映射到参数 `num2`。 +2. 对于 `sub_btn` 监听器,我们将输入作为集合传递(注意花括号!)。当您传递集合时,函数 `sub()` 接受一个名为 `data` 的单个字典参数,其中键是输入组件,值是这些组件的值。 + +使用哪种语法是个人偏好!对于具有许多输入组件的函数,选项 2 可能更容易管理。 + +$demo_calculator_list_and_dict + +## 函数返回列表与字典 (Function Return List vs Dict) + +类似地,您可以返回多个输出组件的值,可以是: + +1. 值列表,或 +2. 以组件为键的字典 + +首先让我们看一个(1)的示例,其中我们通过返回两个值来设置两个输出组件的值: + +```python +with gr.Blocks() as demo: + food_box = gr.Number(value=10, label="Food Count") + status_box = gr.Textbox() + def eat(food): + if food > 0: + return food - 1, "full" + else: + return 0, "hungry" + gr.Button("EAT").click( + fn=eat, + inputs=food_box, + outputs=[food_box, status_box] + ) +``` + +上面的每个返回语句分别返回与 `food_box` 和 `status_box` 相对应的两个值。 + +除了返回与每个输出组件顺序相对应的值列表外,您还可以返回一个字典,其中键对应于输出组件,值作为新值。这还允许您跳过更新某些输出组件。 + +```python +with gr.Blocks() as demo: + food_box = gr.Number(value=10, label="Food Count") + status_box = gr.Textbox() + def eat(food): + if food > 0: + return {food_box: food - 1, status_box: "full"} + else: + return {status_box: "hungry"} + gr.Button("EAT").click( + fn=eat, + inputs=food_box, + outputs=[food_box, status_box] + ) +``` + +注意,在没有食物的情况下,我们只更新 `status_box` 元素。我们跳过更新 `food_box` 组件。 + +字典返回在事件监听器影响多个组件的返回值或有条件地影响输出时非常有用。 + +请记住,对于字典返回,我们仍然需要在事件监听器中指定可能的输出组件。 + +## 更新组件配置 (Updating Component Configurations) + +事件监听器函数的返回值通常是相应输出组件的更新值。有时我们还希望更新组件的配置,例如可见性。在这种情况下,我们返回一个 `gr.update()` 对象,而不仅仅是更新组件的值。 + +$code_blocks_essay_simple +$demo_blocks_essay_simple + +请注意,我们可以通过 `gr.update()` 方法自我配置文本框。`value=` 参数仍然可以用于更新值以及组件配置。 + +## 连续运行事件 (Running Events Consecutively) + +你也可以使用事件监听器的 `then` 方法按顺序运行事件。在前一个事件运行完成后,这将运行下一个事件。这对于多步更新组件的事件非常有用。 + +例如,在下面的聊天机器人示例中,我们首先立即使用用户消息更新聊天机器人,然后在模拟延迟后使用计算机回复更新聊天机器人。 + +$code_chatbot_simple +$demo_chatbot_simple + +事件监听器的 `.then()` 方法会执行后续事件,无论前一个事件是否引发任何错误。如果只想在前一个事件成功执行后才运行后续事件,请使用 `.success()` 方法,该方法与 `.then()` 接受相同的参数。 + +## 连续运行事件 (Running Events Continuously) + +您可以使用事件监听器的 `every` 参数按固定计划运行事件。这将在客户端连接打开的情况下,每隔一定秒数运行一次事件。如果连接关闭,事件将在下一次迭代后停止运行。 +请注意,这不考虑事件本身的运行时间。因此,使用 `every=gr.Timer(5)` 运行时间为 1 秒的函数实际上每 6 秒运行一次。 + +以下是每秒更新的正弦曲线示例! + +$code_sine_curve +$demo_sine_curve + +## 收集事件数据 (Gathering Event Data) + +您可以通过将相关的事件数据类作为类型提示添加到事件监听器函数的参数中,收集有关事件的特定数据。 + +例如,使用 `gradio.SelectData` 参数可以为 `.select()` 的事件数据添加类型提示。当用户选择触发组件的一部分时,将触发此事件,并且事件数据包含有关用户的具体选择的信息。如果用户在 `Textbox` 中选择了特定单词,在 `Gallery` 中选择了特定图像或在 `DataFrame` 中选择了特定单元格,则事件数据参数将包含有关具体选择的信息。 + +在下面的双人井字游戏演示中,用户可以选择 `DataFrame` 中的一个单元格进行移动。事件数据参数包含有关所选单元格的信息。我们可以首先检查单元格是否为空,然后用用户的移动更新单元格。 + +$code_tictactoe + +$demo_tictactoe diff --git a/guides/cn/03_building-with-blocks/02_controlling-layout.md b/guides/cn/03_building-with-blocks/02_controlling-layout.md new file mode 100644 index 0000000..92e9940 --- /dev/null +++ b/guides/cn/03_building-with-blocks/02_controlling-layout.md @@ -0,0 +1,95 @@ +# 控制布局 (Controlling Layout) + +默认情况下,块中的组件是垂直排列的。让我们看看如何重新排列组件。在幕后,这种布局结构使用了[Web 开发的 flexbox 模型](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox)。 + +## Row 行 + +`with gr.Row` 下的元素将水平显示。例如,要并排显示两个按钮: + +```python +with gr.Blocks() as demo: + with gr.Row(): + btn1 = gr.Button("按钮1") + btn2 = gr.Button("按钮2") +``` + +要使行中的每个元素具有相同的高度,请使用 `style` 方法的 `equal_height` 参数。 + +```python +with gr.Blocks() as demo: + with gr.Row(equal_height=True): + textbox = gr.Textbox() + btn2 = gr.Button("按钮2") +``` + +可以通过每个组件中存在的 `scale` 和 `min_width` 参数来控制行中元素的宽度。 + +- `scale` 是一个整数,定义了元素在行中的占用空间。如果将 scale 设置为 `0`,则元素不会扩展占用空间。如果将 scale 设置为 `1` 或更大,则元素将扩展。行中的多个元素将按比例扩展。在下面的示例中,`btn1` 将比 `btn2` 扩展两倍,而 `btn0` 将根本不会扩展: + +```python +with gr.Blocks() as demo: + with gr.Row(): + btn0 = gr.Button("按钮0", scale=0) + btn1 = gr.Button("按钮1", scale=1) + btn2 = gr.Button("按钮2", scale=2) +``` + +- `min_width` 将设置元素的最小宽度。如果没有足够的空间满足所有的 `min_width` 值,行将换行。 + +在[文档](https://gradio.app/docs/row)中了解有关行的更多信息。 + +## 列和嵌套 (Columns and Nesting) + +列中的组件将垂直放置在一起。由于默认布局对于块应用程序来说是垂直布局,因此为了有用,列通常嵌套在行中。例如: + +$code_rows_and_columns +$demo_rows_and_columns + +查看第一列如何垂直排列两个文本框。第二列垂直排列图像和按钮。注意两列的相对宽度由 `scale` 参数设置。具有两倍 `scale` 值的列占据两倍的宽度。 + +在[文档](https://gradio.app/docs/column)中了解有关列的更多信息。 + +## 选项卡和手风琴 (Tabs and Accordions) + +您还可以使用 `with gr.Tab('tab_name'):` 语句创建选项卡。在 `with gr.Tab('tab_name'):` 上下文中创建的任何组件都将显示在该选项卡中。连续的 Tab 子句被分组在一起,以便一次只能选择一个选项卡,并且只显示该选项卡上下文中的组件。 + +例如: + +$code_blocks_flipper +$demo_blocks_flipper + +还请注意本示例中的 `gr.Accordion('label')`。手风琴是一种可以切换打开或关闭的布局。与 `Tabs` 一样,它是可以选择性隐藏或显示内容的布局元素。在 `with gr.Accordion('label'):` 内定义的任何组件在单击手风琴的切换图标时都会被隐藏或显示。 + +在文档中了解有关[Tabs](https://gradio.app/docs/tab)和[Accordions](https://gradio.app/docs/accordion)的更多信息。 + +## 可见性 (Visibility) + +组件和布局元素都有一个 `visible` 参数,可以在初始时设置,并使用 `gr.update()` 进行更新。在 Column 上设置 `gr.update(visible=...)` 可用于显示或隐藏一组组件。 + +$code_blocks_form +$demo_blocks_form + +## 可变数量的输出 (Variable Number of Outputs) + +通过以动态方式调整组件的可见性,可以创建支持 _可变数量输出_ 的 Gradio 演示。这是一个非常简单的例子,其中输出文本框的数量由输入滑块控制: + +例如: + +$code_variable_outputs +$demo_variable_outputs + +## 分开定义和渲染组件 (Defining and Rendering Components Separately) + +在某些情况下,您可能希望在实际渲染 UI 之前定义组件。例如,您可能希望在相应的 `gr.Textbox` 输入上方显示示例部分,使用 `gr.Examples`。由于 `gr.Examples` 需要一个参数作为输入组件对象,您需要先定义输入组件,然后在定义 `gr.Examples` 对象之后再渲染它。 + +解决方法是在 `gr.Blocks()` 范围之外定义 `gr.Textbox`,并在 UI 中想要放置它的位置使用组件的 `.render()` 方法。 + +这是一个完整的代码示例: + +```python +input_textbox = gr.Textbox() + +with gr.Blocks() as demo: + gr.Examples(["hello", "bonjour", "merhaba"], input_textbox) + input_textbox.render() +``` diff --git a/guides/cn/03_building-with-blocks/03_state-in-blocks.md b/guides/cn/03_building-with-blocks/03_state-in-blocks.md new file mode 100644 index 0000000..91de176 --- /dev/null +++ b/guides/cn/03_building-with-blocks/03_state-in-blocks.md @@ -0,0 +1,30 @@ +# 分块状态 (State in Blocks) + +我们已经介绍了[接口状态](https://gradio.app/interface-state),这篇指南将介绍分块状态,它的工作原理大致相同。 + +## 全局状态 (Global State) + +分块中的全局状态与接口中的全局状态相同。在函数调用外创建的任何变量都是在所有用户之间共享的引用。 + +## 会话状态 (Session State) + +Gradio 在分块应用程序中同样支持会话**状态**,即在页面会话中跨多次提交保持的数据。需要再次强调,会话数据*不会*在模型的不同用户之间共享。要在会话状态中存储数据,需要完成以下三个步骤: + +1. 创建一个 `gr.State()` 对象。如果此可状态对象有一个默认值,请将其传递给构造函数。 +2. 在事件监听器中,将 `State` 对象作为输入和输出。 +3. 在事件监听器函数中,将变量添加到输入参数和返回值中。 + +让我们来看一个猜词游戏的例子。 + +$code_hangman +$demo_hangman + +让我们看看在这个游戏中如何完成上述的 3 个步骤: + +1. 我们将已使用的字母存储在 `used_letters_var` 中。在 `State` 的构造函数中,将其初始值设置为空列表`[]`。 +2. 在 `btn.click()` 中,我们在输入和输出中都引用了 `used_letters_var`。 +3. 在 `guess_letter` 中,我们将此 `State` 的值传递给 `used_letters`,然后在返回语句中返回更新后的该 `State` 的值。 + +对于更复杂的应用程序,您可能会在一个单独的分块应用程序中使用许多存储会话状态的 `State` 变量。 + +在[文档](https://gradio.app/docs/state)中了解更多关于 `State` 的信息。 diff --git a/guides/cn/03_building-with-blocks/04_custom-CSS-and-JS.md b/guides/cn/03_building-with-blocks/04_custom-CSS-and-JS.md new file mode 100644 index 0000000..ebdec04 --- /dev/null +++ b/guides/cn/03_building-with-blocks/04_custom-CSS-and-JS.md @@ -0,0 +1,58 @@ +# 自定义的 JS 和 CSS + +本指南介绍了如何更灵活地为 Blocks 添加样式,并添加 JavaScript 代码到事件监听器中。 + +**警告**:在自定义的 JS 和 CSS 中使用查询选择器不能保证能在所有 Gradio 版本中正常工作,因为 Gradio 的 HTML DOM 可能会发生变化。我们建议谨慎使用查询选择器。 + +## 自定义的 CSS + +Gradio 主题是自定义应用程序外观和感觉的最简单方式。您可以从各种主题中进行选择,或者创建自己的主题。要实现这一点,请将 `theme=` kwarg 传递给 `Blocks` 构造函数。例如: + +```python +with gr.Blocks(theme=gr.themes.Glass()): + ... +``` + +Gradio 自带一套预构建的主题,您可以从 `gr.themes.*` 中加载这些主题。您可以扩展这些主题,或者从头开始创建自己的主题 - 有关更多详细信息,请参阅[主题指南](/theming-guide)。 + +要增加附加的样式能力,您可以使用 `css=` kwarg 将任何 CSS 传递给您的应用程序。 + +Gradio 应用程序的基类是 `gradio-container`,因此下面是一个示例,用于更改 Gradio 应用程序的背景颜色: + +```python +with gr.Blocks(css=".gradio-container {background-color: red}") as demo: + ... +``` + +如果您想在您的 CSS 中引用外部文件,请使用 `"file="` 作为文件路径的前缀(可以是相对路径或绝对路径),例如: + +```python +with gr.Blocks(css=".gradio-container {background: url('file=clouds.jpg')}") as demo: + ... +``` + +您还可以将 CSS 文件的文件路径传递给 `css` 参数。 + +## `elem_id` 和 `elem_classes` 参数 + +您可以使用 `elem_id` 来为任何组件添加 HTML 元素 `id`,并使用 `elem_classes` 添加一个类或类列表。这将使您能够更轻松地使用 CSS 选择元素。这种方法更有可能在 Gradio 版本之间保持稳定,因为内置的类名或 id 可能会发生变化(但正如上面的警告中所提到的,如果您使用自定义 CSS,我们不能保证在 Gradio 版本之间完全兼容,因为 DOM 元素本身可能会发生变化)。 + +```python +css = """ +#warning {background-color: #FFCCCB} +.feedback textarea {font-size: 24px !important} +""" + +with gr.Blocks(css=css) as demo: + box1 = gr.Textbox(value="Good Job", elem_classes="feedback") + box2 = gr.Textbox(value="Failure", elem_id="warning", elem_classes="feedback") +``` + +CSS `#warning` 规则集仅针对第二个文本框,而 `.feedback` 规则集将同时作用于两个文本框。请注意,在针对类时,您可能需要使用 `!important` 选择器来覆盖默认的 Gradio 样式。 + +## 自定义的 JS + +事件监听器具有 `_js` 参数,可以接受 JavaScript 函数作为字符串,并像 Python 事件监听器函数一样处理它。您可以传递 JavaScript 函数和 Python 函数(在这种情况下,先运行 JavaScript 函数),或者仅传递 JavaScript(并将 Python 的 `fn` 设置为 `None`)。请查看下面的代码: + +$code_blocks_js_methods +$demo_blocks_js_methods diff --git a/guides/cn/04_integrating-other-frameworks/01_using-hugging-face-integrations.md b/guides/cn/04_integrating-other-frameworks/01_using-hugging-face-integrations.md new file mode 100644 index 0000000..d5dc202 --- /dev/null +++ b/guides/cn/04_integrating-other-frameworks/01_using-hugging-face-integrations.md @@ -0,0 +1,136 @@ +# 使用 Hugging Face 集成 + +相关空间:https://huggingface.co/spaces/gradio/helsinki_translation_en_es +标签:HUB,SPACES,EMBED + +由 Omar Sanseviero 贡献🦙 + +## 介绍 + +Hugging Face Hub 是一个集成平台,拥有超过 190,000 个[模型](https://huggingface.co/models),32,000 个[数据集](https://huggingface.co/datasets)和 40,000 个[演示](https://huggingface.co/spaces),也被称为 Spaces。虽然 Hugging Face 以其🤗 transformers 和 diffusers 库而闻名,但 Hub 还支持许多机器学习库,如 PyTorch,TensorFlow,spaCy 等,涵盖了从计算机视觉到强化学习等各个领域。 + +Gradio 拥有多个功能,使其非常容易利用 Hub 上的现有模型和 Spaces。本指南将介绍这些功能。 + +## 使用 `pipeline` 进行常规推理 + +首先,让我们构建一个简单的界面,将英文翻译成西班牙文。在赫尔辛基大学共享的一千多个模型中,有一个[现有模型](https://huggingface.co/Helsinki-NLP/opus-mt-en-es),名为 `opus-mt-en-es`,可以正好做到这一点! + +🤗 transformers 库有一个非常易于使用的抽象层,[`pipeline()`](https://huggingface.co/docs/transformers/v4.16.2/en/main_classes/pipelines#transformers.pipeline)处理大部分复杂代码,为常见任务提供简单的 API。通过指定任务和(可选)模型,您可以使用几行代码使用现有模型: + +```python +import gradio as gr + +from transformers import pipeline + +pipe = pipeline("translation", model="Helsinki-NLP/opus-mt-en-es") + +def predict(text): + return pipe(text)[0]["translation_text"] + +demo = gr.Interface( + fn=predict, + inputs='text', + outputs='text', +) + +demo.launch() +``` + +但是,`gradio` 实际上使将 `pipeline` 转换为演示更加容易,只需使用 `gradio.Interface.from_pipeline` 方法,无需指定输入和输出组件: + +```python +from transformers import pipeline +import gradio as gr + +pipe = pipeline("translation", model="Helsinki-NLP/opus-mt-en-es") + +demo = gr.Interface.from_pipeline(pipe) +demo.launch() +``` + +上述代码生成了以下界面,您可以在浏览器中直接尝试: + + + +## Using Hugging Face Inference Endpoints + +Hugging Face 提供了一个名为[Serverless Inference Endpoints](https://huggingface.co/inference-api)的免费服务,允许您向 Hub 中的模型发送 HTTP 请求。对于基于 transformers 或 diffusers 的模型,API 的速度可以比自己运行推理快 2 到 10 倍。该 API 是免费的(受速率限制),您可以在想要在生产中使用时切换到专用的[推理端点](https://huggingface.co/pricing)。 + +让我们尝试使用推理 API 而不是自己加载模型的方式进行相同的演示。鉴于 Inference Endpoints 支持的 Hugging Face 模型,Gradio 可以自动推断出预期的输入和输出,并进行底层服务器调用,因此您不必担心定义预测函数。以下是代码示例! + +```python +import gradio as gr + +demo = gr.load("Helsinki-NLP/opus-mt-en-es", src="models") + +demo.launch() +``` + +请注意,我们只需指定模型名称并说明 `src` 应为 `models`(Hugging Face 的 Model Hub)。由于您不会在计算机上加载模型,因此无需安装任何依赖项(除了 `gradio`)。 + +您可能会注意到,第一次推理大约需要 20 秒。这是因为推理 API 正在服务器中加载模型。之后您会获得一些好处: + +- 推理速度更快。 +- 服务器缓存您的请求。 +- 您获得内置的自动缩放功能。 + +## 托管您的 Gradio 演示 + +[Hugging Face Spaces](https://hf.co/spaces)允许任何人免费托管其 Gradio 演示,上传 Gradio 演示只需几分钟。您可以前往[hf.co/new-space](https://huggingface.co/new-space),选择 Gradio SDK,创建一个 `app.py` 文件,完成!您将拥有一个可以与任何人共享的演示。要了解更多信息,请阅读[此指南以使用网站在 Hugging Face Spaces 上托管](https://huggingface.co/blog/gradio-spaces)。 + +或者,您可以通过使用[huggingface_hub client library](https://huggingface.co/docs/huggingface_hub/index)库来以编程方式创建一个 Space。这是一个示例: + +```python +from huggingface_hub import ( + create_repo, + get_full_repo_name, + upload_file, +) +create_repo(name=target_space_name, token=hf_token, repo_type="space", space_sdk="gradio") +repo_name = get_full_repo_name(model_id=target_space_name, token=hf_token) +file_url = upload_file( + path_or_fileobj="file.txt", + path_in_repo="app.py", + repo_id=repo_name, + repo_type="space", + token=hf_token, +) +``` + +在这里,`create_repo` 使用特定帐户的 Write Token 在特定帐户下创建一个带有目标名称的 gradio repo。`repo_name` 获取相关存储库的完整存储库名称。最后,`upload_file` 将文件上传到存储库中,并将其命名为 `app.py`。 + +## 在其他网站上嵌入您的 Space 演示 + +在本指南中,您已经看到了许多嵌入的 Gradio 演示。您也可以在自己的网站上这样做!第一步是创建一个包含您想展示的演示的 Hugging Face Space。然后,[按照此处的步骤将 Space 嵌入到您的网站上](/sharing-your-app/#embedding-hosted-spaces)。 + +## 从 Spaces 加载演示 + +您还可以在 Hugging Face Spaces 上使用和混合现有的 Gradio 演示。例如,您可以将两个现有的 Gradio 演示放在单独的选项卡中并创建一个新的演示。您可以在本地运行此新演示,或将其上传到 Spaces,为混合和创建新的演示提供无限可能性! + +以下是一个完全实现此目标的示例: + +```python +import gradio as gr + +with gr.Blocks() as demo: + with gr.Tab("Translate to Spanish"): + gr.load("gradio/helsinki_translation_en_es", src="spaces") + with gr.Tab("Translate to French"): + gr.load("abidlabs/en2fr", src="spaces") + +demo.launch() +``` + +请注意,我们使用了 `gr.load()`,这与使用推理 API 加载模型所使用的方法相同。但是,在这里,我们指定 `src` 为 `spaces`(Hugging Face Spaces)。 + +## 小结 + +就是这样!让我们回顾一下 Gradio 和 Hugging Face 共同工作的各种方式: + +1. 您可以使用 `from_pipeline()` 将 `transformers` pipeline 转换为 Gradio 演示 +2. 您可以使用 `gr.load()` 轻松地围绕推理 API 构建演示,而无需加载模型 +3. 您可以在 Hugging Face Spaces 上托管您的 Gradio 演示,可以使用 GUI 或完全使用 Python。 +4. 您可以将托管在 Hugging Face Spaces 上的 Gradio 演示嵌入到自己的网站上。 +5. 您可以使用 `gr.load()` 从 Hugging Face Spaces 加载演示,以重新混合和创建新的 Gradio 演示。 + +🤗 diff --git a/guides/cn/04_integrating-other-frameworks/Gradio-and-Comet.md b/guides/cn/04_integrating-other-frameworks/Gradio-and-Comet.md new file mode 100644 index 0000000..fd0aea7 --- /dev/null +++ b/guides/cn/04_integrating-other-frameworks/Gradio-and-Comet.md @@ -0,0 +1,269 @@ +# 使用 Gradio 和 Comet + +Tags: COMET, SPACES +由 Comet 团队贡献 + +## 介绍 + +在这个指南中,我们将展示您可以如何使用 Gradio 和 Comet。我们将介绍使用 Comet 和 Gradio 的基本知识,并向您展示如何利用 Gradio 的高级功能,如 [使用 iFrames 进行嵌入](https://www.gradio.app/sharing-your-app/#embedding-with-iframes) 和 [状态](https://www.gradio.app/docs/#state) 来构建一些令人惊叹的模型评估工作流程。 + +下面是本指南涵盖的主题列表。 + +1. 将 Gradio UI 记录到您的 Comet 实验中 +2. 直接将 Gradio 应用程序嵌入到您的 Comet 项目中 +3. 直接将 Hugging Face Spaces 嵌入到您的 Comet 项目中 +4. 将 Gradio 应用程序的模型推理记录到 Comet 中 + +## 什么是 Comet? + +[Comet](https://www.comet.com?utm_source=gradio&utm_medium=referral&utm_campaign=gradio-integration&utm_content=gradio-docs) 是一个 MLOps 平台,旨在帮助数据科学家和团队更快地构建更好的模型!Comet 提供工具来跟踪、解释、管理和监控您的模型,集中在一个地方!它可以与 Jupyter 笔记本和脚本配合使用,最重要的是,它是 100% 免费的! + +## 设置 + +首先,安装运行这些示例所需的依赖项 + +```shell +pip install comet_ml torch torchvision transformers gradio shap requests Pillow +``` + +接下来,您需要[注册一个 Comet 账户](https://www.comet.com/signup?utm_source=gradio&utm_medium=referral&utm_campaign=gradio-integration&utm_content=gradio-docs)。一旦您设置了您的账户,[获取您的 API 密钥](https://www.comet.com/docs/v2/guides/getting-started/quickstart/#get-an-api-key?utm_source=gradio&utm_medium=referral&utm_campaign=gradio-integration&utm_content=gradio-docs) 并配置您的 Comet 凭据 + +如果您将这些示例作为脚本运行,您可以将您的凭据导出为环境变量 + +```shell +export COMET_API_KEY="<您的 API 密钥>" +export COMET_WORKSPACE="<您的工作空间名称>" +export COMET_PROJECT_NAME="<您的项目名称>" +``` + +或者将它们设置在您的工作目录中的 `.comet.config` 文件中。您的文件应按以下方式格式化。 + +```shell +[comet] +api_key=<您的 API 密钥> +workspace=<您的工作空间名称> +project_name=<您的项目名称> +``` + +如果您使用提供的 Colab Notebooks 运行这些示例,请在开始 Gradio UI 之前运行带有以下片段的单元格。运行此单元格可以让您交互式地将 API 密钥添加到笔记本中。 + +```python +import comet_ml +comet_ml.init() +``` + +## 1. 将 Gradio UI 记录到您的 Comet 实验中 + +[![在 Colab 中打开](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/comet-ml/comet-examples/blob/master/integrations/model-evaluation/gradio/notebooks/Gradio_and_Comet.ipynb) + +在这个例子中,我们将介绍如何将您的 Gradio 应用程序记录到 Comet,并使用 Gradio 自定义面板与其进行交互。 + +我们先通过使用 `resnet18` 构建一个简单的图像分类示例。 + +```python +import comet_ml + +import requests +import torch +from PIL import Image +from torchvision import transforms + +torch.hub.download_url_to_file("https://github.com/pytorch/hub/raw/master/images/dog.jpg", "dog.jpg") + +if torch.cuda.is_available(): + device = "cuda" +else: + device = "cpu" + +model = torch.hub.load("pytorch/vision:v0.6.0", "resnet18", pretrained=True).eval() +model = model.to(device) + +# 为 ImageNet 下载可读的标签。 +response = requests.get("https://git.io/JJkYN") +labels = response.text.split("\n") + + +def predict(inp): + inp = Image.fromarray(inp.astype("uint8"), "RGB") + inp = transforms.ToTensor()(inp).unsqueeze(0) + with torch.no_grad(): + prediction = torch.nn.functional.softmax(model(inp.to(device))[0], dim=0) + return {labels[i]: float(prediction[i]) for i in range(1000)} + + +inputs = gr.Image() +outputs = gr.Label(num_top_classes=3) + +io = gr.Interface( + fn=predict, inputs=inputs, outputs=outputs, examples=["dog.jpg"] +) +io.launch(inline=False, share=True) + +experiment = comet_ml.Experiment() +experiment.add_tag("image-classifier") + +io.integrate(comet_ml=experiment) +``` + +此片段中的最后一行将将 Gradio 应用程序的 URL 记录到您的 Comet 实验中。您可以在实验的文本选项卡中找到该 URL。 + + + +将 Gradio 面板添加到您的实验中,与应用程序进行交互。 + + + +## 2. 直接将 Gradio 应用程序嵌入到您的 Comet 项目中 + + + +如果您要长期托管 Gradio 应用程序,可以使用 Gradio Panel Extended 自定义面板进行嵌入 UI。 + +转到您的 Comet 项目页面,转到面板选项卡。单击“+ 添加”按钮以打开面板搜索页面。 + +adding-panels + +接下来,在公共面板部分搜索 Gradio Panel Extended 并单击“添加”。 + +gradio-panel-extended + +添加面板后,单击“编辑”以访问面板选项页面,并粘贴您的 Gradio 应用程序的 URL。 + +![Edit-Gradio-Panel-Options](https://user-images.githubusercontent.com/7529846/214573001-23814b5a-ca65-4ace-a8a5-b27cdda70f7a.gif) + +Edit-Gradio-Panel-URL + +## 3. 直接将 Hugging Face Spaces 嵌入到您的 Comet 项目中 + + + +您还可以使用 Hugging Face Spaces 面板将托管在 Hugging Faces Spaces 中的 Gradio 应用程序嵌入到您的 Comet 项目中。 + +转到 Comet 项目页面,转到面板选项卡。单击“+添加”按钮以打开面板搜索页面。然后,在公共面板部分搜索 Hugging Face Spaces 面板并单击“添加”。 + +huggingface-spaces-panel + +添加面板后,单击“编辑”以访问面板选项页面,并粘贴您的 Hugging Face Space 路径,例如 `pytorch/ResNet` + +Edit-HF-Space + +## 4. 记录模型推断结果到 Comet + + + +[![在 Colab 中打开](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/comet-ml/comet-examples/blob/master/integrations/model-evaluation/gradio/notebooks/Logging_Model_Inferences_with_Comet_and_Gradio.ipynb) + +在前面的示例中,我们演示了通过 Comet UI 与 Gradio 应用程序交互的各种方法。此外,您还可以将 Gradio 应用程序的模型推断(例如 SHAP 图)记录到 Comet 中。 + +在以下代码段中,我们将记录来自文本生成模型的推断。我们可以使用 Gradio 的[State](https://www.gradio.app/docs/#state)对象在多次推断调用之间保持实验的持久性。这将使您能够将多个模型推断记录到单个实验中。 + +```python +import comet_ml +import gradio as gr +import shap +import torch +from transformers import AutoModelForCausalLM, AutoTokenizer + +if torch.cuda.is_available(): + device = "cuda" +else: + device = "cpu" + +MODEL_NAME = "gpt2" + +model = AutoModelForCausalLM.from_pretrained(MODEL_NAME) + +# set model decoder to true +model.config.is_decoder = True +# set text-generation params under task_specific_params +model.config.task_specific_params["text-generation"] = { + "do_sample": True, + "max_length": 50, + "temperature": 0.7, + "top_k": 50, + "no_repeat_ngram_size": 2, +} +model = model.to(device) + +tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) +explainer = shap.Explainer(model, tokenizer) + + +def start_experiment(): + """Returns an APIExperiment object that is thread safe + and can be used to log inferences to a single Experiment + """ + try: + api = comet_ml.API() + workspace = api.get_default_workspace() + project_name = comet_ml.config.get_config()["comet.project_name"] + + experiment = comet_ml.APIExperiment( + workspace=workspace, project_name=project_name + ) + experiment.log_other("Created from", "gradio-inference") + + message = f"Started Experiment: [{experiment.name}]({experiment.url})" + return (experiment, message) + + except Exception as e: + return None, None + + +def predict(text, state, message): + experiment = state + + shap_values = explainer([text]) + plot = shap.plots.text(shap_values, display=False) + + if experiment is not None: + experiment.log_other("message", message) + experiment.log_html(plot) + + return plot + + +with gr.Blocks() as demo: + start_experiment_btn = gr.Button("Start New Experiment") + experiment_status = gr.Markdown() + + # Log a message to the Experiment to provide more context + experiment_message = gr.Textbox(label="Experiment Message") + experiment = gr.State() + + input_text = gr.Textbox(label="Input Text", lines=5, interactive=True) + submit_btn = gr.Button("Submit") + + output = gr.HTML(interactive=True) + + start_experiment_btn.click( + start_experiment, outputs=[experiment, experiment_status] + ) + submit_btn.click( + predict, inputs=[input_text, experiment, experiment_message], outputs=[output] + ) +``` + +该代码段中的推断结果将保存在实验的 HTML 选项卡中。 + + + +## 结论 + +希望您对本指南有所裨益,并能为您构建出色的 Comet 和 Gradio 模型评估工作流程提供一些启示。 + +## 如何在 Comet 组织上贡献 Gradio 演示 + +- 在 Hugging Face 上创建帐号[此处](https://huggingface.co/join)。 +- 在用户名下添加 Gradio 演示,请参阅[此处](https://huggingface.co/course/chapter9/4?fw=pt)以设置 Gradio 演示。 +- 请求加入 Comet 组织[此处](https://huggingface.co/Comet)。 + +## 更多资源 + +- [Comet 文档](https://www.comet.com/docs/v2/?utm_source=gradio&utm_medium=referral&utm_campaign=gradio-integration&utm_content=gradio-docs) diff --git a/guides/cn/04_integrating-other-frameworks/Gradio-and-ONNX-on-Hugging-Face.md b/guides/cn/04_integrating-other-frameworks/Gradio-and-ONNX-on-Hugging-Face.md new file mode 100644 index 0000000..a8e8d94 --- /dev/null +++ b/guides/cn/04_integrating-other-frameworks/Gradio-and-ONNX-on-Hugging-Face.md @@ -0,0 +1,142 @@ +# Gradio 和 ONNX 在 Hugging Face 上 + +Related spaces: https://huggingface.co/spaces/onnx/EfficientNet-Lite4 +Tags: ONNX,SPACES +由 Gradio 和 ONNX 团队贡献 + +## 介绍 + +在这个指南中,我们将为您介绍以下内容: + +- ONNX、ONNX 模型仓库、Gradio 和 Hugging Face Spaces 的介绍 +- 如何为 EfficientNet-Lite4 设置 Gradio 演示 +- 如何为 Hugging Face 上的 ONNX 组织贡献自己的 Gradio 演示 + +下面是一个 ONNX 模型的示例:在下面尝试 EfficientNet-Lite4 演示。 + + + +## ONNX 模型仓库是什么? + +Open Neural Network Exchange([ONNX](https://onnx.ai/))是一种表示机器学习模型的开放标准格式。ONNX 由一个实现了该格式的合作伙伴社区支持,该社区将其实施到许多框架和工具中。例如,如果您在 TensorFlow 或 PyTorch 中训练了一个模型,您可以轻松地将其转换为 ONNX,然后使用类似 ONNX Runtime 的引擎 / 编译器在各种设备上运行它。 + +[ONNX 模型仓库](https://github.com/onnx/models)是由社区成员贡献的一组预训练的先进模型,格式为 ONNX。每个模型都附带了用于模型训练和运行推理的 Jupyter 笔记本。这些笔记本以 Python 编写,并包含到训练数据集的链接,以及描述模型架构的原始论文的参考文献。 + +## Hugging Face Spaces 和 Gradio 是什么? + +### Gradio + +Gradio 可让用户使用 Python 代码将其机器学习模型演示为 Web 应用程序。Gradio 将 Python 函数封装到用户界面中,演示可以在 jupyter 笔记本、colab 笔记本中启动,并可以嵌入到您自己的网站上,并在 Hugging Face Spaces 上免费托管。 + +在此处开始[https://gradio.app/getting_started](https://gradio.app/getting_started) + +### Hugging Face Spaces + +Hugging Face Spaces 是 Gradio 演示的免费托管选项。Spaces 提供了 3 种 SDK 选项:Gradio、Streamlit 和静态 HTML 演示。Spaces 可以是公共的或私有的,工作流程与 github repos 类似。目前 Hugging Face 上有 2000 多个 Spaces。在此处了解更多关于 Spaces 的信息[https://huggingface.co/spaces/launch](https://huggingface.co/spaces/launch)。 + +### Hugging Face 模型 + +Hugging Face 模型中心还支持 ONNX 模型,并且可以通过[ONNX 标签](https://huggingface.co/models?library=onnx&sort=downloads)对 ONNX 模型进行筛选 + +## Hugging Face 是如何帮助 ONNX 模型仓库的? + +ONNX 模型仓库中有许多 Jupyter 笔记本供用户测试模型。以前,用户需要自己下载模型并在本地运行这些笔记本测试。有了 Hugging Face,测试过程可以更简单和用户友好。用户可以在 Hugging Face Spaces 上轻松尝试 ONNX 模型仓库中的某个模型,并使用 ONNX Runtime 运行由 Gradio 提供支持的快速演示,全部在云端进行,无需在本地下载任何内容。请注意,ONNX 有各种运行时,例如[ONNX Runtime](https://github.com/microsoft/onnxruntime)、[MXNet](https://github.com/apache/incubator-mxnet)等 + +## ONNX Runtime 的作用是什么? + +ONNX Runtime 是一个跨平台的推理和训练机器学习加速器。它使得在 Hugging Face 上使用 ONNX 模型仓库中的模型进行实时 Gradio 演示成为可能。 + +ONNX Runtime 可以实现更快的客户体验和更低的成本,支持来自 PyTorch 和 TensorFlow/Keras 等深度学习框架以及 scikit-learn、LightGBM、XGBoost 等传统机器学习库的模型。ONNX Runtime 与不同的硬件、驱动程序和操作系统兼容,并通过利用适用的硬件加速器以及图形优化和转换提供最佳性能。有关更多信息,请参阅[官方网站](https://onnxruntime.ai/)。 + +## 为 EfficientNet-Lite4 设置 Gradio 演示 + +EfficientNet-Lite 4 是 EfficientNet-Lite 系列中最大和最准确的模型。它是一个仅使用整数量化的模型,能够在所有 EfficientNet 模型中提供最高的准确率。在 Pixel 4 CPU 上以实时方式运行(例如 30ms/ 图像)时,可以实现 80.4%的 ImageNet top-1 准确率。要了解更多信息,请阅读[模型卡片](https://github.com/onnx/models/tree/main/vision/classification/efficientnet-lite4) + +在这里,我们将演示如何使用 Gradio 为 EfficientNet-Lite4 设置示例演示 + +首先,我们导入所需的依赖项并下载和载入来自 ONNX 模型仓库的 efficientnet-lite4 模型。然后从 labels_map.txt 文件加载标签。接下来,我们设置预处理函数、加载用于推理的模型并设置推理函数。最后,将推理函数封装到 Gradio 接口中,供用户进行交互。下面是完整的代码。 + +```python +import numpy as np +import math +import matplotlib.pyplot as plt +import cv2 +import json +import gradio as gr +from huggingface_hub import hf_hub_download +from onnx import hub +import onnxruntime as ort + +# 从ONNX模型仓库加载ONNX模型 +model = hub.load("efficientnet-lite4") +# 加载标签文本文件 +labels = json.load(open("labels_map.txt", "r")) + +# 通过将图像从中心调整大小并裁剪到224x224来设置图像文件的尺寸 +def pre_process_edgetpu(img, dims): + output_height, output_width, _ = dims + img = resize_with_aspectratio(img, output_height, output_width, inter_pol=cv2.INTER_LINEAR) + img = center_crop(img, output_height, output_width) + img = np.asarray(img, dtype='float32') + # 将jpg像素值从[0 - 255]转换为浮点数组[-1.0 - 1.0] + img -= [127.0, 127.0, 127.0] + img /= [128.0, 128.0, 128.0] + return img + +# 使用等比例缩放调整图像尺寸 +def resize_with_aspectratio(img, out_height, out_width, scale=87.5, inter_pol=cv2.INTER_LINEAR): + height, width, _ = img.shape + new_height = int(100. * out_height / scale) + new_width = int(100. * out_width / scale) + if height > width: + w = new_width + h = int(new_height * height / width) + else: + h = new_height + w = int(new_width * width / height) + img = cv2.resize(img, (w, h), interpolation=inter_pol) + return img + +# crops the image around the center based on given height and width +def center_crop(img, out_height, out_width): + height, width, _ = img.shape + left = int((width - out_width) / 2) + right = int((width + out_width) / 2) + top = int((height - out_height) / 2) + bottom = int((height + out_height) / 2) + img = img[top:bottom, left:right] + return img + + +sess = ort.InferenceSession(model) + +def inference(img): + img = cv2.imread(img) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + img = pre_process_edgetpu(img, (224, 224, 3)) + + img_batch = np.expand_dims(img, axis=0) + + results = sess.run(["Softmax:0"], {"images:0": img_batch})[0] + result = reversed(results[0].argsort()[-5:]) + resultdic = {} + for r in result: + resultdic[labels[str(r)]] = float(results[0][r]) + return resultdic + +title = "EfficientNet-Lite4" +description = "EfficientNet-Lite 4是最大的变体,也是EfficientNet-Lite模型集合中最准确的。它是一个仅包含整数的量化模型,具有所有EfficientNet模型中最高的准确度。在Pixel 4 CPU上,它实现了80.4%的ImageNet top-1准确度,同时仍然可以实时运行(例如30ms/图像)。" +examples = [['catonnx.jpg']] +gr.Interface(inference, gr.Image(type="filepath"), "label", title=title, description=description, examples=examples).launch() +``` + +## 如何使用 ONNX 模型在 HF Spaces 上贡献 Gradio 演示 + +- 将模型添加到[onnx model zoo](https://github.com/onnx/models/blob/main/.github/PULL_REQUEST_TEMPLATE.md) +- 在 Hugging Face 上创建一个账号[here](https://huggingface.co/join). +- 要查看还有哪些模型需要添加到 ONNX 组织中,请参阅[Models list](https://github.com/onnx/models#models)中的列表 +- 在您的用户名下添加 Gradio Demo,请参阅此[博文](https://huggingface.co/blog/gradio-spaces)以在 Hugging Face 上设置 Gradio Demo。 +- 请求加入 ONNX 组织[here](https://huggingface.co/onnx). +- 一旦获准,将模型从您的用户名下转移到 ONNX 组织 +- 在模型表中为模型添加徽章,在[Models list](https://github.com/onnx/models#models)中查看示例 diff --git a/guides/cn/04_integrating-other-frameworks/Gradio-and-Wandb-Integration.md b/guides/cn/04_integrating-other-frameworks/Gradio-and-Wandb-Integration.md new file mode 100644 index 0000000..3379510 --- /dev/null +++ b/guides/cn/04_integrating-other-frameworks/Gradio-and-Wandb-Integration.md @@ -0,0 +1,284 @@ +# Gradio and W&B Integration + +相关空间:https://huggingface.co/spaces/akhaliq/JoJoGAN +标签:WANDB, SPACES +由 Gradio 团队贡献 + +## 介绍 + +在本指南中,我们将引导您完成以下内容: + +- Gradio、Hugging Face Spaces 和 Wandb 的介绍 +- 如何使用 Wandb 集成为 JoJoGAN 设置 Gradio 演示 +- 如何在 Hugging Face 的 Wandb 组织中追踪实验并贡献您自己的 Gradio 演示 + +下面是一个使用 Wandb 跟踪训练和实验的模型示例,请在下方尝试 JoJoGAN 演示。 + + + +## 什么是 Wandb? + +Weights and Biases (W&B) 允许数据科学家和机器学习科学家在从训练到生产的每个阶段跟踪他们的机器学习实验。任何指标都可以对样本进行聚合,并在可自定义和可搜索的仪表板中显示,如下所示: + +Screen Shot 2022-08-01 at 5 54 59 PM + +## 什么是 Hugging Face Spaces 和 Gradio? + +### Gradio + +Gradio 让用户可以使用几行 Python 代码将其机器学习模型演示为 Web 应用程序。Gradio 将任何 Python 函数(例如机器学习模型的推断函数)包装成一个用户界面,这些演示可以在 jupyter 笔记本、colab 笔记本中启动,也可以嵌入到您自己的网站中,免费托管在 Hugging Face Spaces 上。 + +在这里开始 [here](https://gradio.app/getting_started) + +### Hugging Face Spaces + +Hugging Face Spaces 是 Gradio 演示的免费托管选项。Spaces 有 3 个 SDK 选项:Gradio、Streamlit 和静态 HTML 演示。Spaces 可以是公共的或私有的,工作流程类似于 github 存储库。目前在 Hugging Face 上有 2000 多个 Spaces。了解更多关于 Spaces 的信息 [here](https://huggingface.co/spaces/launch)。 + +## 为 JoJoGAN 设置 Gradio 演示 + +现在,让我们引导您如何在自己的环境中完成此操作。我们假设您对 W&B 和 Gradio 还不太了解,只是为了本教程的目的。 + +让我们开始吧! + +1. 创建 W&B 账号 + + 如果您还没有 W&B 账号,请按照[这些快速说明](https://app.wandb.ai/login)创建免费账号。这不应该超过几分钟的时间。一旦完成(或者如果您已经有一个账户),接下来,我们将运行一个快速的 colab。 + +2. 打开 Colab 安装 Gradio 和 W&B + + 我们将按照 JoJoGAN 存储库中提供的 colab 进行操作,稍作修改以更有效地使用 Wandb 和 Gradio。 + + [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mchong6/JoJoGAN/blob/main/stylize.ipynb) + + 在顶部安装 Gradio 和 Wandb: + + ```sh + + pip install gradio wandb + ``` + +3. 微调 StyleGAN 和 W&B 实验跟踪 + + 下一步将打开一个 W&B 仪表板,以跟踪实验,并显示一个 Gradio 演示提供的预训练模型,您可以从下拉菜单中选择。这是您需要的代码: + + ```python + + alpha = 1.0 + alpha = 1-alpha + + preserve_color = True + num_iter = 100 + log_interval = 50 + + samples = [] + column_names = ["Reference (y)", "Style Code(w)", "Real Face Image(x)"] + + wandb.init(project="JoJoGAN") + config = wandb.config + config.num_iter = num_iter + config.preserve_color = preserve_color + wandb.log( + {"Style reference": [wandb.Image(transforms.ToPILImage()(target_im))]}, + step=0) + + # 加载判别器用于感知损失 + discriminator = Discriminator(1024, 2).eval().to(device) + ckpt = torch.load('models/stylegan2-ffhq-config-f.pt', map_location=lambda storage, loc: storage) + discriminator.load_state_dict(ckpt["d"], strict=False) + + # 重置生成器 + del generator + generator = deepcopy(original_generator) + + g_optim = optim.Adam(generator.parameters(), lr=2e-3, betas=(0, 0.99)) + + # 用于生成一族合理真实图像-> 假图像的更换图层 + if preserve_color: + id_swap = [9,11,15,16,17] + else: + id_swap = list(range(7, generator.n_latent)) + + for idx in tqdm(range(num_iter)): + mean_w = generator.get_latent(torch.randn([latents.size(0), latent_dim]).to(device)).unsqueeze(1).repeat(1, generator.n_latent, 1) + in_latent = latents.clone() + in_latent[:, id_swap] = alpha*latents[:, id_swap] + (1-alpha)*mean_w[:, id_swap] + + img = generator(in_latent, input_is_latent=True) + + with torch.no_grad(): + real_feat = discriminator(targets) + fake_feat = discriminator(img) + + loss = sum([F.l1_loss(a, b) for a, b in zip(fake_feat, real_feat)])/len(fake_feat) + + wandb.log({"loss": loss}, step=idx) + if idx % log_interval == 0: + generator.eval() + my_sample = generator(my_w, input_is_latent=True) + generator.train() + my_sample = transforms.ToPILImage()(utils.make_grid(my_sample, normalize=True, range=(-1, 1))) + wandb.log( + {"Current stylization": [wandb.Image(my_sample)]}, + step=idx) + table_data = [ + wandb.Image(transforms.ToPILImage()(target_im)), + wandb.Image(img), + wandb.Image(my_sample), + ] + samples.append(table_data) + + g_optim.zero_grad() + loss.backward() + g_optim.step() + + out_table = wandb.Table(data=samples, columns=column_names) + wandb.log({" 当前样本数 ": out_table}) + ``` + +4. 保存、下载和加载模型 + + 以下是如何保存和下载您的模型。 + + ```python + + from PIL import Image + import torch + torch.backends.cudnn.benchmark = True + from torchvision import transforms, utils + from util import * + import math + import random + import numpy as np + from torch import nn, autograd, optim + from torch.nn import functional as F + from tqdm import tqdm + import lpips + from model import * + from e4e_projection import projection as e4e_projection + + from copy import deepcopy + import imageio + + import os + import sys + import torchvision.transforms as transforms + from argparse import Namespace + from e4e.models.psp import pSp + from util import * + from huggingface_hub import hf_hub_download + from google.colab import files + torch.save({"g": generator.state_dict()}, "your-model-name.pt") + + files.download('your-model-name.pt') + + latent_dim = 512 + device="cuda" + model_path_s = hf_hub_download(repo_id="akhaliq/jojogan-stylegan2-ffhq-config-f", filename="stylegan2-ffhq-config-f.pt") + original_generator = Generator(1024, latent_dim, 8, 2).to(device) + ckpt = torch.load(model_path_s, map_location=lambda storage, loc: storage) + original_generator.load_state_dict(ckpt["g_ema"], strict=False) + mean_latent = original_generator.mean_latent(10000) + + generator = deepcopy(original_generator) + + ckpt = torch.load("/content/JoJoGAN/your-model-name.pt", map_location=lambda storage, loc: storage) + generator.load_state_dict(ckpt["g"], strict=False) + generator.eval() + + plt.rcParams['figure.dpi'] = 150 + + transform = transforms.Compose( + [ + transforms.Resize((1024, 1024)), + transforms.ToTensor(), + transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), + ] + ) + + def inference(img): + img.save('out.jpg') + aligned_face = align_face('out.jpg') + + my_w = e4e_projection(aligned_face, "out.pt", device).unsqueeze(0) + with torch.no_grad(): + my_sample = generator(my_w, input_is_latent=True) + + npimage = my_sample[0].cpu().permute(1, 2, 0).detach().numpy() + imageio.imwrite('filename.jpeg', npimage) + return 'filename.jpeg' + ``` + +5. 构建 Gradio 演示 + + ```python + + import gradio as gr + + title = "JoJoGAN" + description = "JoJoGAN 的 Gradio 演示:一次性面部风格化。要使用它,只需上传您的图像,或单击示例之一加载它们。在下面的链接中阅读更多信息。" + + demo = gr.Interface( + inference, + gr.Image(type="pil"), + gr.Image(type=" 文件 "), + title=title, + description=description + ) + + demo.launch(share=True) + ``` + +6. 将 Gradio 集成到 W&B 仪表板 + + 最后一步——将 Gradio 演示与 W&B 仪表板集成,只需要一行额外的代码 : + + ```python + + demo.integrate(wandb=wandb) + ``` + + 调用集成之后,将创建一个演示,您可以将其集成到仪表板或报告中 + + 在 W&B 之外,使用 gradio-app 标记允许任何人直接将 Gradio 演示嵌入到其博客、网站、文档等中的 HF spaces 上 : + + ```html + <gradio-app space="akhaliq/JoJoGAN"> <gradio-app> + ``` + +7.(可选)在 Gradio 应用程序中嵌入 W&B 图 + + 也可以在 Gradio 应用程序中嵌入 W&B 图。为此,您可以创建一个 W&B 报告,并在一个 `gr.HTML` 块中将其嵌入到 Gradio 应用程序中。 + + 报告需要是公开的,您需要在 iFrame 中包装 URL,如下所示 : + The Report will need to be public and you will need to wrap the URL within an iFrame like this: + ```python + + import gradio as gr + + def wandb_report(url): + iframe = f'<iframe src={url} style="border:none;height:1024px;width:100%">' + return gr.HTML(iframe) + + with gr.Blocks() as demo: + report_url = 'https://wandb.ai/_scott/pytorch-sweeps-demo/reports/loss-22-10-07-16-00-17---VmlldzoyNzU2NzAx' + report = wandb_report(report_url) + + demo.launch(share=True) + ``` + +## 结论 + +希望您喜欢此嵌入 Gradio 演示到 W&B 报告的简短演示!感谢您一直阅读到最后。回顾一下 : + +- 仅需要一个单一参考图像即可对 JoJoGAN 进行微调,通常在 GPU 上需要约 1 分钟。训练完成后,可以将样式应用于任何输入图像。在论文中阅读更多内容。 + +- W&B 可以通过添加几行代码来跟踪实验,您可以在单个集中的仪表板中可视化、排序和理解您的实验。 + +- Gradio 则在用户友好的界面中演示模型,可以在网络上任何地方共享。 + +## 如何在 Wandb 组织的 HF spaces 上 贡献 Gradio 演示 + +- 在 Hugging Face 上创建一个帐户[此处](https://huggingface.co/join)。 +- 在您的用户名下添加 Gradio 演示,请参阅[此教程](https://huggingface.co/course/chapter9/4?fw=pt) 以在 Hugging Face 上设置 Gradio 演示。 +- 申请加入 wandb 组织[此处](https://huggingface.co/wandb)。 +- 批准后,将模型从自己的用户名转移到 Wandb 组织中。 diff --git a/guides/cn/04_integrating-other-frameworks/image-classification-in-pytorch.md b/guides/cn/04_integrating-other-frameworks/image-classification-in-pytorch.md new file mode 100644 index 0000000..4588852 --- /dev/null +++ b/guides/cn/04_integrating-other-frameworks/image-classification-in-pytorch.md @@ -0,0 +1,88 @@ +# PyTorch 图像分类 + +Related spaces: https://huggingface.co/spaces/abidlabs/pytorch-image-classifier, https://huggingface.co/spaces/pytorch/ResNet, https://huggingface.co/spaces/pytorch/ResNext, https://huggingface.co/spaces/pytorch/SqueezeNet +Tags: VISION, RESNET, PYTORCH + +## 介绍 + +图像分类是计算机视觉中的一个核心任务。构建更好的分类器以区分图片中存在的物体是当前研究的一个热点领域,因为它的应用范围从自动驾驶车辆到医学成像等领域都很广泛。 + +这样的模型非常适合 Gradio 的 _image_ 输入组件,因此在本教程中,我们将使用 Gradio 构建一个用于图像分类的 Web 演示。我们将能够在 Python 中构建整个 Web 应用程序,效果如下(试试其中一个示例!): + + + +让我们开始吧! + +### 先决条件 + +确保您已经[安装](/getting_started)了 `gradio` Python 包。我们将使用一个预训练的图像分类模型,所以您还应该安装了 `torch`。 + +## 第一步 - 设置图像分类模型 + +首先,我们需要一个图像分类模型。在本教程中,我们将使用一个预训练的 Resnet-18 模型,因为它可以从[PyTorch Hub](https://pytorch.org/hub/pytorch_vision_resnet/)轻松下载。您可以使用其他预训练模型或训练自己的模型。 + +```python +import torch + +model = torch.hub.load('pytorch/vision:v0.6.0', 'resnet18', pretrained=True).eval() +``` + +由于我们将使用模型进行推断,所以我们调用了 `.eval()` 方法。 + +## 第二步 - 定义 `predict` 函数 + +接下来,我们需要定义一个函数,该函数接受*用户输入*,在本示例中是一张图片,并返回预测结果。预测结果应该以字典的形式返回,其中键是类别名称,值是置信度概率。我们将从这个[text 文件](https://git.io/JJkYN)中加载类别名称。 + +对于我们的预训练模型,它的代码如下: + +```python +import requests +from PIL import Image +from torchvision import transforms + +# 下载ImageNet的可读标签。 +response = requests.get("https://git.io/JJkYN") +labels = response.text.split("\n") + +def predict(inp): + inp = transforms.ToTensor()(inp).unsqueeze(0) + with torch.no_grad(): + prediction = torch.nn.functional.softmax(model(inp)[0], dim=0) + confidences = {labels[i]: float(prediction[i]) for i in range(1000)} + return confidences +``` + +让我们逐步来看一下这段代码。该函数接受一个参数: + +- `inp`:输入图片,类型为 `PIL` 图像 + +然后,该函数将图像转换为 PIL 图像,最终转换为 PyTorch 的 `tensor`,将其输入模型,并返回: + +- `confidences`:预测结果,以字典形式表示,其中键是类别标签,值是置信度概率 + +## 第三步 - 创建 Gradio 界面 + +现在我们已经设置好了预测函数,我们可以创建一个 Gradio 界面。 + +在本例中,输入组件是一个拖放图片的组件。为了创建这个输入组件,我们使用 `Image(type="pil")` 来创建该组件,并处理预处理操作将其转换为 `PIL` 图像。 + +输出组件将是一个 `Label`,它以良好的形式显示顶部标签。由于我们不想显示所有 1000 个类别标签,所以我们将其定制为只显示前 3 个标签,构造为 `Label(num_top_classes=3)`。 + +最后,我们添加了一个 `examples` 参数,允许我们预填一些预定义的示例到界面中。Gradio 的代码如下: + +```python +import gradio as gr + +gr.Interface(fn=predict, + inputs=gr.Image(type="pil"), + outputs=gr.Label(num_top_classes=3), + examples=["lion.jpg", "cheetah.jpg"]).launch() +``` + +这将产生以下界面,您可以在浏览器中直接尝试(试试上传自己的示例图片!): + + + +--- + +完成了!这就是构建图像分类器 Web 演示所需的所有代码。如果您想与他人共享,请在 `launch()` 接口时设置 `share=True`! diff --git a/guides/cn/04_integrating-other-frameworks/image-classification-in-tensorflow.md b/guides/cn/04_integrating-other-frameworks/image-classification-in-tensorflow.md new file mode 100644 index 0000000..d03e77f --- /dev/null +++ b/guides/cn/04_integrating-other-frameworks/image-classification-in-tensorflow.md @@ -0,0 +1,86 @@ +# TensorFlow 和 Keras 中的图像分类 + +相关空间:https://huggingface.co/spaces/abidlabs/keras-image-classifier +标签:VISION, MOBILENET, TENSORFLOW + +## 简介 + +图像分类是计算机视觉中的一项核心任务。构建更好的分类器来识别图像中的物体是一个研究的热点领域,因为它在交通控制系统到卫星成像等应用中都有广泛的应用。 + +这样的模型非常适合与 Gradio 的 _image_ 输入组件一起使用,因此在本教程中,我们将使用 Gradio 构建一个用于图像分类的 Web 演示。我们可以在 Python 中构建整个 Web 应用程序,它的界面将如下所示(试试其中一个例子!): + + + +让我们开始吧! + +### 先决条件 + +确保您已经[安装](/getting_started)了 `gradio` Python 包。我们将使用一个预训练的 Keras 图像分类模型,因此您还应该安装了 `tensorflow`。 + +## 第一步 —— 设置图像分类模型 + +首先,我们需要一个图像分类模型。在本教程中,我们将使用一个预训练的 Mobile Net 模型,因为它可以从[Keras](https://keras.io/api/applications/mobilenet/)轻松下载。您也可以使用其他预训练模型或训练自己的模型。 + +```python +import tensorflow as tf + +inception_net = tf.keras.applications.MobileNetV2() +``` + +此行代码将使用 Keras 库自动下载 MobileNet 模型和权重。 + +## 第二步 —— 定义 `predict` 函数 + +接下来,我们需要定义一个函数,该函数接收*用户输入*作为参数(在本例中为图像),并返回预测结果。预测结果应以字典形式返回,其中键是类名,值是置信概率。我们将从这个[text 文件](https://git.io/JJkYN)中加载类名。 + +对于我们的预训练模型,函数将如下所示: + +```python +import requests + +# 从ImageNet下载可读性标签。 +response = requests.get("https://git.io/JJkYN") +labels = response.text.split("\n") + +def classify_image(inp): + inp = inp.reshape((-1, 224, 224, 3)) + inp = tf.keras.applications.mobilenet_v2.preprocess_input(inp) + prediction = inception_net.predict(inp).flatten() + confidences = {labels[i]: float(prediction[i]) for i in range(1000)} + return confidences +``` + +让我们来详细了解一下。该函数接受一个参数: + +- `inp`:输入图像的 `numpy` 数组 + +然后,函数添加一个批次维度,通过模型进行处理,并返回: + +- `confidences`:预测结果,以字典形式表示,其中键是类标签,值是置信概率 + +## 第三步 —— 创建 Gradio 界面 + +现在我们已经设置好了预测函数,我们可以围绕它创建一个 Gradio 界面。 + +在这种情况下,输入组件是一个拖放图像组件。要创建此输入组件,我们可以使用 `"gradio.inputs.Image"` 类,该类创建该组件并处理预处理以将其转换为 numpy 数组。我们将使用一个参数来实例化该类,该参数会自动将输入图像预处理为 224 像素 x224 像素的大小,这是 MobileNet 所期望的尺寸。 + +输出组件将是一个 `"label"`,它以美观的形式显示顶部标签。由于我们不想显示所有的 1,000 个类标签,所以我们将自定义它只显示前 3 个标签。 + +最后,我们还将添加一个 `examples` 参数,它允许我们使用一些预定义的示例预填充我们的接口。Gradio 的代码如下所示: + +```python +import gradio as gr + +gr.Interface(fn=classify_image, + inputs=gr.Image(width=224, height=224), + outputs=gr.Label(num_top_classes=3), + examples=["banana.jpg", "car.jpg"]).launch() +``` + +这将生成以下界面,您可以在浏览器中立即尝试(尝试上传您自己的示例!): + + + +--- + +完成!这就是构建图像分类器的 Web 演示所需的所有代码。如果您想与他人分享,请尝试在启动接口时设置 `share=True`! diff --git a/guides/cn/04_integrating-other-frameworks/image-classification-with-vision-transformers.md b/guides/cn/04_integrating-other-frameworks/image-classification-with-vision-transformers.md new file mode 100644 index 0000000..a1186da --- /dev/null +++ b/guides/cn/04_integrating-other-frameworks/image-classification-with-vision-transformers.md @@ -0,0 +1,52 @@ +# Vision Transformers 图像分类 + +相关空间:https://huggingface.co/spaces/abidlabs/vision-transformer +标签:VISION, TRANSFORMERS, HUB + +## 简介 + +图像分类是计算机视觉中的重要任务。构建更好的分类器以确定图像中存在的对象是当前研究的热点领域,因为它在从人脸识别到制造质量控制等方面都有应用。 + +最先进的图像分类器基于 _transformers_ 架构,该架构最初在自然语言处理任务中很受欢迎。这种架构通常被称为 vision transformers (ViT)。这些模型非常适合与 Gradio 的*图像*输入组件一起使用,因此在本教程中,我们将构建一个使用 Gradio 进行图像分类的 Web 演示。我们只需用**一行 Python 代码**即可构建整个 Web 应用程序,其效果如下(试用一下示例之一!): + + + +让我们开始吧! + +### 先决条件 + +确保您已经[安装](/getting_started)了 `gradio` Python 包。 + +## 步骤 1 - 选择 Vision 图像分类模型 + +首先,我们需要一个图像分类模型。在本教程中,我们将使用[Hugging Face Model Hub](https://huggingface.co/models?pipeline_tag=image-classification)上的一个模型。该 Hub 包含数千个模型,涵盖了多种不同的机器学习任务。 + +在左侧边栏中展开 Tasks 类别,并选择我们感兴趣的“Image Classification”作为我们的任务。然后,您将看到 Hub 上为图像分类设计的所有模型。 + +在撰写时,最受欢迎的模型是 `google/vit-base-patch16-224`,该模型在分辨率为 224x224 像素的 ImageNet 图像上进行了训练。我们将在演示中使用此模型。 + +## 步骤 2 - 使用 Gradio 加载 Vision Transformer 模型 + +当使用 Hugging Face Hub 上的模型时,我们无需为演示定义输入或输出组件。同样,我们不需要关心预处理或后处理的细节。所有这些都可以从模型标签中自动推断出来。 + +除了导入语句外,我们只需要一行代码即可加载并启动演示。 + +我们使用 `gr.Interface.load()` 方法,并传入包含 `huggingface/` 的模型路径,以指定它来自 Hugging Face Hub。 + +```python +import gradio as gr + +gr.Interface.load( + "huggingface/google/vit-base-patch16-224", + examples=["alligator.jpg", "laptop.jpg"]).launch() +``` + +请注意,我们添加了一个 `examples` 参数,允许我们使用一些预定义的示例预填充我们的界面。 + +这将生成以下接口,您可以直接在浏览器中尝试。当您输入图像时,它会自动进行预处理并发送到 Hugging Face Hub API,通过模型处理,并以人类可解释的预测结果返回。尝试上传您自己的图像! + + + +--- + +完成!只需一行代码,您就建立了一个图像分类器的 Web 演示。如果您想与他人分享,请在 `launch()` 接口时设置 `share=True`。 diff --git a/guides/cn/05_tabular-data-science-and-plots/01_connecting-to-a-database.md b/guides/cn/05_tabular-data-science-and-plots/01_connecting-to-a-database.md new file mode 100644 index 0000000..2c3cc67 --- /dev/null +++ b/guides/cn/05_tabular-data-science-and-plots/01_connecting-to-a-database.md @@ -0,0 +1,152 @@ +# 连接到数据库 + +相关空间:https://huggingface.co/spaces/gradio/chicago-bike-share-dashboard +标签:TABULAR, PLOTS + +## 介绍 + +本指南介绍如何使用 Gradio 连接您的应用程序到数据库。我们将会 +连接到在 AWS 上托管的 PostgreSQL 数据库,但 Gradio 对于您连接的数据库类型和托管位置没有任何限制。因此,只要您能编写 Python 代码来连接 +您的数据,您就可以使用 Gradio 在 Web 界面中显示它 💪 + +## 概述 + +我们将分析来自芝加哥的自行车共享数据。数据托管在 kaggle [这里](https://www.kaggle.com/datasets/evangower/cyclistic-bike-share?select=202203-divvy-tripdata.csv)。 +我们的目标是创建一个仪表盘,让我们的业务利益相关者能够回答以下问题: + +1. 电动自行车是否比普通自行车更受欢迎? +2. 哪些出发自行车站点最受欢迎? + +在本指南结束时,我们将拥有一个如下所示的功能齐全的应用程序: + + + +## 步骤 1 - 创建数据库 + +我们将在 Amazon 的 RDS 服务上托管我们的数据。如果还没有 AWS 账号,请创建一个 +并在免费层级上创建一个 PostgreSQL 数据库。 + +**重要提示**:如果您计划在 HuggingFace Spaces 上托管此演示,请确保数据库在 **8080** 端口上。Spaces +将阻止除端口 80、443 或 8080 之外的所有外部连接,如此[处所示](https://huggingface.co/docs/hub/spaces-overview#networking)。 +RDS 不允许您在 80 或 443 端口上创建 postgreSQL 实例。 + +创建完数据库后,从 Kaggle 下载数据集并将其上传到数据库中。 +为了演示的目的,我们只会上传 2022 年 3 月的数据。 + +## 步骤 2.a - 编写 ETL 代码 + +我们将查询数据库,按自行车类型(电动、标准或有码)进行分组,并获取总骑行次数。 +我们还将查询每个站点的出发骑行次数,并获取前 5 个。 + +然后,我们将使用 matplotlib 将查询结果可视化。 + +我们将使用 pandas 的[read_sql](https://pandas.pydata.org/docs/reference/api/pandas.read_sql.html) +方法来连接数据库。这需要安装 `psycopg2` 库。 + +为了连接到数据库,我们将指定数据库的用户名、密码和主机作为环境变量。 +这样可以通过避免将敏感信息以明文形式存储在应用程序文件中,使我们的应用程序更安全。 + +```python +import os +import pandas as pd +import matplotlib.pyplot as plt + +DB_USER = os.getenv("DB_USER") +DB_PASSWORD = os.getenv("DB_PASSWORD") +DB_HOST = os.getenv("DB_HOST") +PORT = 8080 +DB_NAME = "bikeshare" + +connection_string = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}?port={PORT}&dbname={DB_NAME}" + +def get_count_ride_type(): + df = pd.read_sql( + """ + SELECT COUNT(ride_id) as n, rideable_type + FROM rides + GROUP BY rideable_type + ORDER BY n DESC + """, + con=connection_string + ) + fig_m, ax = plt.subplots() + ax.bar(x=df['rideable_type'], height=df['n']) + ax.set_title("Number of rides by bycycle type") + ax.set_ylabel("Number of Rides") + ax.set_xlabel("Bicycle Type") + return fig_m + + +def get_most_popular_stations(): + + df = pd.read_sql( + """ + SELECT COUNT(ride_id) as n, MAX(start_station_name) as station + FROM RIDES + WHERE start_station_name is NOT NULL + GROUP BY start_station_id + ORDER BY n DESC + LIMIT 5 + """, + con=connection_string + ) + fig_m, ax = plt.subplots() + ax.bar(x=df['station'], height=df['n']) + ax.set_title("Most popular stations") + ax.set_ylabel("Number of Rides") + ax.set_xlabel("Station Name") + ax.set_xticklabels( + df['station'], rotation=45, ha="right", rotation_mode="anchor" + ) + ax.tick_params(axis="x", labelsize=8) + fig_m.tight_layout() + return fig_m +``` + +如果您在本地运行我们的脚本,可以像下面这样将凭据作为环境变量传递: + +```bash +DB_USER='username' DB_PASSWORD='password' DB_HOST='host' python app.py +``` + +## 步骤 2.c - 编写您的 gradio 应用程序 + +我们将使用两个单独的 `gr.Plot` 组件将我们的 matplotlib 图表并排显示在一起,使用 `gr.Row()`。 +因为我们已经在 `demo.load()` 事件触发器中封装了获取数据的函数, +我们的演示将在每次网页加载时从数据库**动态**获取最新数据。🪄 + +```python +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + bike_type = gr.Plot() + station = gr.Plot() + + demo.load(get_count_ride_type, inputs=None, outputs=bike_type) + demo.load(get_most_popular_stations, inputs=None, outputs=station) + +demo.launch() +``` + +## 步骤 3 - 部署 + +如果您运行上述代码,您的应用程序将在本地运行。 +您甚至可以通过将 `share=True` 参数传递给 `launch` 来获得一个临时共享链接。 + +但是如果您想要一个永久的部署解决方案呢? +让我们将我们的 Gradio 应用程序部署到免费的 HuggingFace Spaces 平台上。 + +如果您之前没有使用过 Spaces,请按照之前的指南[这里](/using_hugging_face_integrations)进行操作。 +您将需要将 `DB_USER`、`DB_PASSWORD` 和 `DB_HOST` 变量添加为 "Repo Secrets"。您可以在 " 设置 " 选项卡中进行此操作。 + +![secrets](/assets/guides/secrets.png) + +## 结论 + +恭喜你!您知道如何将您的 Gradio 应用程序连接到云端托管的数据库!☁️ + +我们的仪表板现在正在[Spaces](https://huggingface.co/spaces/gradio/chicago-bike-share-dashboard)上运行。 +完整代码在[这里](https://huggingface.co/spaces/gradio/chicago-bike-share-dashboard/blob/main/app.py) + +正如您所见,Gradio 使您可以连接到您的数据并以您想要的方式显示!🔥 diff --git a/guides/cn/05_tabular-data-science-and-plots/creating-a-dashboard-from-bigquery-data.md b/guides/cn/05_tabular-data-science-and-plots/creating-a-dashboard-from-bigquery-data.md new file mode 100644 index 0000000..00aa2e3 --- /dev/null +++ b/guides/cn/05_tabular-data-science-and-plots/creating-a-dashboard-from-bigquery-data.md @@ -0,0 +1,122 @@ +# 从 BigQuery 数据创建实时仪表盘 + +Tags: 表格 , 仪表盘 , 绘图 + +[Google BigQuery](https://cloud.google.com/bigquery) 是一个基于云的用于处理大规模数据集的服务。它是一个无服务器且高度可扩展的数据仓库解决方案,使用户能够使用类似 SQL 的查询分析数据。 + +在本教程中,我们将向您展示如何使用 `gradio` 在 Python 中查询 BigQuery 数据集并在实时仪表盘中显示数据。仪表板将如下所示: + + + +在本指南中,我们将介绍以下步骤: + +1. 设置 BigQuery 凭据 +2. 使用 BigQuery 客户端 +3. 构建实时仪表盘(仅需 _7 行 Python 代码_) + +我们将使用[纽约时报的 COVID 数据集](https://www.nytimes.com/interactive/2021/us/covid-cases.html),该数据集作为一个公共数据集可在 BigQuery 上使用。数据集名为 `covid19_nyt.us_counties`,其中包含有关美国各县 COVID 确诊病例和死亡人数的最新信息。 + +**先决条件**:本指南使用 [Gradio Blocks](../quickstart/#blocks-more-flexibility-and-control),因此请确保您熟悉 Blocks 类。 + +## 设置 BigQuery 凭据 + +要使用 Gradio 和 BigQuery,您需要获取您的 BigQuery 凭据,并将其与 [BigQuery Python 客户端](https://pypi.org/project/google-cloud-bigquery/) 一起使用。如果您已经拥有 BigQuery 凭据(作为 `.json` 文件),则可以跳过此部分。否则,您可以在几分钟内免费完成此操作。 + +1. 首先,登录到您的 Google Cloud 帐户,并转到 Google Cloud 控制台 (https://console.cloud.google.com/) + +2. 在 Cloud 控制台中,单击左上角的汉堡菜单,然后从菜单中选择“API 与服务”。如果您没有现有项目,则需要创建一个项目。 + +3. 然后,单击“+ 启用的 API 与服务”按钮,该按钮允许您为项目启用特定服务。搜索“BigQuery API”,单击它,然后单击“启用”按钮。如果您看到“管理”按钮,则表示 BigQuery 已启用,您已准备就绪。 + +4. 在“API 与服务”菜单中,单击“凭据”选项卡,然后单击“创建凭据”按钮。 + +5. 在“创建凭据”对话框中,选择“服务帐号密钥”作为要创建的凭据类型,并为其命名。还可以通过为其授予角色(例如“BigQuery 用户”)为服务帐号授予权限,从而允许您运行查询。 + +6. 在选择服务帐号后,选择“JSON”密钥类型,然后单击“创建”按钮。这将下载包含您凭据的 JSON 密钥文件到您的计算机。它的外观类似于以下内容: + +```json +{ + "type": "service_account", + "project_id": "your project", + "private_key_id": "your private key id", + "private_key": "private key", + "client_email": "email", + "client_id": "client id", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/email_id" +} +``` + +## 使用 BigQuery 客户端 + +获得凭据后,您需要使用 BigQuery Python 客户端使用您的凭据进行身份验证。为此,您需要在终端中运行以下命令安装 BigQuery Python 客户端: + +```bash +pip install google-cloud-bigquery[pandas] +``` + +您会注意到我们已安装了 pandas 插件,这对于将 BigQuery 数据集处理为 pandas 数据帧将非常有用。安装了客户端之后,您可以通过运行以下代码使用您的凭据进行身份验证: + +```py +from google.cloud import bigquery + +client = bigquery.Client.from_service_account_json("path/to/key.json") +``` + +完成凭据身份验证后,您现在可以使用 BigQuery Python 客户端与您的 BigQuery 数据集进行交互。 + +以下是一个示例函数,该函数在 BigQuery 中查询 `covid19_nyt.us_counties` 数据集,以显示截至当前日期的确诊人数最多的前 20 个县: + +```py +import numpy as np + +QUERY = ( + 'SELECT * FROM `bigquery-public-data.covid19_nyt.us_counties` ' + 'ORDER BY date DESC,confirmed_cases DESC ' + 'LIMIT 20') + +def run_query(): + query_job = client.query(QUERY) + query_result = query_job.result() + df = query_result.to_dataframe() + # Select a subset of columns + df = df[["confirmed_cases", "deaths", "county", "state_name"]] + # Convert numeric columns to standard numpy types + df = df.astype({"deaths": np.int64, "confirmed_cases": np.int64}) + return df +``` + +## 构建实时仪表盘 + +一旦您有了查询数据的函数,您可以使用 Gradio 库的 `gr.DataFrame` 组件以表格形式显示结果。这是一种检查数据并确保查询正确的有用方式。 + +以下是如何使用 `gr.DataFrame` 组件显示结果的示例。通过将 `run_query` 函数传递给 `gr.DataFrame`,我们指示 Gradio 在页面加载时立即运行该函数并显示结果。此外,您还可以传递关键字 `every`,以告知仪表板每小时刷新一次(60\*60 秒)。 + +```py +import gradio as gr + +with gr.Blocks() as demo: + gr.DataFrame(run_query, every=gr.Timer(60*60)) + +demo.queue().launch() # Run the demo using queuing +``` + +也许您想在我们的仪表盘中添加一个可视化效果。您可以使用 `gr.ScatterPlot()` 组件将数据可视化为散点图。这可以让您查看数据中不同变量(例如病例数和死亡数)之间的关系,并可用于探索数据和获取见解。同样,我们可以实时完成这一操作 +通过传递 `every` 参数。 + +以下是一个完整示例,展示了如何在显示数据时使用 `gr.ScatterPlot` 来进行可视化。 + +```py +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown("# 💉 Covid Dashboard (Updated Hourly)") + with gr.Row(): + gr.DataFrame(run_query, every=gr.Timer(60*60)) + gr.ScatterPlot(run_query, every=gr.Timer(60*60), x="confirmed_cases", + y="deaths", tooltip="county", width=500, height=500) + +demo.queue().launch() # Run the demo with queuing enabled +``` diff --git a/guides/cn/05_tabular-data-science-and-plots/creating-a-dashboard-from-supabase-data.md b/guides/cn/05_tabular-data-science-and-plots/creating-a-dashboard-from-supabase-data.md new file mode 100644 index 0000000..de81653 --- /dev/null +++ b/guides/cn/05_tabular-data-science-and-plots/creating-a-dashboard-from-supabase-data.md @@ -0,0 +1,123 @@ +# 从 Supabase 数据创建仪表盘 + +Tags: TABULAR, DASHBOARD, PLOTS + +[Supabase](https://supabase.com/) 是一个基于云的开源后端,提供了 PostgreSQL 数据库、身份验证和其他有用的功能,用于构建 Web 和移动应用程序。在本教程中,您将学习如何从 Supabase 读取数据,并在 Gradio 仪表盘上以**实时**方式绘制数据。 + +**先决条件 :** 要开始,您需要一个免费的 Supabase 账户,您可以在此处注册:[https://app.supabase.com/](https://app.supabase.com/) + +在这个端到端指南中,您将学习如何: + +- 在 Supabase 中创建表 +- 使用 Supabase Python 客户端向 Supabase 写入数据 +- 使用 Gradio 在实时仪表盘中可视化数据 + +如果您已经在 Supabase 上有数据想要在仪表盘中可视化,您可以跳过前两个部分,直接到[可视化数据](#visualize-the-data-in-a-real-time-gradio-dashboard)! + +## 在 Supabase 中创建表 + +首先,我们需要一些要可视化的数据。根据这个[出色的指南](https://supabase.com/blog/loading-data-supabase-python),我们将创建一些虚假的商务数据,并将其放入 Supabase 中。 + +1\. 在 Supabase 中创建一个新项目。一旦您登录,点击 "New Project" 按钮 + +2\. 给您的项目命名并设置数据库密码。您还可以选择定价计划(对于我们来说,免费计划已足够!) + +3\. 在数据库启动时(可能需要多达 2 分钟),您将看到您的 API 密钥。 + +4\. 在左侧窗格中单击 "Table Editor"(表图标)以创建一个新表。我们将创建一个名为 `Product` 的单表,具有以下模式: + +
+ + + + + +
product_idint8
inventory_countint8
pricefloat8
product_namevarchar
+
+ +5\. 点击保存以保存表结构。 + +我们的表已经准备好了! + +## 将数据写入 Supabase + +下一步是向 Supabase 数据集中写入数据。我们将使用 Supabase Python 库来完成这个任务。 + +6\. 通过在终端中运行以下命令来安装 `supabase` 库: + +```bash +pip install supabase +``` + +7\. 获取项目 URL 和 API 密钥。点击左侧窗格上的设置(齿轮图标),然后点击 'API'。URL 列在项目 URL 框中,API 密钥列在项目 API 密钥(带有 `service_role`、`secret` 标签)中 + +8\. 现在,运行以下 Python 脚本将一些虚假数据写入表中(注意您需要在步骤 7 中放入 `SUPABASE_URL` 和 `SUPABASE_SECRET_KEY` 的值): + +```python +import supabase + +# 初始化Supabase客户端 +client = supabase.create_client('SUPABASE_URL', 'SUPABASE_SECRET_KEY') + +# 定义要写入的数据 +import random + +main_list = [] +for i in range(10): + value = {'product_id': i, + 'product_name': f"Item {i}", + 'inventory_count': random.randint(1, 100), + 'price': random.random()*100 + } + main_list.append(value) + +# 将数据写入表中 +data = client.table('Product').insert(main_list).execute() +``` + +返回 Supabase 仪表板并刷新页面,您将看到 10 行数据填充到 `Product` 表中! + +## 在实时 Gradio 仪表盘中可视化数据 + +最后,我们将使用相同的 `supabase` Python 库从 Supabase 数据集中读取数据,并使用 `gradio` 创建一个实时仪表盘。 + +注意:我们在本节中重复了某些步骤(比如创建 Supabase 客户端),以防您没有完成之前的部分。如第 7 步所述,您将需要数据库的项目 URL 和 API 密钥。 + +9\. 编写一个函数,从 `Product` 表加载数据并将其作为 pandas DataFrame 返回: + +import supabase + +```python +import supabase +import pandas as pd + +client = supabase.create_client('SUPABASE_URL', 'SUPABASE_SECRET_KEY') + +def read_data(): + response = client.table('Product').select("*").execute() + df = pd.DataFrame(response.data) + return df +``` + +10\. 使用两个条形图创建一个小的 Gradio 仪表盘,每分钟绘制所有项目的价格和库存量,并实时更新: + +```python +import gradio as gr + +with gr.Blocks() as dashboard: + with gr.Row(): + gr.BarPlot(read_data, x="product_id", y="price", title="价格", every=gr.Timer(60)) + gr.BarPlot(read_data, x="product_id", y="inventory_count", title="库存", every=gr.Timer(60)) + +dashboard.queue().launch() +``` + +请注意,通过将函数传递给 `gr.BarPlot()`,我们可以在网络应用加载时查询数据库(然后每 60 秒查询一次,因为有 `every` 参数)。您的最终仪表盘应如下所示: + + + +## 结论 + +就是这样!在本教程中,您学习了如何将数据写入 Supabase 数据集,然后读取该数据并将结果绘制为条形图。如果您更新 Supabase 数据库中的数据,您会注意到 Gradio 仪表盘将在一分钟内更新。 + +尝试在此示例中添加更多绘图和可视化(或使用不同的数据集),以构建一个更复杂的仪表盘! diff --git a/guides/cn/05_tabular-data-science-and-plots/creating-a-realtime-dashboard-from-google-sheets.md b/guides/cn/05_tabular-data-science-and-plots/creating-a-realtime-dashboard-from-google-sheets.md new file mode 100644 index 0000000..907941d --- /dev/null +++ b/guides/cn/05_tabular-data-science-and-plots/creating-a-realtime-dashboard-from-google-sheets.md @@ -0,0 +1,121 @@ +# 从 Google Sheets 创建实时仪表盘 + +Tags: TABULAR, DASHBOARD, PLOTS +[Google Sheets](https://www.google.com/sheets/about/) 是一种以电子表格形式存储表格数据的简便方法。借助 Gradio 和 pandas,可以轻松从公共或私有 Google Sheets 读取数据,然后显示数据或绘制数据。在本博文中,我们将构建一个小型 _real-time_ 仪表盘,该仪表盘在 Google Sheets 中的数据更新时进行更新。 +构建仪表盘本身只需要使用 Gradio 的 9 行 Python 代码,我们的最终仪表盘如下所示: + + +**先决条件**:本指南使用[Gradio Blocks](../quickstart/#blocks-more-flexibility-and-control),因此请确保您熟悉 Blocks 类。 +具体步骤略有不同,具体取决于您是使用公开访问还是私有 Google Sheet。我们将分别介绍这两种情况,所以让我们开始吧! + +## Public Google Sheets + +由于[`pandas` 库](https://pandas.pydata.org/)的存在,从公共 Google Sheet 构建仪表盘非常简单: + +1. 获取要使用的 Google Sheets 的网址。为此,只需进入 Google Sheets,单击右上角的“共享”按钮,然后单击“获取可共享链接”按钮。这将给您一个类似于以下示例的网址: + +```html +https://docs.google.com/spreadsheets/d/1UoKzzRzOCt-FXLLqDKLbryEKEgllGAQUEJ5qtmmQwpU/edit#gid=0 +``` + +2. 现在,修改此网址并使用它从 Google Sheets 读取数据到 Pandas DataFrame 中。 (在下面的代码中,用您的公开 Google Sheet 的网址替换 `URL` 变量): + +```python +import pandas as pd +URL = "https://docs.google.com/spreadsheets/d/1UoKzzRzOCt-FXLLqDKLbryEKEgllGAQUEJ5qtmmQwpU/edit#gid=0"csv_url = URL.replace('/edit#gid=', '/export?format=csv&gid=') +def get_data(): + return pd.read_csv(csv_url) +``` + +3. 数据查询是一个函数,这意味着可以使用 `gr.DataFrame` 组件实时显示或使用 `gr.LinePlot` 组件实时绘制数据(当然,根据数据的不同,可能需要不同的绘图方法)。只需将函数传递给相应的组件,并根据组件刷新的频率(以秒为单位)设置 `every` 参数。以下是 Gradio 代码: + +```python +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown("# 📈 Real-Time Line Plot") + with gr.Row(): + with gr.Column(): + gr.DataFrame(get_data, every=gr.Timer(5)) + with gr.Column(): + gr.LinePlot(get_data, every=gr.Timer(5), x="Date", y="Sales", y_title="Sales ($ millions)", overlay_point=True, width=500, height=500) + +demo.queue().launch() # Run the demo with queuing enabled +``` + +到此为止!您现在拥有一个仪表盘,每 5 秒刷新一次,从 Google Sheets 中获取数据。 + +## 私有 Google Sheets + +对于私有 Google Sheets,流程需要更多的工作量,但并不多!关键区别在于,现在您必须经过身份验证,以授权访问私有 Google Sheets。 + +### 身份验证 + +要进行身份验证,需从 Google Cloud 获取凭据。以下是[如何设置 Google Cloud 凭据](https://developers.google.com/workspace/guides/create-credentials): + +1. 首先,登录您的 Google Cloud 帐户并转到 Google Cloud 控制台(https://console.cloud.google.com/) +2. 在 Cloud 控制台中,单击左上角的汉堡菜单,然后从菜单中选择“API 和服务”。如果您没有现有项目,则需要创建一个。 +3. 然后,点击“+ 启用的 API 和服务”按钮,允许您为项目启用特定的服务。搜索“Google Sheets API”,点击它,然后单击“启用”按钮。如果看到“管理”按钮,则表示 Google Sheets 已启用,并且您已准备就绪。 +4. 在 API 和服务菜单中,点击“凭据”选项卡,然后点击“创建凭据”按钮。 +5. 在“创建凭据”对话框中,选择“服务帐号密钥”作为要创建的凭据类型,并为其命名。**记下服务帐号的电子邮件地址** +6. 在选择服务帐号之后,选择“JSON”密钥类型,然后点击“创建”按钮。这将下载包含您凭据的 JSON 密钥文件到您的计算机。文件类似于以下示例: + +```json +{ + "type": "service_account", + "project_id": "your project", + "private_key_id": "your private key id", + "private_key": "private key", + "client_email": "email", + "client_id": "client id", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/email_id" +} +``` + +### 查询 + +在获得凭据的 `.json` 文件后,可以按照以下步骤查询您的 Google Sheet: + +1. 单击 Google Sheet 右上角的“共享”按钮。使用身份验证子部分第 5 步的服务的电子邮件地址共享 Google Sheets(此步骤很重要!)。然后单击“获取可共享链接”按钮。这将给您一个类似于以下示例的网址: + +```html +https://docs.google.com/spreadsheets/d/1UoKzzRzOCt-FXLLqDKLbryEKEgllGAQUEJ5qtmmQwpU/edit#gid=0 +``` + +2. 安装 [`gspread` 库](https://docs.gspread.org/en/v5.7.0/),通过在终端运行以下命令使 Python 中使用 [Google Sheets API](https://developers.google.com/sheets/api/guides/concepts) 更加简单:`pip install gspread` +3. 编写一个函数来从 Google Sheet 中加载数据,如下所示(用您的私有 Google Sheet 的 URL 替换 `URL` 变量): + +```python +import gspreadimport pandas as pd +# 与 Google 进行身份验证并获取表格URL = 'https://docs.google.com/spreadsheets/d/1_91Vps76SKOdDQ8cFxZQdgjTJiz23375sAT7vPvaj4k/edit#gid=0' +gc = gspread.service_account("path/to/key.json")sh = gc.open_by_url(URL)worksheet = sh.sheet1 +def get_data(): + values = worksheet.get_all_values() + df = pd.DataFrame(values[1:], columns=values[0]) + return df +``` + +4\. 数据查询是一个函数,这意味着可以使用 `gr.DataFrame` 组件实时显示数据,或使用 `gr.LinePlot` 组件实时绘制数据(当然,根据数据的不同,可能需要使用不同的图表)。要实现这一点,只需将函数传递给相应的组件,并根据需要设置 `every` 参数来确定组件刷新的频率(以秒为单位)。以下是 Gradio 代码: + +```python +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown("# 📈 实时折线图") + with gr.Row(): + with gr.Column(): + gr.DataFrame(get_data, every=gr.Timer(5)) + with gr.Column(): + gr.LinePlot(get_data, every=gr.Timer(5), x="日期", y="销售额", y_title="销售额(百万美元)", overlay_point=True, width=500, height=500) + +demo.queue().launch() # 启动带有排队功能的演示 +``` + +现在你有一个每 5 秒刷新一次的仪表盘,可以从你的 Google 表格中获取数据。 + +## 结论 + +就是这样!只需几行代码,你就可以使用 `gradio` 和其他库从公共或私有的 Google 表格中读取数据,然后在实时仪表盘中显示和绘制数据。 diff --git a/guides/cn/05_tabular-data-science-and-plots/plot-component-for-maps.md b/guides/cn/05_tabular-data-science-and-plots/plot-component-for-maps.md new file mode 100644 index 0000000..bd48d87 --- /dev/null +++ b/guides/cn/05_tabular-data-science-and-plots/plot-component-for-maps.md @@ -0,0 +1,111 @@ +# 如何使用地图组件绘制图表 + +Related spaces: +Tags: PLOTS, MAPS + +## 简介 + +本指南介绍如何使用 Gradio 的 `Plot` 组件在地图上绘制地理数据。Gradio 的 `Plot` 组件可以与 Matplotlib、Bokeh 和 Plotly 一起使用。在本指南中,我们将使用 Plotly 进行操作。Plotly 可以让开发人员轻松创建各种地图来展示他们的地理数据。点击[这里](https://plotly.com/python/maps/)查看一些示例。 + +## 概述 + +我们将使用纽约市的 Airbnb 数据集,该数据集托管在 kaggle 上,点击[这里](https://www.kaggle.com/datasets/dgomonov/new-york-city-airbnb-open-data)。我已经将其上传到 Hugging Face Hub 作为一个数据集,方便使用和下载,点击[这里](https://huggingface.co/datasets/gradio/NYC-Airbnb-Open-Data)。使用这些数据,我们将在地图上绘制 Airbnb 的位置,并允许基于价格和位置进行筛选。下面是我们将要构建的演示。 ⚡️ + +$demo_map_airbnb + +## 步骤 1-加载 CSV 数据 💾 + +让我们首先从 Hugging Face Hub 加载纽约市的 Airbnb 数据。 + +```python +from datasets import load_dataset + +dataset = load_dataset("gradio/NYC-Airbnb-Open-Data", split="train") +df = dataset.to_pandas() + +def filter_map(min_price, max_price, boroughs): + new_df = df[(df['neighbourhood_group'].isin(boroughs)) & + (df['price'] > min_price) & (df['price'] < max_price)] + names = new_df["name"].tolist() + prices = new_df["price"].tolist() + text_list = [(names[i], prices[i]) for i in range(0, len(names))] +``` + +在上面的代码中,我们先将 CSV 数据加载到一个 pandas dataframe 中。让我们首先定义一个函数,这将作为 gradio 应用程序的预测函数。该函数将接受最低价格、最高价格范围和筛选结果地区的列表作为参数。我们可以使用传入的值 (`min_price`、`max_price` 和地区列表) 来筛选数据框并创建 `new_df`。接下来,我们将创建包含每个 Airbnb 的名称和价格的 `text_list`,以便在地图上使用作为标签。 + +## 步骤 2-地图图表 🌐 + +Plotly 使得处理地图变得很容易。让我们看一下下面的代码,了解如何创建地图图表。 + +```python +import plotly.graph_objects as go + +fig = go.Figure(go.Scattermapbox( + customdata=text_list, + lat=new_df['latitude'].tolist(), + lon=new_df['longitude'].tolist(), + mode='markers', + marker=go.scattermapbox.Marker( + size=6 + ), + hoverinfo="text", + hovertemplate='Name: %{customdata[0]}
Price: $%{customdata[1]}' + )) + +fig.update_layout( + mapbox_style="open-street-map", + hovermode='closest', + mapbox=dict( + bearing=0, + center=go.layout.mapbox.Center( + lat=40.67, + lon=-73.90 + ), + pitch=0, + zoom=9 + ), +) +``` + +上面的代码中,我们通过传入经纬度列表来创建一个散点图。我们还传入了名称和价格的自定义数据,以便在鼠标悬停在每个标记上时显示额外的信息。接下来,我们使用 `update_layout` 来指定其他地图设置,例如缩放和居中。 + +有关使用 Mapbox 和 Plotly 创建散点图的更多信息,请点击[这里](https://plotly.com/python/scattermapbox/)。 + +## 步骤 3-Gradio 应用程序 ⚡️ + +我们将使用两个 `gr.Number` 组件和一个 `gr.CheckboxGroup` 组件,允许用户指定价格范围和地区位置。然后,我们将使用 `gr.Plot` 组件作为我们之前创建的 Plotly + Mapbox 地图的输出。 + +```python +with gr.Blocks() as demo: + with gr.Column(): + with gr.Row(): + min_price = gr.Number(value=250, label="Minimum Price") + max_price = gr.Number(value=1000, label="Maximum Price") + boroughs = gr.CheckboxGroup(choices=["Queens", "Brooklyn", "Manhattan", "Bronx", "Staten Island"], value=["Queens", "Brooklyn"], label="Select Boroughs:") + btn = gr.Button(value="Update Filter") + map = gr.Plot() + demo.load(filter_map, [min_price, max_price, boroughs], map) + btn.click(filter_map, [min_price, max_price, boroughs], map) +``` + +我们使用 `gr.Column` 和 `gr.Row` 布局这些组件,并为演示加载时和点击 " 更新筛选 " 按钮时添加了事件触发器,以触发地图更新新的筛选条件。 + +以下是完整演示代码: + +$code_map_airbnb + +## 步骤 4-部署 Deployment 🤗 + +如果你运行上面的代码,你的应用程序将在本地运行。 +如果要获取临时共享链接,可以将 `share=True` 参数传递给 `launch`。 + +但如果你想要一个永久的部署解决方案呢? +让我们将我们的 Gradio 应用程序部署到免费的 HuggingFace Spaces 平台。 + +如果你以前没有使用过 Spaces,请按照之前的指南[这里](/using_hugging_face_integrations)。 + +## 结论 🎉 + +你已经完成了!这是构建地图演示所需的所有代码。 + +链接到演示:[地图演示](https://huggingface.co/spaces/gradio/map_airbnb)和[完整代码](https://huggingface.co/spaces/gradio/map_airbnb/blob/main/run.py)(在 Hugging Face Spaces) diff --git a/guides/cn/05_tabular-data-science-and-plots/using-gradio-for-tabular-workflows.md b/guides/cn/05_tabular-data-science-and-plots/using-gradio-for-tabular-workflows.md new file mode 100644 index 0000000..0d2f2db --- /dev/null +++ b/guides/cn/05_tabular-data-science-and-plots/using-gradio-for-tabular-workflows.md @@ -0,0 +1,103 @@ +## 使用 Gradio 进行表格数据科学工作流 + +Related spaces: https://huggingface.co/spaces/scikit-learn/gradio-skops-integration,https://huggingface.co/spaces/scikit-learn/tabular-playground,https://huggingface.co/spaces/merve/gradio-analysis-dashboard + +## 介绍 + +表格数据科学是机器学习中应用最广泛的领域,涉及的问题从客户分割到流失预测不等。在表格数据科学工作流的各个阶段中,将工作内容传达给利益相关者或客户可能很麻烦,这会阻碍数据科学家专注于重要事项,如数据分析和模型构建。数据科学家可能会花费数小时构建一个接受 DataFrame 并返回图表、预测或数据集中的聚类图的仪表板。在本指南中,我们将介绍如何使用 `gradio` 改进您的数据科学工作流程。我们还将讨论如何使用 `gradio` 和[skops](https://skops.readthedocs.io/en/stable/)一行代码即可构建界面! + +### 先决条件 + +确保您已经[安装](/getting_started)了 `gradio` Python 软件包。 + +## 让我们创建一个简单的界面! + +我们将看一下如何创建一个简单的界面,该界面根据产品信息预测故障。 + +```python +import gradio as gr +import pandas as pd +import joblib +import datasets + + +inputs = [gr.Dataframe(row_count = (2, "dynamic"), col_count=(4,"dynamic"), label="Input Data", interactive=1)] + +outputs = [gr.Dataframe(row_count = (2, "dynamic"), col_count=(1, "fixed"), label="Predictions", headers=["Failures"])] + +model = joblib.load("model.pkl") + +# we will give our dataframe as example +df = datasets.load_dataset("merve/supersoaker-failures") +df = df["train"].to_pandas() + +def infer(input_dataframe): + return pd.DataFrame(model.predict(input_dataframe)) + +gr.Interface(fn = infer, inputs = inputs, outputs = outputs, examples = [[df.head(2)]]).launch() +``` + +让我们来解析上述代码。 + +- `fn`:推理函数,接受输入数据帧并返回预测结果。 +- `inputs`:我们使用 `Dataframe` 组件作为输入。我们将输入定义为具有 2 行 4 列的数据帧,最初的数据帧将呈现出上述形状的空数据帧。当将 `row_count` 设置为 `dynamic` 时,不必依赖于正在输入的数据集来预定义组件。 +- `outputs`:用于存储输出的数据帧组件。该界面可以接受单个或多个样本进行推断,并在一列中为每个样本返回 0 或 1,因此我们将 `row_count` 设置为 2,`col_count` 设置为 1。`headers` 是由数据帧的列名组成的列表。 +- `examples`:您可以通过拖放 CSV 文件或通过示例传递 pandas DataFrame,界面会自动获取其标题。 + +现在我们将为简化版数据可视化仪表板创建一个示例。您可以在相关空间中找到更全面的版本。 + + + +```python +import gradio as gr +import pandas as pd +import datasets +import seaborn as sns +import matplotlib.pyplot as plt + +df = datasets.load_dataset("merve/supersoaker-failures") +df = df["train"].to_pandas() +df.dropna(axis=0, inplace=True) + +def plot(df): + plt.scatter(df.measurement_13, df.measurement_15, c = df.loading,alpha=0.5) + plt.savefig("scatter.png") + df['failure'].value_counts().plot(kind='bar') + plt.savefig("bar.png") + sns.heatmap(df.select_dtypes(include="number").corr()) + plt.savefig("corr.png") + plots = ["corr.png","scatter.png", "bar.png"] + return plots + +inputs = [gr.Dataframe(label="Supersoaker Production Data")] +outputs = [gr.Gallery(label="Profiling Dashboard", columns=(1,3))] + +gr.Interface(plot, inputs=inputs, outputs=outputs, examples=[df.head(100)], title="Supersoaker Failures Analysis Dashboard").launch() +``` + + + +我们将使用与训练模型相同的数据集,但这次我们将创建一个可视化仪表板以展示它。 + +- `fn`:根据数据创建图表的函数。 +- `inputs`:我们使用了与上述相同的 `Dataframe` 组件。 +- `outputs`:我们使用 `Gallery` 组件来存放我们的可视化结果。 +- `examples`:我们将数据集本身作为示例。 + +## 使用 skops 一行代码轻松加载表格数据界面 + +`skops` 是一个构建在 `huggingface_hub` 和 `sklearn` 之上的库。通过最新的 `gradio` 集成,您可以使用一行代码构建表格数据界面! + +```python +import gradio as gr + +# 标题和描述是可选的 +title = "Supersoaker产品缺陷预测" +description = "该模型预测Supersoaker生产线故障。在下面的数据帧组件中,您可以拖放数据集的任意切片或自行编辑值。" + +gr.load("huggingface/scikit-learn/tabular-playground", title=title, description=description).launch() +``` + + + +使用 `skops` 将 `sklearn` 模型推送到 Hugging Face Hub 时,会包含一个包含示例输入和列名的 `config.json` 文件,解决的任务类型是 `tabular-classification` 或 `tabular-regression`。根据任务类型,`gradio` 构建界面并使用列名和示例输入来构建它。您可以[参考 skops 在 Hub 上托管模型的文档](https://skops.readthedocs.io/en/latest/auto_examples/plot_hf_hub.html#sphx-glr-auto-examples-plot-hf-hub-py)来了解如何使用 `skops` 将模型推送到 Hub。 diff --git a/guides/cn/06_client-libraries/01_getting-started-with-the-python-client.md b/guides/cn/06_client-libraries/01_getting-started-with-the-python-client.md new file mode 100644 index 0000000..d17f0b1 --- /dev/null +++ b/guides/cn/06_client-libraries/01_getting-started-with-the-python-client.md @@ -0,0 +1,260 @@ +# 使用 Gradio Python 客户端入门 + +Tags: CLIENT, API, SPACES + +Gradio Python 客户端使得将任何 Gradio 应用程序作为 API 使用变得非常容易。例如,考虑一下从麦克风录制的[Whisper 音频文件](https://huggingface.co/spaces/abidlabs/whisper)的转录。 + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/whisper-screenshot.jpg) + +使用 `gradio_client` 库,我们可以轻松地将 Gradio 用作 API,以编程方式转录音频文件。 + +下面是完成此操作的整个代码: + +```python +from gradio_client import Client + +client = Client("abidlabs/whisper") +client.predict("audio_sample.wav") + +>> "这是Whisper语音识别模型的测试。" +``` + +Gradio 客户端适用于任何托管在 Hugging Face Spaces 上的 Gradio 应用程序,无论是图像生成器、文本摘要生成器、有状态聊天机器人、税金计算器还是其他任何应用程序!Gradio 客户端主要用于托管在[Hugging Face Spaces](https://hf.space)上的应用程序,但你的应用程序可以托管在任何地方,比如你自己的服务器。 + +**先决条件**:要使用 Gradio 客户端,你不需要详细了解 `gradio` 库。但是,了解 Gradio 的输入和输出组件的概念会有所帮助。 + +## 安装 + +如果你已经安装了最新版本的 `gradio`,那么 `gradio_client` 就作为依赖项包含在其中。 + +否则,可以使用 pip(或 pip3)安装轻量级的 `gradio_client` 包,并且已经测试可以在 Python 3.9 或更高版本上运行: + +```bash +$ pip install gradio_client +``` + +## 连接到运行中的 Gradio 应用程序 + +首先创建一个 `Client` 对象,并将其连接到运行在 Hugging Face Spaces 上或其他任何地方的 Gradio 应用程序。 + +## 连接到 Hugging Face 空间 + +```python +from gradio_client import Client + +client = Client("abidlabs/en2fr") # 一个将英文翻译为法文的Space +``` + +你还可以通过在 `hf_token` 参数中传递你的 HF 令牌来连接到私有空间。你可以在这里获取你的 HF 令牌:https://huggingface.co/settings/tokens + +```python +from gradio_client import Client + +client = Client("abidlabs/my-private-space", hf_token="...") +``` + +## 复制空间以供私人使用 + +虽然你可以将任何公共空间用作 API,但如果你发出太多请求,你可能会受到 Hugging Face 的频率限制。要无限制地使用一个空间,只需将其复制以创建一个私有空间,然后可以根据需要进行多个请求! + +`gradio_client` 包括一个类方法:`Client.duplicate()`,使这个过程变得简单(你需要传递你的[Hugging Face 令牌](https://huggingface.co/settings/tokens)或使用 Hugging Face CLI 登录): + +```python +import os +from gradio_client import Client + +HF_TOKEN = os.environ.get("HF_TOKEN") + +client = Client.duplicate("abidlabs/whisper", hf_token=HF_TOKEN) +client.predict("audio_sample.wav") + +>> "This is a test of the whisper speech recognition model." +``` + +> > " 这是 Whisper 语音识别模型的测试。" + +如果之前已复制了一个空间,重新运行 `duplicate()` 将*不会*创建一个新的空间。相反,客户端将连接到之前创建的空间。因此,多次运行 `Client.duplicate()` 方法是安全的。 + +**注意:** 如果原始空间使用了 GPU,你的私有空间也将使用 GPU,并且你的 Hugging Face 账户将根据 GPU 的价格计费。为了降低费用,在 1 小时没有活动后,你的空间将自动休眠。你还可以使用 `duplicate()` 的 `hardware` 参数来设置硬件。 + +## 连接到通用 Gradio 应用程序 + +如果你的应用程序运行在其他地方,只需提供完整的 URL,包括 "http://" 或 "https://"。下面是一个在共享 URL 上运行的 Gradio 应用程序进行预测的示例: + +```python +from gradio_client import Client + +client = Client("https://bec81a83-5b5c-471e.gradio.live") +``` + +## 检查 API 端点 + +一旦连接到 Gradio 应用程序,可以通过调用 `Client.view_api()` 方法查看可用的 API 端点。对于 Whisper 空间,我们可以看到以下信息: + +```bash +Client.predict() Usage Info +--------------------------- +Named API endpoints: 1 + + - predict(input_audio, api_name="/predict") -> value_0 + Parameters: + - [Audio] input_audio: str (filepath or URL) + Returns: + - [Textbox] value_0: str (value) +``` + +这显示了在此空间中有 1 个 API 端点,并显示了如何使用 API 端点进行预测:我们应该调用 `.predict()` 方法(我们将在下面探讨),提供类型为 `str` 的参数 `input_audio`,它是一个`文件路径或 URL`。 + +我们还应该提供 `api_name='/predict'` 参数给 `predict()` 方法。虽然如果一个 Gradio 应用程序只有一个命名的端点,这不是必需的,但它允许我们在单个应用程序中调用不同的端点(如果它们可用)。如果一个应用程序有无名的 API 端点,可以通过运行 `.view_api(all_endpoints=True)` 来显示它们。 + +## 进行预测 + +进行预测的最简单方法是只需使用相应的参数调用 `.predict()` 函数: + +```python +from gradio_client import Client + +client = Client("abidlabs/en2fr", api_name='/predict') +client.predict("Hello") + +>> Bonjour +``` + +如果有多个参数,那么你应该将它们作为单独的参数传递给 `.predict()`,就像这样: + +````python +from gradio_client import Client + +client = Client("gradio/calculator") +client.predict(4, "add", 5) + +>> 9.0 + + +对于某些输入,例如图像,你应该传递文件的文件路径或URL。同样,对应的输出类型,你将获得一个文件路径或URL。 + + +```python +from gradio_client import Client + +client = Client("abidlabs/whisper") +client.predict("https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3") + +>> "My thought I have nobody by a beauty and will as you poured. Mr. Rochester is serve in that so don't find simpus, and devoted abode, to at might in a r—" + +```` + +## 异步运行任务(Running jobs asynchronously) + +应注意`.predict()`是一个*阻塞*操作,因为它在返回预测之前等待操作完成。 + +在许多情况下,直到你需要预测结果之前,你最好让作业在后台运行。你可以通过使用`.submit()`方法创建一个`Job`实例,然后稍后调用`.result()`在作业上获取结果。例如: + +```python +from gradio_client import Client + +client = Client(space="abidlabs/en2fr") +job = client.submit("Hello", api_name="/predict") # 这不是阻塞的 + +# 做其他事情 + +job.result() # 这是阻塞的 + +>> Bonjour +``` + +## 添加回调 (Adding callbacks) + +或者,可以添加一个或多个回调来在作业完成后执行操作,像这样: + +```python +from gradio_client import Client + +def print_result(x): + print(" 翻译的结果是:{x}") + +client = Client(space="abidlabs/en2fr") + +job = client.submit("Hello", api_name="/predict", result_callbacks=[print_result]) + +# 做其他事情 + +>> 翻译的结果是:Bonjour + +``` + +## 状态 (Status) + +`Job`对象还允许您通过调用`.status()`方法获取运行作业的状态。这将返回一个`StatusUpdate`对象,具有以下属性:`code`(状态代码,其中之一表示状态的一组定义的字符串。参见`utils.Status`类)、`rank`(此作业在队列中的当前位置)、`queue_size`(总队列大小)、`eta`(此作业将完成的预计时间)、`success`(表示作业是否成功完成的布尔值)和`time`(生成状态的时间)。 + +```py +from gradio_client import Client + +client = Client(src="gradio/calculator") +job = client.submit(5, "add", 4, api_name="/predict") +job.status() + +>> +``` + +_注意_:`Job`类还有一个`.done()`实例方法,返回一个布尔值,指示作业是否已完成。 + +## 取消作业 (Cancelling Jobs) + +`Job`类还有一个`.cancel()`实例方法,取消已排队但尚未开始的作业。例如,如果你运行: + +```py +client = Client("abidlabs/whisper") +job1 = client.submit("audio_sample1.wav") +job2 = client.submit("audio_sample2.wav") +job1.cancel() # 将返回 False,假设作业已开始 +job2.cancel() # 将返回 True,表示作业已取消 +``` + +如果第一个作业已开始处理,则它将不会被取消。如果第二个作业尚未开始,则它将成功取消并从队列中删除。 + +## 生成器端点 (Generator Endpoints) + +某些Gradio API端点不返回单个值,而是返回一系列值。你可以随时从这样的生成器端点获取返回的一系列值,方法是运行`job.outputs()`: + +```py +from gradio_client import Client + +client = Client(src="gradio/count_generator") +job = client.submit(3, api_name="/count") +while not job.done(): + time.sleep(0.1) +job.outputs() + +>> ['0', '1', '2'] +``` + +请注意,在生成器端点上运行`job.result()`只会获得端点返回的*第一个*值。 + +`Job`对象还是可迭代的,这意味着您可以使用它按照从端点返回的结果逐个显示生成器函数的结果。以下是使用`Job`作为生成器的等效示例: + +```py +from gradio_client import Client + +client = Client(src="gradio/count_generator") +job = client.submit(3, api_name="/count") + +for o in job: + print(o) + +>> 0 +>> 1 +>> 2 +``` + +你还可以取消具有迭代输出的作业,在这种情况下,作业将在当前迭代完成运行后完成。 + +```py +from gradio_client import Client +import time + +client = Client("abidlabs/test-yield") +job = client.submit("abcdef") +time.sleep(3) +job.cancel() # 作业在运行 2 个迭代后取消 +``` diff --git a/guides/cn/06_client-libraries/02_getting-started-with-the-js-client.md b/guides/cn/06_client-libraries/02_getting-started-with-the-js-client.md new file mode 100644 index 0000000..a651f9c --- /dev/null +++ b/guides/cn/06_client-libraries/02_getting-started-with-the-js-client.md @@ -0,0 +1,267 @@ +# 使用Gradio JavaScript客户端快速入门 + +Tags: CLIENT, API, SPACES + +Gradio JavaScript客户端使得使用任何Gradio应用作为API非常简单。例如,考虑一下这个[从麦克风录音的Hugging Face Space,用于转录音频文件](https://huggingface.co/spaces/abidlabs/whisper)。 + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/whisper-screenshot.jpg) + +使用`@gradio/client`库,我们可以轻松地以编程方式使用Gradio作为API来转录音频文件。 + +以下是完成此操作的完整代码: + +```js +import { Client } from "@gradio/client"; + +const response = await fetch( + "https://github.com/audio-samples/audio-samples.github.io/raw/master/samples/wav/ted_speakers/SalmanKhan/sample-1.wav" +); +const audio_file = await response.blob(); + +const app = await Client.connect("abidlabs/whisper"); +const transcription = await app.predict("/predict", [audio_file]); + +console.log(transcription.data); +// [ "I said the same phrase 30 times." ] +``` + +Gradio客户端适用于任何托管的Gradio应用,无论是图像生成器、文本摘要生成器、有状态的聊天机器人、税收计算器还是其他任何应用!Gradio客户端通常与托管在[Hugging Face Spaces](https://hf.space)上的应用一起使用,但您的应用可以托管在任何地方,比如您自己的服务器。 + +**先决条件**:要使用Gradio客户端,您不需要深入了解`gradio`库的细节。但是,熟悉Gradio的输入和输出组件的概念会有所帮助。 + +## 安装 + +可以使用您选择的软件包管理器从npm注册表安装轻量级的`@gradio/client`包,并支持18及以上的Node版本: + +```bash +npm i @gradio/client +``` + +## 连接到正在运行的Gradio应用 + +首先,通过实例化`Client`对象并将其连接到在Hugging Face Spaces或任何其他位置运行的Gradio应用来建立连接。 + +## 连接到Hugging Face Space + +```js +import { Client } from "@gradio/client"; + +const app = Client.connect("abidlabs/en2fr"); // 一个从英语翻译为法语的 Space +``` + +您还可以通过在options参数的`hf_token`属性中传入您的HF token来连接到私有Spaces。您可以在此处获取您的HF token:https://huggingface.co/settings/tokens + +```js +import { Client } from "@gradio/client"; + +const app = Client.connect("abidlabs/my-private-space", { hf_token="hf_..." }) +``` + +## 为私人使用复制一个Space + +虽然您可以将任何公共Space用作API,但是如果您发出的请求过多,Hugging Face可能会对您进行速率限制。为了无限制使用Space,只需复制Space以创建私有Space,然后使用它来进行任意数量的请求! + +`@gradio/client`还导出了另一个函数`duplicate`,以使此过程变得简单(您将需要传入您的[Hugging Face token](https://huggingface.co/settings/tokens))。 + +`duplicate`与`Client`几乎相同,唯一的区别在于底层实现: + +```js +import { Client } from "@gradio/client"; + +const response = await fetch( + "https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3" +); +const audio_file = await response.blob(); + +const app = await Client.duplicate("abidlabs/whisper", { hf_token: "hf_..." }); +const transcription = app.predict("/predict", [audio_file]); +``` + +如果您之前复制过一个Space,则重新运行`duplicate`不会创建一个新的Space。而是客户端将连接到先前创建的Space。因此,可以安全地多次使用相同的Space重新运行`duplicate`方法。 + +**注意:**如果原始Space使用了GPU,您的私有Space也将使用GPU,并且将根据GPU的价格向您的Hugging Face账户计费。为了最大程度地减少费用,在5分钟不活动后,您的Space将自动进入休眠状态。您还可以使用`duplicate`的options对象的`hardware`和`timeout`属性来设置硬件,例如: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.duplicate("abidlabs/whisper", { + hf_token: "hf_...", + timeout: 60, + hardware: "a10g-small" +}); +``` + +## 连接到通用的Gradio应用 + +如果您的应用程序在其他地方运行,只需提供完整的URL,包括"http://"或"https://"。以下是向运行在共享URL上的Gradio应用进行预测的示例: + +```js +import { Client } from "@gradio/client"; + +const app = Client.connect("https://bec81a83-5b5c-471e.gradio.live"); +``` + +## 检查API端点 + +一旦连接到Gradio应用程序,可以通过调用`Client`的`view_api`方法来查看可用的API端点。 + +对于Whisper Space,我们可以这样做: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("abidlabs/whisper"); + +const app_info = await app.view_info(); + +console.log(app_info); +``` + +然后我们会看到以下内容: + +```json +{ + "named_endpoints": { + "/predict": { + "parameters": [ + { + "label": "text", + "component": "Textbox", + "type": "string" + } + ], + "returns": [ + { + "label": "output", + "component": "Textbox", + "type": "string" + } + ] + } + }, + "unnamed_endpoints": {} +} +``` + +这告诉我们该Space中有1个API端点,并显示了如何使用API端点进行预测:我们应该调用`.predict()`方法(下面将进行更多探索),并提供类型为`string`的参数`input_audio`,它是指向文件的URL。 + +我们还应该提供`api_name='/predict'`参数给`predict()`方法。虽然如果一个Gradio应用只有1个命名的端点,这不是必需的,但它可以允许我们在单个应用中调用不同的端点。如果应用有未命名的API端点,可以通过运行`.view_api(all_endpoints=True)`来显示它们。 + +## 进行预测 + +进行预测的最简单方法就是使用适当的参数调用`.predict()`方法: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("abidlabs/en2fr"); +const result = await app.predict("/predict", ["Hello"]); +``` + +如果有多个参数,您应该将它们作为一个数组传递给`.predict()`,像这样: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("gradio/calculator"); +const result = await app.predict("/predict", [4, "add", 5]); +``` + +对于某些输入,例如图像,您应该根据所需要的方便程度传入`Buffer`、`Blob`或`File`。在Node.js中,可以使用`Buffer`或`Blob`;在浏览器环境中,可以使用`Blob`或`File`。 + +```js +import { Client } from "@gradio/client"; + +const response = await fetch( + "https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3" +); +const audio_file = await response.blob(); + +const app = await Client.connect("abidlabs/whisper"); +const result = await Client.connect.predict("/predict", [audio_file]); +``` + +## 使用事件 + +如果您使用的API可以随时间返回结果,或者您希望访问有关作业状态的信息,您可以使用事件接口获取更大的灵活性。这对于迭代的或生成器的端点特别有用,因为它们会生成一系列离散的响应值。 + +```js +import { Client } from "@gradio/client"; + +function log_result(payload) { + const { + data: [translation] + } = payload; + + console.log(`翻译结果为:${translation}`); +} + +const app = await Client.connect("abidlabs/en2fr"); +const job = app.submit("/predict", ["Hello"]); + +job.on("data", log_result); +``` + +## 状态 + +事件接口还可以通过监听`"status"`事件来获取运行作业的状态。这将返回一个对象,其中包含以下属性:`status`(当前作业的人类可读状态,`"pending" | "generating" | "complete" | "error"`),`code`(作业的详细gradio code),`position`(此作业在队列中的当前位置),`queue_size`(总队列大小),`eta`(作业完成的预计时间),`success`(表示作业是否成功完成的布尔值)和`time`(作业状态生成的时间,是一个`Date`对象)。 + +```js +import { Client } from "@gradio/client"; + +function log_status(status) { + console.log(`此作业的当前状态为:${JSON.stringify(status, null, 2)}`); +} + +const app = await Client.connect("abidlabs/en2fr"); +const job = app.submit("/predict", ["Hello"]); + +job.on("status", log_status); +``` + +## 取消作业 + +作业实例还具有`.cancel()`方法,用于取消已排队但尚未启动的作业。例如,如果您运行以下命令: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("abidlabs/en2fr"); +const job_one = app.submit("/predict", ["Hello"]); +const job_two = app.submit("/predict", ["Friends"]); + +job_one.cancel(); +job_two.cancel(); +``` + +如果第一个作业已经开始处理,那么它将不会被取消,但客户端将不再监听更新(丢弃该作业)。如果第二个作业尚未启动,它将被成功取消并从队列中移除。 + +## 生成器端点 + +某些Gradio API端点不返回单个值,而是返回一系列值。您可以使用事件接口实时侦听这些值: + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("gradio/count_generator"); +const job = app.submit(0, [9]); + +job.on("data", (data) => console.log(data)); +``` + +这将按生成端点生成的值进行日志记录。 + +您还可以取消具有迭代输出的作业,在这种情况下,作业将立即完成。 + +```js +import { Client } from "@gradio/client"; + +const app = await Client.connect("gradio/count_generator"); +const job = app.submit(0, [9]); + +job.on("data", (data) => console.log(data)); + +setTimeout(() => { + job.cancel(); +}, 3000); +``` diff --git a/guides/cn/06_client-libraries/fastapi-app-with-the-gradio-client.md b/guides/cn/06_client-libraries/fastapi-app-with-the-gradio-client.md new file mode 100644 index 0000000..58ef087 --- /dev/null +++ b/guides/cn/06_client-libraries/fastapi-app-with-the-gradio-client.md @@ -0,0 +1,193 @@ +# 使用Gradio Python客户端构建FastAPI应用 + +Tags: CLIENT, API, WEB APP + +在本博客文章中,我们将演示如何使用 `gradio_client` [Python库](getting-started-with-the-python-client/) 来以编程方式创建Gradio应用的请求,通过创建一个示例FastAPI Web应用。我们将构建的 Web 应用名为“Acappellify”,它允许用户上传视频文件作为输入,并返回一个没有伴奏音乐的视频版本。它还会显示生成的视频库。 + +**先决条件** + +在开始之前,请确保您正在运行Python 3.9或更高版本,并已安装以下库: + +- `gradio_client` +- `fastapi` +- `uvicorn` + +您可以使用`pip`安装这些库: + +```bash +$ pip install gradio_client fastapi uvicorn +``` + +您还需要安装ffmpeg。您可以通过在终端中运行以下命令来检查您是否已安装ffmpeg: + +```bash +$ ffmpeg version +``` + +否则,通过按照这些说明安装ffmpeg [链接](https://www.hostinger.com/tutorials/how-to-install-ffmpeg)。 + +## 步骤1:编写视频处理函数 + +让我们从似乎最复杂的部分开始--使用机器学习从视频中去除音乐。 + +幸运的是,我们有一个现有的Space可以简化这个过程:[https://huggingface.co/spaces/abidlabs/music-separation](https://huggingface.co/spaces/abidlabs/music-separation)。该空间接受一个音频文件,并生成两个独立的音频文件:一个带有伴奏音乐,一个带有原始剪辑中的其他所有声音。非常适合我们的客户端使用! + +打开一个新的Python文件,比如`main.py`,并通过从`gradio_client`导入 `Client` 类,并将其连接到该Space: + +```py +from gradio_client import Client + +client = Client("abidlabs/music-separation") + +def acapellify(audio_path): + result = client.predict(audio_path, api_name="/predict") + return result[0] +``` + +所需的代码仅如上所示--请注意,API端点返回一个包含两个音频文件(一个没有音乐,一个只有音乐)的列表,因此我们只返回列表的第一个元素。 + +--- + +**注意**:由于这是一个公共Space,可能会有其他用户同时使用该Space,这可能导致速度较慢。您可以使用自己的[Hugging Face token](https://huggingface.co/settings/tokens)复制此Space,创建一个只有您自己访问权限的私有Space,并绕过排队。要做到这一点,只需用下面的代码替换上面的前两行: + +```py +from gradio_client import Client + +client = Client.duplicate("abidlabs/music-separation", hf_token=YOUR_HF_TOKEN) +``` + +其他的代码保持不变! + +--- + +现在,当然,我们正在处理视频文件,所以我们首先需要从视频文件中提取音频。为此,我们将使用`ffmpeg`库,它在处理音频和视频文件时做了很多艰巨的工作。使用`ffmpeg`的最常见方法是通过命令行,在Python的`subprocess`模块中调用它: + +我们的视频处理工作流包含三个步骤: + +1. 首先,我们从视频文件路径开始,并使用`ffmpeg`提取音频。 +2. 然后,我们通过上面的`acapellify()`函数传入音频文件。 +3. 最后,我们将新音频与原始视频合并,生成最终的Acapellify视频。 + +以下是Python中的完整代码,您可以将其添加到`main.py`文件中: + +```python +import subprocess + +def process_video(video_path): + old_audio = os.path.basename(video_path).split(".")[0] + ".m4a" + subprocess.run(['ffmpeg', '-y', '-i', video_path, '-vn', '-acodec', 'copy', old_audio]) + + new_audio = acapellify(old_audio) + + new_video = f"acap_{video_path}" + subprocess.call(['ffmpeg', '-y', '-i', video_path, '-i', new_audio, '-map', '0:v', '-map', '1:a', '-c:v', 'copy', '-c:a', 'aac', '-strict', 'experimental', f"static/{new_video}"]) + return new_video +``` + +如果您想了解所有命令行参数的详细信息,请阅读[ffmpeg文档](https://ffmpeg.org/ffmpeg.html),因为它们超出了本教程的范围。 + +## 步骤2: 创建一个FastAPI应用(后端路由) + +接下来,我们将创建一个简单的FastAPI应用程序。如果您以前没有使用过FastAPI,请查看[优秀的FastAPI文档](https://fastapi.tiangolo.com/)。否则,下面的基本模板将看起来很熟悉,我们将其添加到`main.py`中: + +```python +import os +from fastapi import FastAPI, File, UploadFile, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +app = FastAPI() +os.makedirs("static", exist_ok=True) +app.mount("/static", StaticFiles(directory="static"), name="static") +templates = Jinja2Templates(directory="templates") + +videos = [] + +@app.get("/", response_class=HTMLResponse) +async def home(request: Request): + return templates.TemplateResponse( + "home.html", {"request": request, "videos": videos}) + +@app.post("/uploadvideo/") +async def upload_video(video: UploadFile = File(...)): + new_video = process_video(video.filename) + videos.append(new_video) + return RedirectResponse(url='/', status_code=303) +``` + +在这个示例中,FastAPI应用程序有两个路由:`/` 和 `/uploadvideo/`。 + +`/` 路由返回一个显示所有上传视频的画廊的HTML模板。 + +`/uploadvideo/` 路由接受一个带有`UploadFile`对象的 `POST` 请求,表示上传的视频文件。视频文件通过`process_video()`方法进行 "acapellify",并将输出视频存储在一个列表中,该列表在内存中存储了所有上传的视频。 + +请注意,这只是一个非常基本的示例,如果这是一个发布应用程序,则需要添加更多逻辑来处理文件存储、用户身份验证和安全性考虑等。 + +## 步骤3:创建一个FastAPI应用(前端模板) + +最后,我们创建Web应用的前端。首先,在与`main.py`相同的目录下创建一个名为`templates`的文件夹。然后,在`templates`文件夹中创建一个名为`home.html`的模板。下面是最终的文件结构: + +```csv +├── main.py +├── templates +│ └── home.html +``` + +将以下内容写入`home.html`文件中: + +```html +<!DOCTYPE html> <html> <head> <title> 视频库 </title> <style> +body { font-family: sans-serif; margin: 0; padding: 0; background-color: +#f5f5f5; } h1 { text-align: center; margin-top: 30px; margin-bottom: 20px; } +.gallery { display: flex; flex-wrap: wrap; justify-content: center; gap: 20px; +padding: 20px; } .video { border: 2px solid #ccc; box-shadow: 0px 0px 10px +rgba(0, 0, 0, 0.2); border-radius: 5px; overflow: hidden; width: 300px; +margin-bottom: 20px; } .video video { width: 100%; height: 200px; } .video p { +text-align: center; margin: 10px 0; } form { margin-top: 20px; text-align: +center; } input[type="file"] { display: none; } .upload-btn { display: +inline-block; background-color: #3498db; color: #fff; padding: 10px 20px; +font-size: 16px; border: none; border-radius: 5px; cursor: pointer; } +.upload-btn:hover { background-color: #2980b9; } .file-name { margin-left: 10px; +} </style> </head> <body> <h1> 视频库 </h1> {% if videos %} +<div class="gallery"> {% for video in videos %} <div class="video"> +<video controls> <source src="{{ url_for('static', path=video) }}" +type="video/mp4"> 您的浏览器不支持视频标签。 </video> <p>{{ video +}}</p> </div> {% endfor %} </div> {% else %} <p> +尚未上传任何视频。</p> {% endif %} <form action="/uploadvideo/" +method="post" enctype="multipart/form-data"> <label for="video-upload" +class="upload-btn"> 选择视频文件 </label> <input type="file" name="video" +id="video-upload"> <span class="file-name"></span> <button +type="submit" class="upload-btn"> 上传 </button> </form> <script> // +在表单中显示所选文件名 const fileUpload = +document.getElementById("video-upload"); const fileName = +document.querySelector(".file-name"); fileUpload.addEventListener("change", (e) +=> { fileName.textContent = e.target.files[0].name; }); </script> </body> +</html> +``` + +## 第4步:运行 FastAPI 应用 + +最后,我们准备好运行由 Gradio Python 客户端提供支持的 FastAPI 应用程序。 + +打开终端并导航到包含 `main.py` 文件的目录,然后在终端中运行以下命令: + +```bash +$ uvicorn main:app +``` + +您应该会看到如下输出: + +```csv +Loaded as API: https://abidlabs-music-separation.hf.space ✔ +INFO: Started server process [1360] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +就是这样!开始上传视频,您将在响应中得到一些“acapellified”视频(处理时间根据您的视频长度可能需要几秒钟到几分钟)。以下是上传两个视频后 UI 的外观: + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/acapellify.png) + +如果您想了解如何在项目中使用 Gradio Python 客户端的更多信息,请[阅读专门的指南](/getting-started-with-the-python-client/)。 diff --git a/guides/cn/06_client-libraries/gradio-and-llm-agents.md b/guides/cn/06_client-libraries/gradio-and-llm-agents.md new file mode 100644 index 0000000..3e15045 --- /dev/null +++ b/guides/cn/06_client-libraries/gradio-and-llm-agents.md @@ -0,0 +1,135 @@ +# Gradio & LLM Agents 🤝 + +非常强大的大型语言模型(LLM),如果我们能赋予它们完成专门任务的技能,它们将变得更加强大。 + +[gradio_tools](https://github.com/freddyaboulton/gradio-tools)库可以将任何[Gradio](https://github.com/gradio-app/gradio)应用程序转化为[工具](https://python.langchain.com/en/latest/modules/agents/tools.html),供[代理](https://docs.langchain.com/docs/components/agents/agent)使用以完成任务。例如,一个LLM可以使用Gradio工具转录在网上找到的语音记录,然后为您summarize它。或者它可以使用不同的Gradio工具对您的Google Drive上的文档应用OCR,然后回答相关问题。 + +本指南将展示如何使用`gradio_tools`让您的LLM代理访问全球托管的最先进的Gradio应用程序。尽管`gradio_tools`与不止一个代理框架兼容,但本指南将重点介绍[Langchain代理](https://docs.langchain.com/docs/components/agents/)。 + +## 一些背景信息 + +### 代理是什么? + +[LangChain代理](https://docs.langchain.com/docs/components/agents/agent)是一个大型语言模型(LLM),它根据使用其众多工具之一的输入来生成输出。 + +### Gradio是什么? + +[Gradio](https://github.com/gradio-app/gradio)是用于构建机器学习Web应用程序并与全球共享的事实上的标准框架-完全由Python驱动!🐍 + +## gradio_tools - 一个端到端的示例 + +要开始使用`gradio_tools`,您只需要导入和初始化工具,然后将其传递给langchain代理! + +在下面的示例中,我们导入`StableDiffusionPromptGeneratorTool`以创建一个良好的稳定扩散提示, +`StableDiffusionTool`以使用我们改进的提示创建一张图片,`ImageCaptioningTool`以为生成的图片加上标题,以及 +`TextToVideoTool`以根据提示创建一个视频。 + +然后,我们告诉我们的代理创建一张狗正在滑板的图片,但在使用图像生成器之前请先改进我们的提示。我们还要求 +它为生成的图片添加标题并创建一个视频。代理可以根据需要决定使用哪个工具,而不需要我们明确告知。 + +```python +import os + +if not os.getenv("OPENAI_API_KEY"): + raise ValueError("OPENAI_API_KEY 必须设置 ") + +from langchain.agents import initialize_agent +from langchain.llms import OpenAI +from gradio_tools import (StableDiffusionTool, ImageCaptioningTool, StableDiffusionPromptGeneratorTool, + TextToVideoTool) + +from langchain.memory import ConversationBufferMemory + +llm = OpenAI(temperature=0) +memory = ConversationBufferMemory(memory_key="chat_history") +tools = [StableDiffusionTool().langchain, ImageCaptioningTool().langchain, + StableDiffusionPromptGeneratorTool().langchain, TextToVideoTool().langchain] + +agent = initialize_agent(tools, llm, memory=memory, agent="conversational-react-description", verbose=True) +output = agent.run(input=("Please create a photo of a dog riding a skateboard " + "but improve my prompt prior to using an image generator." + "Please caption the generated image and create a video for it using the improved prompt.")) +``` + +您会注意到我们正在使用一些与`gradio_tools`一起提供的预构建工具。请参阅此[文档](https://github.com/freddyaboulton/gradio-tools#gradio-tools-gradio--llm-agents)以获取完整的`gradio_tools`工具列表。 +如果您想使用当前不在`gradio_tools`中的工具,很容易添加您自己的工具。下一节将介绍如何添加自己的工具。 + +## gradio_tools - 创建自己的工具 + +核心抽象是`GradioTool`,它允许您为LLM定义一个新的工具,只要您实现标准接口: + +```python +class GradioTool(BaseTool): + + def __init__(self, name: str, description: str, src: str) -> None: + + @abstractmethod + def create_job(self, query: str) -> Job: + pass + + @abstractmethod + def postprocess(self, output: Tuple[Any] | Any) -> str: + pass +``` + +需要满足的要求是: + +1. 工具的名称 +2. 工具的描述。这非常关键!代理根据其描述决定使用哪个工具。请确切描述输入和输出应该是什么样的,最好包括示例。 +3. Gradio应用程序的url或space id,例如`freddyaboulton/calculator`。基于该值,`gradio_tool`将通过API创建一个[gradio客户端](https://github.com/gradio-app/gradio/blob/main/client/python/README.md)实例来查询上游应用程序。如果您不熟悉gradio客户端库,请确保点击链接了解更多信息。 +4. create_job - 给定一个字符串,该方法应该解析该字符串并从客户端返回一个job。大多数情况下,这只需将字符串传递给客户端的`submit`函数即可。有关创建job的更多信息,请参阅[这里](https://github.com/gradio-app/gradio/blob/main/client/python/README.md#making-a-prediction) +5. postprocess - 给定作业的结果,将其转换为LLM可以向用户显示的字符串。 +6. _Optional可选_ - 某些库,例如[MiniChain](https://github.com/srush/MiniChain/tree/main),可能需要一些关于工具使用的底层gradio输入和输出类型的信息。默认情况下,这将返回gr.Textbox(),但如果您想提供更准确的信息,请实现工具的`_block_input(self, gr)`和`_block_output(self, gr)`方法。`gr`变量是gradio模块(通过`import gradio as gr`获得的结果)。`GradiTool`父类将自动引入`gr`并将其传递给`_block_input`和`_block_output`方法。 + +就是这样! + +一旦您创建了自己的工具,请在`gradio_tools`存储库上发起拉取请求!我们欢迎所有贡献。 + +## 示例工具 - 稳定扩散 + +以下是作为示例的稳定扩散工具代码: + +from gradio_tool import GradioTool +import os + +class StableDiffusionTool(GradioTool): +"""Tool for calling stable diffusion from llm""" + + def __init__( + self, + name="StableDiffusion", + description=( + "An image generator. Use this to generate images based on " + "text input. Input should be a description of what the image should " + "look like. The output will be a path to an image file." + ), + src="gradio-client-demos/stable-diffusion", + hf_token=None, + ) -> None: + super().__init__(name, description, src, hf_token) + + def create_job(self, query: str) -> Job: + return self.client.submit(query, "", 9, fn_index=1) + + def postprocess(self, output: str) -> str: + return [os.path.join(output, i) for i in os.listdir(output) if not i.endswith("json")][0] + + def _block_input(self, gr) -> "gr.components.Component": + return gr.Textbox() + + def _block_output(self, gr) -> "gr.components.Component": + return gr.Image() + +``` +关于此实现的一些注意事项: +1. 所有的 `GradioTool` 实例都有一个名为 `client` 的属性,它指向底层的 [gradio 客户端](https://github.com/gradio-app/gradio/tree/main/client/python#gradio_client-use-a-gradio-app-as-an-api----in-3-lines-of-python),这就是您在 `create_job` 方法中应该使用的内容。 + +2. `create_job` 方法只是将查询字符串传递给客户端的 `submit` 函数,并硬编码了一些其他参数,即负面提示字符串和指南缩放。我们可以在后续版本中修改我们的工具,以便从输入字符串中接受这些值。 + +3. `postprocess` 方法只是返回由稳定扩散空间创建的图库中的第一个图像。我们使用 `os` 模块获取图像的完整路径。 + +## Conclusion + +现在,您已经知道如何通过数千个运行在野外的 gradio 空间来扩展您的 LLM 的能力了! +同样,我们欢迎对 [gradio_tools](https://github.com/freddyaboulton/gradio-tools) 库的任何贡献。我们很兴奋看到大家构建的工具! +``` diff --git a/guides/cn/07_other-tutorials/create-your-own-friends-with-a-gan.md b/guides/cn/07_other-tutorials/create-your-own-friends-with-a-gan.md new file mode 100644 index 0000000..d610fea --- /dev/null +++ b/guides/cn/07_other-tutorials/create-your-own-friends-with-a-gan.md @@ -0,0 +1,226 @@ +# 使用 GAN 创建您自己的朋友 + +spaces/NimaBoscarino/cryptopunks, https://huggingface.co/spaces/nateraw/cryptopunks-generator +Tags: GAN, IMAGE, HUB + +由 Nima BoscarinoNate Raw 贡献 + +## 简介 + +最近,加密货币、NFTs 和 Web3 运动似乎都非常流行!数字资产以惊人的金额在市场上上市,几乎每个名人都推出了自己的 NFT 收藏。虽然您的加密资产可能是应税的,例如在加拿大(https://www.canada.ca/en/revenue-agency/programs/about-canada-revenue-agency-cra/compliance/digital-currency/cryptocurrency-guide.html),但今天我们将探索一些有趣且无税的方法来生成自己的一系列过程生成的 CryptoPunks(https://www.larvalabs.com/cryptopunks)。 + +生成对抗网络(GANs),通常称为 GANs,是一类特定的深度学习模型,旨在通过学习输入数据集来创建(生成!)与原始训练集中的元素具有令人信服的相似性的新材料。众所周知,网站[thispersondoesnotexist.com](https://thispersondoesnotexist.com/)通过名为 StyleGAN2 的模型生成了栩栩如生但是合成的人物图像而迅速走红。GANs 在机器学习领域获得了人们的关注,现在被用于生成各种图像、文本甚至音乐! + +今天我们将简要介绍 GAN 的高级直觉,然后我们将围绕一个预训练的 GAN 构建一个小型演示,看看这一切都是怎么回事。下面是我们将要组合的东西的一瞥: + + + +### 先决条件 + +确保已经[安装](/getting_started)了 `gradio` Python 包。要使用预训练模型,请还安装 `torch` 和 `torchvision`。 + +## GANs:简介 + +最初在[Goodfellow 等人 2014 年的论文](https://arxiv.org/abs/1406.2661)中提出,GANs 由互相竞争的神经网络组成,旨在相互智能地欺骗对方。一种网络,称为“生成器”,负责生成图像。另一个网络,称为“鉴别器”,从生成器一次接收一张图像,以及来自训练数据集的 **real 真实**图像。然后,鉴别器必须猜测:哪张图像是假的? + +生成器不断训练以创建对鉴别器更难以识别的图像,而鉴别器每次正确检测到伪造图像时,都会为生成器设置更高的门槛。随着网络之间的这种竞争(**adversarial 对抗性!**),生成的图像改善到了对人眼来说无法区分的地步! + +如果您想更深入地了解 GANs,可以参考[Analytics Vidhya 上的这篇优秀文章](https://www.analyticsvidhya.com/blog/2021/06/a-detailed-explanation-of-gan-with-implementation-using-tensorflow-and-keras/)或这个[PyTorch 教程](https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html)。不过,现在我们将深入看一下演示! + +## 步骤 1 - 创建生成器模型 + +要使用 GAN 生成新图像,只需要生成器模型。生成器可以使用许多不同的架构,但是对于这个演示,我们将使用一个预训练的 GAN 生成器模型,其架构如下: + +```python +from torch import nn + +class Generator(nn.Module): + # 有关nc,nz和ngf的解释,请参见下面的链接 + # https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html#inputs + def __init__(self, nc=4, nz=100, ngf=64): + super(Generator, self).__init__() + self.network = nn.Sequential( + nn.ConvTranspose2d(nz, ngf * 4, 3, 1, 0, bias=False), + nn.BatchNorm2d(ngf * 4), + nn.ReLU(True), + nn.ConvTranspose2d(ngf * 4, ngf * 2, 3, 2, 1, bias=False), + nn.BatchNorm2d(ngf * 2), + nn.ReLU(True), + nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 0, bias=False), + nn.BatchNorm2d(ngf), + nn.ReLU(True), + nn.ConvTranspose2d(ngf, nc, 4, 2, 1, bias=False), + nn.Tanh(), + ) + + def forward(self, input): + output = self.network(input) + return output +``` + +我们正在使用来自[此 repo 的 @teddykoker](https://github.com/teddykoker/cryptopunks-gan/blob/main/train.py#L90)的生成器模型,您还可以在那里看到原始的鉴别器模型结构。 + +在实例化模型之后,我们将加载来自 Hugging Face Hub 的权重,存储在[nateraw/cryptopunks-gan](https://huggingface.co/nateraw/cryptopunks-gan)中: + +```python +from huggingface_hub import hf_hub_download +import torch + +model = Generator() +weights_path = hf_hub_download('nateraw/cryptopunks-gan', 'generator.pth') +model.load_state_dict(torch.load(weights_path, map_location=torch.device('cpu'))) # 如果有可用的GPU,请使用'cuda' +``` + +## 步骤 2 - 定义“predict”函数 + +`predict` 函数是使 Gradio 工作的关键!我们通过 Gradio 界面选择的任何输入都将通过我们的 `predict` 函数传递,该函数应对输入进行操作并生成我们可以通过 Gradio 输出组件显示的输出。对于 GANs,常见的做法是将随机噪声传入我们的模型作为输入,因此我们将生成一张随机数的张量并将其传递给模型。然后,我们可以使用 `torchvision` 的 `save_image` 函数将模型的输出保存为 `png` 文件,并返回文件名: + +```python +from torchvision.utils import save_image + +def predict(seed): + num_punks = 4 + torch.manual_seed(seed) + z = torch.randn(num_punks, 100, 1, 1) + punks = model(z) + save_image(punks, "punks.png", normalize=True) + return 'punks.png' +``` + +我们给 `predict` 函数一个 `seed` 参数,这样我们就可以使用一个种子固定随机张量生成。然后,我们可以通过传入相同的种子再次查看生成的 punks。 + +_注意!_ 我们的模型需要一个 100x1x1 的输入张量进行单次推理,或者 (BatchSize)x100x1x1 来生成一批图像。在这个演示中,我们每次生成 4 个 punk。 + +## 第三步—创建一个 Gradio 接口 + +此时,您甚至可以运行您拥有的代码 `predict()`,并在您的文件系统中找到新生成的 punk 在 `./punks.png`。然而,为了制作一个真正的交互演示,我们将用 Gradio 构建一个简单的界面。我们的目标是: + +- 设置一个滑块输入,以便用户可以选择“seed”值 +- 使用图像组件作为输出,展示生成的 punk +- 使用我们的 `predict()` 函数来接受种子并生成图像 + +通过使用 `gr.Interface()`,我们可以使用一个函数调用来定义所有这些 : + +```python +import gradio as gr + +gr.Interface( + predict, + inputs=[ + gr.Slider(0, 1000, label='Seed', default=42), + ], + outputs="image", +).launch() +``` + +启动界面后,您应该会看到像这样的东西 : + + + +## 第四步—更多 punk! + +每次生成 4 个 punk 是一个好的开始,但是也许我们想控制每次想生成多少。通过简单地向我们传递给 `gr.Interface` 的 `inputs` 列表添加另一项即可向我们的 Gradio 界面添加更多输入 : + +```python +gr.Interface( + predict, + inputs=[ + gr.Slider(0, 1000, label='Seed', default=42), + gr.Slider(4, 64, label='Number of Punks', step=1, default=10), # 添加另一个滑块! + ], + outputs="image", +).launch() +``` + +新的输入将传递给我们的 `predict()` 函数,所以我们必须对该函数进行一些更改,以接受一个新的参数 : + +```python +def predict(seed, num_punks): + torch.manual_seed(seed) + z = torch.randn(num_punks, 100, 1, 1) + punks = model(z) + save_image(punks, "punks.png", normalize=True) + return 'punks.png' +``` + +当您重新启动界面时,您应该会看到一个第二个滑块,它可以让您控制 punk 的数量! + +## 第五步-完善它 + +您的 Gradio 应用已经准备好运行了,但是您可以添加一些额外的功能来使其真正准备好发光 ✨ + +我们可以添加一些用户可以轻松尝试的示例,通过将其添加到 `gr.Interface` 中实现 : + +```python +gr.Interface( + # ... + # 将所有内容保持不变,然后添加 + examples=[[123, 15], [42, 29], [456, 8], [1337, 35]], +).launch(cache_examples=True) # cache_examples是可选的 +``` + +`examples` 参数接受一个列表的列表,其中子列表中的每个项目的顺序与我们列出的 `inputs` 的顺序相同。所以在我们的例子中,`[seed, num_punks]`。试一试吧! + +您还可以尝试在 `gr.Interface` 中添加 `title`、`description` 和 `article`。每个参数都接受一个字符串,所以试试看发生了什么👀 `article` 也接受 HTML,如[前面的指南](./key_features/#descriptive-content)所述! + +当您完成所有操作后,您可能会得到类似于这样的结果 : + + + +供参考,这是我们的完整代码 : + +```python +import torch +from torch import nn +from huggingface_hub import hf_hub_download +from torchvision.utils import save_image +import gradio as gr + +class Generator(nn.Module): + # 关于nc、nz和ngf的解释,请参见下面的链接 + # https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html#inputs + def __init__(self, nc=4, nz=100, ngf=64): + super(Generator, self).__init__() + self.network = nn.Sequential( + nn.ConvTranspose2d(nz, ngf * 4, 3, 1, 0, bias=False), + nn.BatchNorm2d(ngf * 4), + nn.ReLU(True), + nn.ConvTranspose2d(ngf * 4, ngf * 2, 3, 2, 1, bias=False), + nn.BatchNorm2d(ngf * 2), + nn.ReLU(True), + nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 0, bias=False), + nn.BatchNorm2d(ngf), + nn.ReLU(True), + nn.ConvTranspose2d(ngf, nc, 4, 2, 1, bias=False), + nn.Tanh(), + ) + + def forward(self, input): + output = self.network(input) + return output + +model = Generator() +weights_path = hf_hub_download('nateraw/cryptopunks-gan', 'generator.pth') +model.load_state_dict(torch.load(weights_path, map_location=torch.device('cpu'))) # 如果您有可用的GPU,使用'cuda' + +def predict(seed, num_punks): + torch.manual_seed(seed) + z = torch.randn(num_punks, 100, 1, 1) + punks = model(z) + save_image(punks, "punks.png", normalize=True) + return 'punks.png' + +gr.Interface( + predict, + inputs=[ + gr.Slider(0, 1000, label='Seed', default=42), + gr.Slider(4, 64, label='Number of Punks', step=1, default=10), + ], + outputs="image", + examples=[[123, 15], [42, 29], [456, 8], [1337, 35]], +).launch(cache_examples=True) +``` + +--- + +恭喜!你已经成功构建了自己的基于 GAN 的 CryptoPunks 生成器,配备了一个时尚的 Gradio 界面,使任何人都能轻松使用。现在你可以在 Hub 上[寻找更多的 GANs](https://huggingface.co/models?other=gan)(或者自己训练)并继续制作更多令人赞叹的演示项目。🤗 diff --git a/guides/cn/07_other-tutorials/creating-a-chatbot.md b/guides/cn/07_other-tutorials/creating-a-chatbot.md new file mode 100644 index 0000000..3e85721 --- /dev/null +++ b/guides/cn/07_other-tutorials/creating-a-chatbot.md @@ -0,0 +1,82 @@ +# 如何创建一个聊天机器人 + +Tags: NLP, TEXT, CHAT +Related spaces: https://huggingface.co/spaces/gradio/chatbot_streaming, https://huggingface.co/spaces/gradio/chatinterface_artifacts, + +## 简介 + +聊天机器人在自然语言处理 (NLP) 研究和工业界被广泛使用。由于聊天机器人是直接由客户和最终用户使用的,因此验证聊天机器人在面对各种输入提示时的行为是否符合预期至关重要。 + +通过使用 `gradio`,您可以轻松构建聊天机器人模型的演示,并与用户共享,或使用直观的聊天机器人图形界面自己尝试。 + +本教程将展示如何使用 Gradio 制作几种不同类型的聊天机器人用户界面:首先是一个简单的文本显示界面,其次是一个用于流式文本响应的界面,最后一个是可以处理媒体文件的聊天机器人。我们创建的聊天机器人界面将如下所示: + +$ 演示 _ 聊天机器人 _ 流式 + +**先决条件**:我们将使用 `gradio.Blocks` 类来构建我们的聊天机器人演示。 +如果您对此还不熟悉,可以[先阅读 Blocks 指南](https://gradio.app/blocks-and-event-listeners)。同时,请确保您使用的是**最新版本**的 Gradio:`pip install --upgrade gradio`。 + +## 简单聊天机器人演示 + +让我们从重新创建上面的简单演示开始。正如您可能已经注意到的,我们的机器人只是随机对任何输入回复 " 你好吗?"、" 我爱你 " 或 " 我非常饿 "。这是使用 Gradio 创建此演示的代码: + +$ 代码 \_ 简单聊天机器人 + +这里有三个 Gradio 组件: + +- 一个 `Chatbot`,其值将整个对话的历史记录作为用户和机器人之间的响应对列表存储。 +- 一个文本框,用户可以在其中键入他们的消息,然后按下 Enter/ 提交以触发聊天机器人的响应 +- 一个 `ClearButton` 按钮,用于清除文本框和整个聊天机器人的历史记录 + +我们有一个名为 `respond()` 的函数,它接收聊天机器人的整个历史记录,附加一个随机消息,等待 1 秒,然后返回更新后的聊天历史记录。`respond()` 函数在返回时还清除了文本框。 + +当然,实际上,您会用自己更复杂的函数替换 `respond()`,该函数可能调用预训练模型或 API 来生成响应。 + +$ 演示 \_ 简单聊天机器人 + +## 为聊天机器人添加流式响应 + +我们可以通过几种方式来改进上述聊天机器人的用户体验。首先,我们可以流式传输响应,以便用户不必等待太长时间才能生成消息。其次,我们可以让用户的消息在聊天历史记录中立即出现,同时聊天机器人的响应正在生成。以下是实现这一点的代码: + +$code_chatbot_streaming + +当用户提交他们的消息时,您会注意到我们现在使用 `.then()` 与三个事件事件 _链_ 起来: + +1. 第一个方法 `user()` 用用户消息更新聊天机器人并清除输入字段。此方法还使输入字段处于非交互状态,以防聊天机器人正在响应时用户发送另一条消息。由于我们希望此操作立即发生,因此我们设置 `queue=False`,以跳过任何可能的队列。聊天机器人的历史记录附加了`(user_message, None)`,其中的 `None` 表示机器人未作出响应。 + +2. 第二个方法 `bot()` 使用机器人的响应更新聊天机器人的历史记录。我们不是创建新消息,而是将先前创建的 `None` 消息替换为机器人的响应。最后,我们逐个字符构造消息并 `yield` 正在构建的中间输出。Gradio 会自动将带有 `yield` 关键字的任何函数 [转换为流式输出接口](/key-features/#iterative-outputs)。 + +3. 第三个方法使输入字段再次可以交互,以便用户可以向机器人发送另一条消息。 + +当然,实际上,您会用自己更复杂的函数替换 `bot()`,该函数可能调用预训练模型或 API 来生成响应。 + +最后,我们通过运行 `demo.queue()` 启用排队,这对于流式中间输出是必需的。您可以通过滚动到本页面顶部的演示来尝试改进后的聊天机器人。 + +## 添加 Markdown、图片、音频或视频 + +`gr.Chatbot` 组件支持包含加粗、斜体和代码等一部分 Markdown 功能。例如,我们可以编写一个函数,以粗体回复用户的消息,类似于 **That's cool!**,如下所示: + +```py +def bot(history): + response = "**That's cool!**" + history[-1][1] = response + return history +``` + +此外,它还可以处理图片、音频和视频等媒体文件。要传递媒体文件,我们必须将文件作为两个字符串的元组传递,如`(filepath, alt_text)` 所示。`alt_text` 是可选的,因此您还可以只传入只有一个元素的元组`(filepath,)`,如下所示: + +```python +def add_file(history, file): + history = history + [((file.name,), None)] + return history +``` + +将所有这些放在一起,我们可以创建一个*多模态*聊天机器人,其中包含一个文本框供用户提交文本,以及一个文件上传按钮供提交图像 / 音频 / 视频文件。余下的代码看起来与之前的代码几乎相同: + +$code_chatbot_multimodal +$demo_chatbot_multimodal + +你完成了!这就是构建聊天机器人模型界面所需的所有代码。最后,我们将结束我们的指南,并提供一些在 Spaces 上运行的聊天机器人的链接,以让你了解其他可能性: + +- [gradio/chatbot_streaming](https://huggingface.co/spaces/gradio/chatbot_streaming):一个使用 `gr.Chatbot` 和 Blocks 构建的流式聊天机器人示例。 +- [gradio/chatinterface_artifacts](https://huggingface.co/spaces/gradio/chatinterface_artifacts):一个展示更丰富消息内容与 artifact 风格响应的 ChatInterface 示例。 diff --git a/guides/cn/07_other-tutorials/creating-a-new-component.md b/guides/cn/07_other-tutorials/creating-a-new-component.md new file mode 100644 index 0000000..2a71b6d --- /dev/null +++ b/guides/cn/07_other-tutorials/creating-a-new-component.md @@ -0,0 +1,390 @@ +# 如何创建一个新组件 + +## 简介 + +本指南旨在说明如何添加一个新组件,你可以在 Gradio 应用程序中使用该组件。该指南将通过代码片段逐步展示如何添加[ColorPicker](https://gradio.app/docs/colorpicker)组件。 + +## 先决条件 + +确保您已经按照[CONTRIBUTING.md](https://github.com/gradio-app/gradio/blob/main/CONTRIBUTING.md)指南设置了本地开发环境(包括客户端和服务器端)。 + +以下是在 Gradio 上创建新组件的步骤: + +1. [创建一个新的 Python 类并导入它](#1-create-a-new-python-class-and-import-it) +2. [创建一个新的 Svelte 组件](#2-create-a-new-svelte-component) +3. [创建一个新的演示](#3-create-a-new-demo) + +## 1. 创建一个新的 Python 类并导入它 + +首先要做的是在[components.py](https://github.com/gradio-app/gradio/blob/main/gradio/components.py)文件中创建一个新的类。这个 Python 类应该继承自一系列的基本组件,并且应该根据要添加的组件的类型(例如输入、输出或静态组件)将其放置在文件中的正确部分。 +一般来说,建议参考现有的组件(例如[TextBox](https://github.com/gradio-app/gradio/blob/main/gradio/components.py#L290)),将其代码复制为骨架,然后根据实际情况进行修改。 + +让我们来看一下添加到[components.py](https://github.com/gradio-app/gradio/blob/main/gradio/components.py)文件中的 ColorPicker 组件的类: + +```python +@document() +class ColorPicker(Changeable, Submittable, IOComponent): + """ + 创建一个颜色选择器,用户可以选择颜色作为字符串输入。 + 预处理:将选择的颜色值作为{str}传递给函数。 + 后处理:期望从函数中返回一个{str},并将颜色选择器的值设置为它。 + 示例格式:表示颜色的十六进制{str},例如红色的"#ff0000"。 + 演示:color_picker,color_generator + """ + + def __init__( + self, + value: str = None, + *, + label: Optional[str] = None, + show_label: bool = True, + interactive: Optional[bool] = None, + visible: bool = True, + elem_id: Optional[str] = None, + **kwargs, + ): + """ + Parameters: + """ + Parameters: + value: default text to provide in color picker. + label: component name in interface. + show_label: if True, will display label. + interactive: if True, will be rendered as an editable color picker; if False, editing will be disabled. If not provided, this is inferred based on whether the component is used as an input or output. + visible: If False, component will be hidden. + elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. + """ + self.value = self.postprocess(value) + self.cleared_value = "#000000" + self.test_input = value + IOComponent.__init__( + self, + label=label, + show_label=show_label, + interactive=interactive, + visible=visible, + elem_id=elem_id, + **kwargs, + ) + + def get_config(self): + return { + "value": self.value, + **IOComponent.get_config(self), + } + + @staticmethod + def update( + value: Optional[Any] = None, + label: Optional[str] = None, + show_label: Optional[bool] = None, + visible: Optional[bool] = None, + interactive: Optional[bool] = None, + ): + return { + "value": value, + "label": label, + "show_label": show_label, + "visible": visible, + "interactive": interactive, + "__type__": "update", + } + + # 输入功能 + def preprocess(self, x: str | None) -> Any: + """ + Any preprocessing needed to be performed on function input. + Parameters: + x (str): text + Returns: + (str): text + """ + if x is None: + return None + else: + return str(x) + + def preprocess_example(self, x: str | None) -> Any: + """ + 在传递给主函数之前,对示例进行任何预处理。 + """ + if x is None: + return None + else: + return str(x) + + # 输出功能 + def postprocess(self, y: str | None): + """ + Any postprocessing needed to be performed on function output. + Parameters: + y (str | None): text + Returns: + (str | None): text + """ + if y is None: + return None + else: + return str(y) + + def deserialize(self, x): + """ + 将从调用接口的序列化输出(例如base64表示)转换为输出的人类可读版本(图像的路径等) + """ + return x +``` + +一旦定义完,就需要在[\_\_init\_\_](https://github.com/gradio-app/gradio/blob/main/gradio/__init__.py)模块类中导入新类,以使其可见。 + +```python + +from gradio.components import ( + ... + ColorPicker, + ... +) + +``` + +### 1.1 为 Python 类编写单元测试 + +在开发新组件时,还应为其编写一套单元测试。这些测试应该放在[gradio/test/test_components.py](https://github.com/gradio-app/gradio/blob/main/test/test_components.py)文件中。同样,如上所述,参考其他组件的测试(例如[Textbox](https://github.com/gradio-app/gradio/blob/main/test/test_components.py))并添加尽可能多的单元测试,以测试新组件的所有不同方面和功能。例如,为 ColorPicker 组件添加了以下测试: + +```python +class TestColorPicker(unittest.TestCase): + def test_component_functions(self): + """ + Preprocess, postprocess, serialize, save_flagged, restore_flagged, tokenize, get_config + """ + color_picker_input = gr.ColorPicker() + self.assertEqual(color_picker_input.preprocess("#000000"), "#000000") + self.assertEqual(color_picker_input.preprocess_example("#000000"), "#000000") + self.assertEqual(color_picker_input.postprocess(None), None) + self.assertEqual(color_picker_input.postprocess("#FFFFFF"), "#FFFFFF") + self.assertEqual(color_picker_input.serialize("#000000", True), "#000000") + + color_picker_input.interpretation_replacement = "unknown" + + self.assertEqual( + color_picker_input.get_config(), + { + "value": None, + "show_label": True, + "label": None, + "style": {}, + "elem_id": None, + "visible": True, + "interactive": None, + "name": "colorpicker", + }, + ) + + def test_in_interface_as_input(self): + """ + 接口、处理、解释 + """ + iface = gr.Interface(lambda x: x, "colorpicker", "colorpicker") + self.assertEqual(iface.process(["#000000"]), ["#000000"]) + + def test_in_interface_as_output(self): + """ + 接口、处理 + + """ + iface = gr.Interface(lambda x: x, "colorpicker", gr.ColorPicker()) + self.assertEqual(iface.process(["#000000"]), ["#000000"]) + + def test_static(self): + """ + 后处理 + """ + component = gr.ColorPicker("#000000") + self.assertEqual(component.get_config().get("value"), "#000000") +``` + +## 2. 创建一个新的 Svelte 组件 + +让我们来看看创建新组件的前端并将其与其 Python 代码映射起来的步骤: + +- 在 [js 文件夹](https://github.com/gradio-app/gradio/tree/main/js/) 中创建一个新的 UI-side Svelte 组件,并确定要放置在什么地方。选项包括:创建新组件的包(如果与现有组件完全不同),或将新组件添加到现有包中,例如 [form 包](https://github.com/gradio-app/gradio/tree/main/js/form)。例如,ColorPicker 组件被包含在 form 包中,因为它与已存在的组件相似。 +- 在您将 Svelte 组件放置的包的 src 文件夹中创建一个带有适当名称的文件,注意:名称必须以大写字母开头。这是“核心”组件,是没有 Gradio 特定功能了解的通用组件。最初,将任何文本 /HTML 添加到此文件,以便组件呈现任何内容。ColorPicker 的 Svelte 应用程序代码如下所示: + +```typescript + + + + +``` + +- 通过执行 `export { default as FileName } from "./FileName.svelte"`,在您将 Svelte 组件放置的包的 index.ts 文件中导出此文件。例如,在 [index.ts](https://github.com/gradio-app/gradio/blob/main/js/form/src/index.ts) 文件中导出了 ColorPicker 文件,并通过 `export { default as ColorPicker } from "./ColorPicker.svelte";` 执行导出。 +- 创建 [js/app/src/components](https://github.com/gradio-app/gradio/tree/main/js/app/src/components) 中的 Gradio 特定组件。这是一个 Gradio 包装器,处理库的特定逻辑,将必要的数据传递给核心组件,并附加任何必要的事件监听器。复制另一个组件的文件夹,重新命名并编辑其中的代码,保持结构不变。 + +在这里,您将拥有三个文件,第一个文件用于 Svelte 应用程序,具体如下所示: + +```typescript + + + + + + + + + +``` + +第二个文件包含了前端的测试,例如 ColorPicker 组件的测试: + +```typescript +import { test, describe, assert, afterEach } from "vitest"; +import { cleanup, render } from "@self/tootils"; + +import ColorPicker from "./ColorPicker.svelte"; +import type { LoadingStatus } from "../StatusTracker/types"; + +const loading_status = { + eta: 0, + queue_position: 1, + status: "complete" as LoadingStatus["status"], + scroll_to_output: false, + visible: true, + fn_index: 0 +}; + +describe("ColorPicker", () => { + afterEach(() => cleanup()); + + test("renders provided value", () => { + const { getByDisplayValue } = render(ColorPicker, { + loading_status, + show_label: true, + interactive: true, + value: "#000000", + label: "ColorPicker" + }); + + const item: HTMLInputElement = getByDisplayValue("#000000"); + assert.equal(item.value, "#000000"); + }); + + test("changing the color should update the value", async () => { + const { component, getByDisplayValue } = render(ColorPicker, { + loading_status, + show_label: true, + interactive: true, + value: "#000000", + label: "ColorPicker" + }); + + const item: HTMLInputElement = getByDisplayValue("#000000"); + + assert.equal(item.value, "#000000"); + + await component.$set({ + value: "#FFFFFF" + }); + + assert.equal(component.value, "#FFFFFF"); + }); +}); +``` + +The third one is the index.ts file: + +```typescript +export { default as Component } from "./ColorPicker.svelte"; +export const modes = ["static", "dynamic"]; +``` + +- `directory.ts` 文件中添加组件的映射。复制并粘贴任何组件的映射行,并编辑其文本。键名必须是 Python 库中实际组件名称的小写版本。例如,对于 ColorPicker 组件,映射如下所示: + +```typescript +export const component_map = { +... +colorpicker: () => import("./ColorPicker"), +... +} +``` + +### 2.1 为 Svelte 组件编写单元测试 + +在开发新组件时,您还应该为其编写一套单元测试。测试应该放置在新组件的文件夹中,文件名为 MyAwesomeComponent.test.ts。同样,像上面那样参考其他组件的测试(例如[Textbox.test.ts](https://github.com/gradio-app/gradio/blob/main/js/app/src/components/Textbox/Textbox.test.ts)),并添加尽可能多的单元测试,以测试新组件的不同方面和功能。 + +### 3. 创建新的演示 + +最后一步是在[gradio/demo 文件夹](https://github.com/gradio-app/gradio/tree/main/demo)中创建一个使用新添加的组件的演示。同样,建议参考现有演示。在一个名为 run.py 的文件中编写演示的代码,添加必要的要求和显示应用程序界面的图像。最后添加一个显示其用法的 gif。 +您可以查看为 ColorPicker 创建的[demo](https://github.com/gradio-app/gradio/tree/main/demo/color_picker),其中以新组件选择的图标和颜色作为输入,并以选择的颜色着色的相同图标作为输出。 + +要测试应用程序: + +- 在终端上运行 `python path/demo/run.py`,它会在地址 [http://localhost:7860](http://localhost:7860) 启动后端; +- 在另一个终端上,运行 `pnpm dev` 以在 [http://localhost:9876](http://localhost:9876) 上启动具有热重新加载功能的前端。 + +## 结论 + +在本指南中,我们展示了将新组件添加到 Gradio 是多么简单,逐步介绍了如何添加 ColorPicker 组件。要了解更多细节,可以参考 PR:[#1695](https://github.com/gradio-app/gradio/pull/1695). diff --git a/guides/cn/07_other-tutorials/developing-faster-with-reload-mode.md b/guides/cn/07_other-tutorials/developing-faster-with-reload-mode.md new file mode 100644 index 0000000..61abbfc --- /dev/null +++ b/guides/cn/07_other-tutorials/developing-faster-with-reload-mode.md @@ -0,0 +1,150 @@ +# 通过自动重载实现更快的开发 + +**先决条件**:本指南要求您了解块的知识。请确保[先阅读块指南](https://gradio.app/blocks-and-event-listeners)。 + +本指南介绍了自动重新加载、在 Python IDE 中重新加载以及在 Jupyter Notebooks 中使用 gradio 的方法。 + +## 为什么要使用自动重载? + +当您构建 Gradio 演示时,特别是使用 Blocks 构建时,您可能会发现反复运行代码以测试更改很麻烦。 + +为了更快速、更便捷地编写代码,我们已经简化了在 **Python IDE**(如 VS Code、Sublime Text、PyCharm 等)中开发或从终端运行 Python 代码时“重新加载”Gradio 应用的方式。我们还开发了一个类似的“魔法命令”,使您可以更快速地重新运行单元格,如果您使用 Jupyter Notebooks(或类似的环境,如 Colab)的话。 + +这个简短的指南将涵盖这两种方法,所以无论您如何编写 Python 代码,您都将知道如何更快地构建 Gradio 应用程序。 + +## Python IDE 重载 🔥 + +如果您使用 Python IDE 构建 Gradio Blocks,那么代码文件(假设命名为 `run.py`)可能如下所示: + +```python +import gradio as gr + +with gr.Blocks() as demo: + gr.Markdown("# 来自Gradio的问候!") + inp = gr.Textbox(placeholder="您叫什么名字?") + out = gr.Textbox() + + inp.change(fn=lambda x: f"欢迎,{x}!", + inputs=inp, + outputs=out) + +if __name__ == "__main__": + demo.launch() +``` + +问题在于,每当您想要更改布局、事件或组件时,都必须通过编写 `python run.py` 来关闭和重新运行应用程序。 + +而不是这样做,您可以通过更改 1 个单词来以**重新加载模式**运行代码:将 `python` 更改为 `gradio`: + +在终端中运行 `gradio run.py`。就是这样! + +现在,您将看到类似于这样的内容: + +```bash +Launching in *reload mode* on: http://127.0.0.1:7860 (Press CTRL+C to quit) + +Watching... + +WARNING: The --reload flag should not be used in production on Windows. +``` + +这里最重要的一行是 `正在观察 ...`。这里发生的情况是 Gradio 将观察 `run.py` 文件所在的目录,如果文件发生更改,它将自动为您重新运行文件。因此,您只需专注于编写代码,Gradio 演示将自动刷新 🥳 + +⚠️ 警告:`gradio` 命令不会检测传递给 `launch()` 方法的参数,因为在重新加载模式下从未调用 `launch()` 方法。例如,设置 `launch()` 中的 `auth` 或 `show_error` 不会在应用程序中反映出来。 + +当您使用重新加载模式时,请记住一件重要的事情:Gradio 专门查找名为 `demo` 的 Gradio Blocks/Interface 演示。如果您将演示命名为其他名称,您需要在代码中的第二个参数中传入演示的 FastAPI 应用程序的名称。对于 Gradio 演示,可以使用 `.app` 属性访问 FastAPI 应用程序。因此,如果您的 `run.py` 文件如下所示: + +```python +import gradio as gr + +with gr.Blocks() as my_demo: + gr.Markdown("# 来自Gradio的问候!") + inp = gr.Textbox(placeholder="您叫什么名字?") + out = gr.Textbox() + + inp.change(fn=lambda x: f"欢迎,{x}!", + inputs=inp, + outputs=out) + +if __name__ == "__main__": + my_demo.launch() +``` + +那么您可以这样启动它:`gradio run.py my_demo.app`。 + +Gradio默认使用UTF-8编码格式。对于**重新加载模式**,如果你的脚本使用的是除UTF-8以外的编码(如GBK): + +1. 在Python脚本的编码声明处指定你想要的编码格式,如:`# -*- coding: gbk -*-` +2. 确保你的代码编辑器识别到该格式。 +3. 执行:`gradio run.py --encoding gbk` + +🔥 如果您的应用程序接受命令行参数,您也可以传递它们。下面是一个例子: + +```python +import gradio as gr +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("--name", type=str, default="User") +args, unknown = parser.parse_known_args() + +with gr.Blocks() as demo: + gr.Markdown(f"# 欢迎 {args.name}!") + inp = gr.Textbox() + out = gr.Textbox() + + inp.change(fn=lambda x: x, inputs=inp, outputs=out) + +if __name__ == "__main__": + demo.launch() +``` + +您可以像这样运行它:`gradio run.py --name Gretel` + +作为一个小提示,只要更改了 `run.py` 源代码或 Gradio 源代码,自动重新加载就会发生。这意味着如果您决定[为 Gradio 做贡献](https://github.com/gradio-app/gradio/blob/main/CONTRIBUTING.md),这将非常有用 ✅ + +## Jupyter Notebook 魔法命令🔮 + +如果您使用 Jupyter Notebooks(或 Colab Notebooks 等)进行开发,我们也为您提供了一个解决方案! + +我们开发了一个 **magic command 魔法命令**,可以为您创建和运行一个 Blocks 演示。要使用此功能,在笔记本顶部加载 gradio 扩展: + +`%load_ext gradio` + +然后,在您正在开发 Gradio 演示的单元格中,只需在顶部写入魔法命令**`%%blocks`**,然后像平常一样编写布局和组件: + +```py +%%blocks + +import gradio as gr + +gr.Markdown("# 来自Gradio的问候!") +inp = gr.Textbox(placeholder="您叫什么名字?") +out = gr.Textbox() + +inp.change(fn=lambda x: f"欢迎,{x}!", + inputs=inp, + outputs=out) +``` + +请注意: + +- 您不需要放置样板代码 `with gr.Blocks() as demo:` 和 `demo.launch()` — Gradio 会自动为您完成! + +- 每次重新运行单元格时,Gradio 都将在相同的端口上重新启动您的应用程序,并使用相同的底层网络服务器。这意味着您将比正常重新运行单元格更快地看到变化。 + +下面是在 Jupyter Notebook 中的示例: + +![](https://i.ibb.co/nrszFws/Blocks.gif) + +🪄这在 colab 笔记本中也适用![这是一个 colab 笔记本](https://colab.research.google.com/drive/1jUlX1w7JqckRHVE-nbDyMPyZ7fYD8488?authuser=1#scrollTo=zxHYjbCTTz_5),您可以在其中看到 Blocks 魔法效果。尝试进行一些更改并重新运行带有 Gradio 代码的单元格! + +Notebook Magic 现在是作者构建 Gradio 演示的首选方式。无论您如何编写 Python 代码,我们都希望这两种方法都能为您提供更好的 Gradio 开发体验。 + +--- + +## 下一步 + +既然您已经了解了如何使用 Gradio 快速开发,请开始构建自己的应用程序吧! + +如果你正在寻找灵感,请尝试浏览其他人用 Gradio 构建的演示,[浏览 Hugging Face Spaces](http://hf.space/) 🤗 diff --git a/guides/cn/07_other-tutorials/how-to-use-3D-model-component.md b/guides/cn/07_other-tutorials/how-to-use-3D-model-component.md new file mode 100644 index 0000000..d2990cc --- /dev/null +++ b/guides/cn/07_other-tutorials/how-to-use-3D-model-component.md @@ -0,0 +1,72 @@ +# 如何使用 3D 模型组件 + +相关空间:https://huggingface.co/spaces/dawood/Model3D, https://huggingface.co/spaces/radames/PIFu-Clothed-Human-Digitization, https://huggingface.co/spaces/radames/dpt-depth-estimation-3d-obj +标签:VISION, IMAGE + +## 介绍 + +机器学习中的 3D 模型越来越受欢迎,并且是一些最有趣的演示实验。使用 `gradio`,您可以轻松构建您的 3D 图像模型的演示,并与任何人分享。Gradio 3D 模型组件接受 3 种文件类型,包括:_.obj_,_.glb_ 和 _.gltf_。 + +本指南将向您展示如何使用几行代码构建您的 3D 图像模型的演示;像下面这个示例一样。点击、拖拽和缩放来玩转 3D 对象: + + + +### 先决条件 + +确保已经[安装](https://gradio.app/quickstart)了 `gradio` Python 包。 + +## 查看代码 + +让我们来看看如何创建上面的最简界面。在这种情况下,预测函数将只返回原始的 3D 模型网格,但您可以更改此函数以在您的机器学习模型上运行推理。我们将在下面看更复杂的示例。 + +```python +import gradio as gr + +def load_mesh(mesh_file_name): + return mesh_file_name + +demo = gr.Interface( + fn=load_mesh, + inputs=gr.Model3D(), + outputs=gr.Model3D(clear_color=[0.0, 0.0, 0.0, 0.0], label="3D Model"), + examples=[ + ["files/Bunny.obj"], + ["files/Duck.glb"], + ["files/Fox.gltf"], + ["files/face.obj"], + ], + cache_examples=True, +) + +demo.launch() +``` + +让我们来解析上面的代码: + +`load_mesh`:这是我们的“预测”函数,为简单起见,该函数将接收 3D 模型网格并返回它。 + +创建界面: + +- `fn`:当用户点击提交时使用的预测函数。在我们的例子中,它是 `load_mesh` 函数。 +- `inputs`:创建一个 model3D 输入组件。输入是一个上传的文件,作为{str}文件路径。 +- `outputs`:创建一个 model3D 输出组件。输出组件也期望一个文件作为{str}文件路径。 + - `clear_color`:这是 3D 模型画布的背景颜色。期望 RGBa 值。 + - `label`:出现在组件左上角的标签。 +- `examples`:3D 模型文件的列表。3D 模型组件可以接受*.obj*,*.glb*和*.gltf*文件类型。 +- `cache_examples`:保存示例的预测输出,以节省推理时间。 + +## 探索更复杂的 Model3D 演示 + +下面是一个使用 DPT 模型预测图像深度,然后使用 3D 点云创建 3D 对象的演示。查看[code.py](https://huggingface.co/spaces/radames/dpt-depth-estimation-3d-obj/blob/main/app.py)文件,了解代码和模型预测函数。 + + +下面是一个使用 PIFu 模型将穿着衣物的人的图像转换为 3D 数字化模型的演示。查看[spaces.py](https://huggingface.co/spaces/radames/PIFu-Clothed-Human-Digitization/blob/main/PIFu/spaces.py)文件,了解代码和模型预测函数。 + + + +--- + +搞定!这就是构建 Model3D 模型界面所需的所有代码。以下是一些您可能会发现有用的参考资料: + +- Gradio 的[“入门指南”](https://gradio.app/getting_started/) +- 第一个[3D 模型演示](https://huggingface.co/spaces/dawood/Model3D)和[完整代码](https://huggingface.co/spaces/dawood/Model3D/tree/main)(在 Hugging Face Spaces 上) diff --git a/guides/cn/07_other-tutorials/named-entity-recognition.md b/guides/cn/07_other-tutorials/named-entity-recognition.md new file mode 100644 index 0000000..0c1535f --- /dev/null +++ b/guides/cn/07_other-tutorials/named-entity-recognition.md @@ -0,0 +1,80 @@ +# 命名实体识别 (Named-Entity Recognition) + +相关空间:https://huggingface.co/spaces/rajistics/biobert_ner_demo,https://huggingface.co/spaces/abidlabs/ner,https://huggingface.co/spaces/rajistics/Financial_Analyst_AI +标签:NER,TEXT,HIGHLIGHT + +## 简介 + +命名实体识别(NER)又称为标记分类或文本标记,它的任务是对一个句子进行分类,将每个单词(或 "token")归为不同的类别,比如人名、地名或词性等。 + +例如,给定以下句子: + +> 芝加哥有巴基斯坦餐厅吗? + +命名实体识别算法可以识别出: + +- "Chicago" as a **location** +- "Pakistani" as an **ethnicity** + +等等。 + +使用 `gradio`(特别是 `HighlightedText` 组件),您可以轻松构建一个 NER 模型的 Web 演示并与团队分享。 + +这是您将能够构建的一个演示的示例: + +$demo_ner_pipeline + +本教程将展示如何使用预训练的 NER 模型并使用 Gradio 界面部署该模型。我们将展示两种不同的使用 `HighlightedText` 组件的方法--根据您的 NER 模型,可以选择其中任何一种更容易学习的方式! + +### 环境要求 + +确保您已经[安装](/getting_started)了 `gradio` Python 包。您还需要一个预训练的命名实体识别模型。在本教程中,我们将使用 `transformers` 库中的一个模型。 + +### 方法一:实体字典列表 + +许多命名实体识别模型输出的是一个字典列表。每个字典包含一个*实体*,一个 " 起始 " 索引和一个 " 结束 " 索引。这就是 `transformers` 库中的 NER 模型的操作方式。 + +```py +from transformers import pipeline +ner_pipeline = pipeline("ner") +ner_pipeline("芝加哥有巴基斯坦餐厅吗?") +``` + +输出结果: + +```bash +[{'entity': 'I-LOC', + 'score': 0.9988978, + 'index': 2, + 'word': 'Chicago', + 'start': 5, + 'end': 12}, + {'entity': 'I-MISC', + 'score': 0.9958592, + 'index': 5, + 'word': 'Pakistani', + 'start': 22, + 'end': 31}] +``` + +如果您有这样的模型,将其连接到 Gradio 的 `HighlightedText` 组件非常简单。您只需要将这个**实体列表**与**原始文本**以字典的形式传递给模型,其中键分别为 `"entities"` 和 `"text"`。 + +下面是一个完整的示例: + +$code_ner_pipeline +$demo_ner_pipeline + +### 方法二:元组列表 + +将数据传递给 `HighlightedText` 组件的另一种方法是使用元组列表。每个元组的第一个元素应该是被归类为特定实体的单词或词组。第二个元素应该是实体标签(如果不需要标签,则为 `None`)。`HighlightedText` 组件会自动组合单词和标签来显示实体。 + +在某些情况下,这比第一种方法更简单。下面是一个使用 Spacy 的词性标注器演示此方法的示例: + +$code_text_analysis +$demo_text_analysis + +--- + +到此为止!您已经了解了为您的 NER 模型构建基于 Web 的图形用户界面所需的全部内容。 + +有趣的提示:只需在 `launch()` 中设置 `share=True`,即可立即与其他人分享您的 NER 演示。 diff --git a/guides/cn/07_other-tutorials/real-time-speech-recognition.md b/guides/cn/07_other-tutorials/real-time-speech-recognition.md new file mode 100644 index 0000000..6a76e69 --- /dev/null +++ b/guides/cn/07_other-tutorials/real-time-speech-recognition.md @@ -0,0 +1,227 @@ +# 实时语音识别 + +Related spaces: https://huggingface.co/spaces/abidlabs/streaming-asr-paused, https://huggingface.co/spaces/abidlabs/full-context-asr +Tags: ASR, SPEECH, STREAMING + +## 介绍 + +自动语音识别(ASR)是机器学习中非常重要且蓬勃发展的领域,它将口语转换为文本。ASR 算法几乎在每部智能手机上都有运行,并越来越多地嵌入到专业工作流程中,例如护士和医生的数字助手。由于 ASR 算法是直接面向客户和最终用户设计的,因此在面对各种语音模式(不同的口音、音调和背景音频条件)时,验证它们的行为是否符合预期非常重要。 + +使用 `gradio`,您可以轻松构建一个 ASR 模型的演示,并与测试团队共享,或通过设备上的麦克风进行自行测试。 + +本教程将展示如何使用预训练的语音识别模型并在 Gradio 界面上部署。我们将从一个 **full-context 全文**模型开始,其中用户在进行预测之前要说完整段音频。然后,我们将调整演示以使其变为 **streaming 流式**,这意味着音频模型将在您说话时将语音转换为文本。我们创建的流式演示将如下所示(在下方尝试或[在新标签页中打开](https://huggingface.co/spaces/abidlabs/streaming-asr-paused)): + + +实时 ASR 本质上是*有状态的*,即模型的预测结果取决于用户先前说的单词。因此,在本教程中,我们还将介绍如何在 Gradio 演示中使用 **state**。 + +### 先决条件 + +确保您已经[安装](/getting_started)了 `gradio` Python 包。您还需要一个预训练的语音识别模型。在本教程中,我们将从两个 ASR 库构建演示: + +- Transformers(为此,`pip install transformers` 和 `pip install torch`)\* DeepSpeech(`pip install deepspeech==0.8.2`) + +确保您至少安装了其中之一,以便您可以跟随本教程操作。如果您尚未安装 `ffmpeg`,请在[系统上下载并安装](https://www.ffmpeg.org/download.html),以便从麦克风处理文件。 + +下面是构建实时语音识别(ASR)应用程序的步骤: + +1. [设置 Transformers ASR 模型](#1-set-up-the-transformers-asr-model) +2. [使用 Transformers 创建一个全文 ASR 演示] + (#2-create-a-full-context-asr-demo-with-transformers) +3. [使用 Transformers 创建一个流式 ASR 演示](#3-create-a-streaming-asr-demo-with-transformers) +4. [使用 DeepSpeech 创建一个流式 ASR 演示](#4-create-a-streaming-asr-demo-with-deepspeech) + +## 1. 设置 Transformers ASR 模型 + +首先,您需要拥有一个 ASR 模型,您可以自己训练,或者需要下载一个预训练模型。在本教程中,我们将使用 Hugging Face 模型的预训练 ASR 模型 `Wav2Vec2`。 + +以下是从 Hugging Face 的 `transformers` 加载 `Wav2Vec2` 的代码: + +```python +from transformers import pipeline +p = pipeline("automatic-speech-recognition") +``` + +就是这样!默认情况下,自动语音识别模型管道会加载 Facebook 的 `facebook/wav2vec2-base-960h` 模型。 + +## 2. 使用 Transformers 创建一个全文 ASR 演示 + +我们将首先创建一个*全文*ASR 演示,其中用户在使用 ASR 模型进行预测之前说完整段音频。使用 Gradio 非常简单,我们只需在上面的 `pipeline` 对象周围创建一个函数。 + +我们将使用 `gradio` 内置的 `Audio` 组件,配置从用户的麦克风接收输入并返回录制音频的文件路径。输出组件将是一个简单的 `Textbox`。 + +```python +import gradio as gr + +def transcribe(audio): + text = p(audio)["text"] + return text + +gr.Interface( + fn=transcribe, + inputs=gr.Audio(sources=["microphone"], type="filepath"), + outputs="text").launch() +``` + +那么这里发生了什么?`transcribe` 函数接受一个参数 `audio`,它是用户录制的音频文件的文件路径。`pipeline` 对象期望一个文件路径,并将其转换为文本,然后返回到前端并在文本框中显示。 + +让我们看看它的效果吧!(录制一段短音频并点击提交,或[在新标签页打开](https://huggingface.co/spaces/abidlabs/full-context-asr)): + + +## 3. 使用 Transformers 创建一个流式 ASR 演示 +太棒了!我们已经构建了一个对短音频剪辑效果良好的 ASR 模型。但是,如果您正在记录较长的音频剪辑,则可能需要一个*流式*界面,即在用户说话时逐句转录音频,而不仅仅在最后一次全部转录。 + +好消息是,我们可以很容易地调整刚刚创建的演示,使其成为流式的,使用相同的 `Wav2Vec2` 模型。 + +最大的变化是我们现在必须引入一个 `state` 参数,它保存到目前为止*转录的音频*。这样,我们只需处理最新的音频块,并将其简单地追加到先前转录的音频中。 + +在向 Gradio 演示添加状态时,您需要完成 3 件事: + +- 在函数中添加 `state` 参数* 在函数末尾返回更新后的 `state`* 在 `Interface` 的 `inputs` 和 `outputs` 中添加 `"state"` 组件 + +以下是代码示例: + +```python +def transcribe(audio, state=""): + text = p(audio)["text"] + state += text + " " + return state, state + +# Set the starting state to an empty string +gr.Interface( + fn=transcribe, + inputs=[ + gr.Audio(sources=["microphone"], type="filepath", streaming=True), + "state" + ], + outputs=[ + "textbox", + "state" + ], + live=True).launch() +``` + +请注意,我们还进行了另一个更改,即我们设置了 `live=True`。这使得 Gradio 接口保持持续运行,因此它可以自动转录音频,而无需用户反复点击提交按钮。 + +让我们看看它的效果(在下方尝试或[在新标签页中打开](https://huggingface.co/spaces/abidlabs/streaming-asr))! + + + +你可能注意到的一件事是,由于音频块非常小,所以转录质量下降了,它们缺乏正确转录所需的上下文。此问题的“hacky”解决方法是简单地增加 `transcribe()` 函数的运行时间,以便处理更长的音频块。我们可以通过在函数中添加 `time.sleep()` 来实现这一点,如下所示(接下来我们将看到一个正确的解决方法) + +```python +from transformers import pipeline +import gradio as gr +import time + +p = pipeline("automatic-speech-recognition") + +def transcribe(audio, state=""): + time.sleep(2) + text = p(audio)["text"] + state += text + " " + return state, state + +gr.Interface( + fn=transcribe, + inputs=[ + gr.Audio(sources=["microphone"], type="filepath", streaming=True), + "state" + ], + outputs=[ + "textbox", + "state" + ], + live=True).launch() +``` + +尝试下面的演示,查看差异(或[在新标签页中打开](https://huggingface.co/spaces/abidlabs/streaming-asr-paused))! + + + +## 4. 使用 DeepSpeech 创建流式 ASR 演示 + +您不仅限于使用 `transformers` 库中的 ASR 模型 - 您可以使用自己的模型或其他库中的模型。`DeepSpeech` 库包含专门用于处理流式音频数据的模型。这些模型在处理流式数据时表现非常好,因为它们能够考虑到先前的音频块在进行预测时产生的影响。 + +深入研究 DeepSpeech 库超出了本指南的范围(可以在[此处查看其优秀的文档](https://deepspeech.readthedocs.io/en/r0.9/)),但是您可以像使用 Transformer ASR 模型一样,使用 DeepSpeech ASR 模型使用类似的方法使用 Gradio。 + +下面是一个完整的示例(在 Linux 上): + +首先通过终端安装 DeepSpeech 库并下载预训练模型: + +```bash +wget https://github.com/mozilla/DeepSpeech/releases/download/v0.8.2/deepspeech-0.8.2-models.pbmm +wget https://github.com/mozilla/DeepSpeech/releases/download/v0.8.2/deepspeech-0.8.2-models.scorer +apt install libasound2-dev portaudio19-dev libportaudio2 libportaudiocpp0 ffmpeg +pip install deepspeech==0.8.2 +``` + +然后,创建与之前相似的 `transcribe()` 函数: + +```python +from deepspeech import Model +import numpy as np + +model_file_path = "deepspeech-0.8.2-models.pbmm" +lm_file_path = "deepspeech-0.8.2-models.scorer" +beam_width = 100 +lm_alpha = 0.93 +lm_beta = 1.18 + +model = Model(model_file_path) +model.enableExternalScorer(lm_file_path) +model.setScorerAlphaBeta(lm_alpha, lm_beta) +model.setBeamWidth(beam_width) + + +def reformat_freq(sr, y): + if sr not in ( + 48000, + 16000, + ): # Deepspeech only supports 16k, (we convert 48k -> 16k) + raise ValueError("Unsupported rate", sr) + if sr == 48000: + y = ( + ((y / max(np.max(y), 1)) * 32767) + .reshape((-1, 3)) + .mean(axis=1) + .astype("int16") + ) + sr = 16000 + return sr, y + + +def transcribe(speech, stream): + _, y = reformat_freq(*speech) + if stream is None: + stream = model.createStream() + stream.feedAudioContent(y) + text = stream.intermediateDecode() + return text, stream + +``` + +然后,如前所述创建一个 Gradio 接口(唯一的区别是返回类型应该是 `numpy` 而不是 `filepath` 以与 DeepSpeech 模型兼容) + +```python +import gradio as gr + +gr.Interface( + fn=transcribe, + inputs=[ + gr.Audio(sources=["microphone"], type="numpy"), + "state" + ], + outputs= [ + "text", + "state" + ], + live=True).launch() +``` + +运行所有这些应该允许您使用一个漂亮的 GUI 部署实时 ASR 模型。尝试一下,看它在您那里运行得有多好。 + +--- + +你已经完成了!这就是构建用于 ASR 模型的基于 Web 的 GUI 所需的所有代码。 + +有趣的提示:您只需在 `launch()` 中设置 `share=True`,即可即时与他人共享 ASR 模型。 diff --git a/guides/cn/07_other-tutorials/running-background-tasks.md b/guides/cn/07_other-tutorials/running-background-tasks.md new file mode 100644 index 0000000..5035be4 --- /dev/null +++ b/guides/cn/07_other-tutorials/running-background-tasks.md @@ -0,0 +1,156 @@ +# 运行后台任务 + +Related spaces: https://huggingface.co/spaces/freddyaboulton/gradio-google-forms +Tags: TASKS, SCHEDULED, TABULAR, DATA + +## 简介 + +本指南介绍了如何从 gradio 应用程序中运行后台任务。 +后台任务是在您的应用程序的请求-响应生命周期之外执行的操作,可以是一次性的或定期的。 +后台任务的示例包括定期将数据与外部数据库同步或通过电子邮件发送模型预测报告。 + +## 概述 + +我们将创建一个简单的“Google Forms”风格的应用程序,用于收集 gradio 库的用户反馈。 +我们将使用一个本地 sqlite 数据库来存储数据,但我们将定期将数据库的状态与[HuggingFace Dataset](https://huggingface.co/datasets)同步,以便始终备份我们的用户评论。 +同步将在每 60 秒运行的后台任务中进行。 + +在演示结束时,您将拥有一个完全可工作的应用程序,类似于以下应用程序 : + + + +## 第一步 - 编写数据库逻辑 💾 + +我们的应用程序将存储评论者的姓名,他们对 gradio 给出的评分(1 到 5 的范围),以及他们想要分享的关于该库的任何评论。让我们编写一些代码,创建一个数据库表来存储这些数据。我们还将编写一些函数,以将评论插入该表中并获取最新的 10 条评论。 + +我们将使用 `sqlite3` 库来连接我们的 sqlite 数据库,但 gradio 可以与任何库一起使用。 + +代码如下 : + +```python +DB_FILE = "./reviews.db" +db = sqlite3.connect(DB_FILE) + +# Create table if it doesn't already exist +try: + db.execute("SELECT * FROM reviews").fetchall() + db.close() +except sqlite3.OperationalError: + db.execute( + ''' + CREATE TABLE reviews (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + name TEXT, review INTEGER, comments TEXT) + ''') + db.commit() + db.close() + +def get_latest_reviews(db: sqlite3.Connection): + reviews = db.execute("SELECT * FROM reviews ORDER BY id DESC limit 10").fetchall() + total_reviews = db.execute("Select COUNT(id) from reviews").fetchone()[0] + reviews = pd.DataFrame(reviews, columns=["id", "date_created", "name", "review", "comments"]) + return reviews, total_reviews + + +def add_review(name: str, review: int, comments: str): + db = sqlite3.connect(DB_FILE) + cursor = db.cursor() + cursor.execute("INSERT INTO reviews(name, review, comments) VALUES(?,?,?)", [name, review, comments]) + db.commit() + reviews, total_reviews = get_latest_reviews(db) + db.close() + return reviews, total_reviews +``` + +让我们还写一个函数,在 gradio 应用程序加载时加载最新的评论 : + +```python +def load_data(): + db = sqlite3.connect(DB_FILE) + reviews, total_reviews = get_latest_reviews(db) + db.close() + return reviews, total_reviews +``` + +## 第二步 - 创建 gradio 应用 ⚡ + +现在我们已经定义了数据库逻辑,我们可以使用 gradio 创建一个动态的网页来询问用户的反馈意见! + +使用以下代码段 : + +```python +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + name = gr.Textbox(label="Name", placeholder="What is your name?") + review = gr.Radio(label="How satisfied are you with using gradio?", choices=[1, 2, 3, 4, 5]) + comments = gr.Textbox(label="Comments", lines=10, placeholder="Do you have any feedback on gradio?") + submit = gr.Button(value="Submit Feedback") + with gr.Column(): + data = gr.Dataframe(label="Most recently created 10 rows") + count = gr.Number(label="Total number of reviews") + submit.click(add_review, [name, review, comments], [data, count]) + demo.load(load_data, None, [data, count]) +``` + +## 第三步 - 与 HuggingFace 数据集同步 🤗 + +在第 2 步后我们可以调用 `demo.launch()` 来运行一个完整功能的应用程序。然而,我们的数据将存储在本地机器上。如果 sqlite 文件意外删除,我们将丢失所有评论!让我们将我们的数据备份到 HuggingFace hub 的数据集中。 + +在继续之前,请在[此处](https://huggingface.co/datasets)创建一个数据集。 + +现在,在我们脚本的**顶部**,我们将使用[huggingface hub 客户端库](https://huggingface.co/docs/huggingface_hub/index)连接到我们的数据集并获取最新的备份。 + +```python +TOKEN = os.environ.get('HUB_TOKEN') +repo = huggingface_hub.Repository( + local_dir="data", + repo_type="dataset", + clone_from="", + use_auth_token=TOKEN +) +repo.git_pull() + +shutil.copyfile("./data/reviews.db", DB_FILE) +``` + +请注意,您需要从 HuggingFace 的“设置”选项卡中获取访问令牌,以上代码才能正常工作。在脚本中,通过环境变量安全访问令牌。 + +![access_token](/assets/guides/access_token.png) + +现在,我们将创建一个后台任务,每 60 秒将我们的本地数据库与数据集中的数据同步一次。 +我们将使用[AdvancedPythonScheduler](https://apscheduler.readthedocs.io/en/3.x/)来处理调度。 +然而,这并不是唯一可用的任务调度库。请随意使用您熟悉的任何库。 + +备份数据的函数如下 : + +```python +from apscheduler.schedulers.background import BackgroundScheduler + +def backup_db(): + shutil.copyfile(DB_FILE, "./data/reviews.db") + db = sqlite3.connect(DB_FILE) + reviews = db.execute("SELECT * FROM reviews").fetchall() + pd.DataFrame(reviews).to_csv("./data/reviews.csv", index=False) + print("updating db") + repo.push_to_hub(blocking=False, commit_message=f"Updating data at {datetime.datetime.now()}") + + +scheduler = BackgroundScheduler() +scheduler.add_job(func=backup_db, trigger="interval", seconds=60) +scheduler.start() +``` + +## 第四步(附加)- 部署到 HuggingFace Spaces + +您可以使用 HuggingFace [Spaces](https://huggingface.co/spaces) 平台免费部署这个应用程序 ✨ + +如果您之前没有使用过 Spaces,请查看[此处](/using_hugging_face_integrations)的先前指南。 +您将需要将 `HUB_TOKEN` 环境变量作为指南中的一个秘密使用。 + +## 结论 + +恭喜!您知道如何在您的 gradio 应用程序中按计划运行后台任务⏲️。 + +在 Spaces 上运行的应用程序可在[此处](https://huggingface.co/spaces/freddyaboulton/gradio-google-forms)查看。 +完整的代码在[此处](https://huggingface.co/spaces/freddyaboulton/gradio-google-forms/blob/main/app.py)。 diff --git a/guides/cn/07_other-tutorials/running-gradio-on-your-web-server-with-nginx.md b/guides/cn/07_other-tutorials/running-gradio-on-your-web-server-with-nginx.md new file mode 100644 index 0000000..d8c509a --- /dev/null +++ b/guides/cn/07_other-tutorials/running-gradio-on-your-web-server-with-nginx.md @@ -0,0 +1,78 @@ +# 在 Web 服务器上使用 Nginx 运行 Gradio 应用 + +标签:部署,Web 服务器,Nginx + +## 介绍 + +Gradio 是一个 Python 库,允许您快速创建可定制的 Web 应用程序,用于机器学习模型和数据处理流水线。Gradio 应用可以免费部署在[Hugging Face Spaces](https://hf.space)上。 + +然而,在某些情况下,您可能希望在自己的 Web 服务器上部署 Gradio 应用。您可能已经在使用[Nginx](https://www.nginx.com/)作为高性能的 Web 服务器来提供您的网站(例如 `https://www.example.com`),并且您希望将 Gradio 附加到网站的特定子路径上(例如 `https://www.example.com/gradio-demo`)。 + +在本指南中,我们将指导您在自己的 Web 服务器上的 Nginx 后面运行 Gradio 应用的过程,以实现此目的。 + +**先决条件** + +1. 安装了 [Nginx 的 Linux Web 服务器](https://www.nginx.com/blog/setting-up-nginx/) 和 [Gradio](/quickstart) 库 + +2. 在 Web 服务器上将 Gradio 应用保存为 Python 文件 + +## 编辑 Nginx 配置文件 + +1. 首先编辑 Web 服务器上的 Nginx 配置文件。默认情况下,文件位于:`/etc/nginx/nginx.conf` + +在 `http` 块中,添加以下行以从单独的文件包含服务器块配置: + +```bash +include /etc/nginx/sites-enabled/*; +``` + +2. 在 `/etc/nginx/sites-available` 目录中创建一个新文件(如果目录不存在则创建),文件名表示您的应用,例如:`sudo nano /etc/nginx/sites-available/my_gradio_app` + +3. 将以下内容粘贴到文件编辑器中: + +```bash +server { + listen 80; + server_name example.com www.example.com; # 将此项更改为您的域名 + + location /gradio-demo/ { # 如果要在不同路径上提供Gradio应用,请更改此项 + proxy_pass http://127.0.0.1:7860/; # 如果您的Gradio应用将在不同端口上运行,请更改此项 + proxy_redirect off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + } +} +``` + +## 在 Web 服务器上运行 Gradio 应用 + +1. 在启动 Gradio 应用之前,您需要将 `root_path` 设置为与 Nginx 配置中指定的子路径相同。这对于 Gradio 在除域的根路径之外的任何子路径上运行是必要的。 + +以下是一个具有自定义 `root_path` 的简单示例 Gradio 应用: + +```python +import gradio as gr +import time + +def test(x): + time.sleep(4) + return x + +gr.Interface(test, "textbox", "textbox").queue().launch(root_path="/gradio-demo") +``` + +2. 通过键入 `tmux` 并按回车键(可选)启动 `tmux` 会话 + +推荐在 `tmux` 会话中运行 Gradio 应用,以便可以轻松地在后台运行它 + +3. 然后,启动您的 Gradio 应用。只需输入 `python`,后跟您的 Gradio Python 文件的名称。默认情况下,应用将在 `localhost:7860` 上运行,但如果它在其他端口上启动,您需要更新上面的 Nginx 配置文件。 + +## 重新启动 Nginx + +1. 如果您在 tmux 会话中,请通过键入 CTRL + B(或 CMD + B),然后按下 "D" 键来退出。 + +2. 最后,通过运行 `sudo systemctl restart nginx` 重新启动 nginx。 + +就是这样!如果您在浏览器中访问 `https://example.com/gradio-demo`,您应该能够看到您的 Gradio 应用在那里运行。 diff --git a/guides/cn/07_other-tutorials/theming-guide.md b/guides/cn/07_other-tutorials/theming-guide.md new file mode 100644 index 0000000..f411f42 --- /dev/null +++ b/guides/cn/07_other-tutorials/theming-guide.md @@ -0,0 +1,408 @@ +# 主题 Theming + +Tags: THEMES + +## 介绍 + +Gradio 具有内置的主题引擎,可让您自定义应用的外观和感觉。您可以选择各种主题,或者创建自己的主题。要这样做,请将 `theme=` kwarg 传递给 `Blocks` 或 `Interface` 构造函数。例如: + +```python +with gr.Blocks(theme=gr.themes.Soft()) as demo: + ... +``` + +
+ +
+ +Gradio 带有一组预构建的主题,您可以从 `gr.themes.*` 中加载这些主题。这些主题包括: + +- `gr.themes.Base()` +- `gr.themes.Default()` +- `gr.themes.Glass()` +- `gr.themes.Monochrome()` +- `gr.themes.Soft()` + +这些主题为数百个 CSS 变量设置了值。您可以使用预构建的主题作为自定义主题的起点,也可以从头开始创建自己的主题。让我们看看每种方法。 + +## 使用主题构建器 + +使用主题构建器构建主题最简单。要在本地启动主题构建器,请运行以下代码: + +```python +import gradio as gr + +gr.themes.builder() +``` + +$demo_theme_builder + +您可以使用上面的 Spaces 上运行的 Theme Builder,但通过 `gr.themes.builder()` 在本地启动时运行速度更快。 + +在 Theme Builder 中编辑值时,应用程序将实时预览更新。您可以下载生成的主题代码,以便在任何 Gradio 应用程序中使用它。 + +在本指南的其余部分,我们将介绍如何以编程方式构建主题。 + +## 通过构造函数扩展主题 + +尽管每个主题都有数百个 CSS 变量,但大多数这些变量的值都是从 8 个核心变量中获取的,可以通过每个预构建主题的构造函数设置这些变量。通过修改这 8 个参数的值,您可以快速更改应用程序的外观和感觉。 + +### 核心颜色 + +前 3 个构造函数参数设置主题的颜色,并且是 `gradio.themes.Color` 对象。在内部,这些 Color 对象包含单个色调的调色板的亮度值,范围从 50,100,200...,800,900,950。其他 CSS 变量是从这 3 种颜色派生的。 + +3 个颜色构造函数参数是: + +- `primary_hue`:这是主题中的主色。在默认主题中,此值设置为 `gradio.themes.colors.orange`。 +- `secondary_hue`:这是主题中用于辅助元素的颜色。在默认主题中,此值设置为 `gradio.themes.colors.blue`。 +- `neutral_hue`:这是主题中用于文本和其他中性元素的颜色。在默认主题中,此值设置为 `gradio.themes.colors.gray`。 + +您可以使用字符串快捷方式修改这些值,例如 + +```python +with gr.Blocks(theme=gr.themes.Default(primary_hue="red", secondary_hue="pink")) as demo: + ... +``` + +或者直接使用 `Color` 对象,如下所示: + +```python +with gr.Blocks(theme=gr.themes.Default(primary_hue=gr.themes.colors.red, secondary_hue=gr.themes.colors.pink)) as demo: + ... +``` + +
+ +
+ +预定义的颜色包括: + +- `slate` +- `gray` +- `zinc` +- `neutral` +- `stone` +- `red` +- `orange` +- `amber` +- `yellow` +- `lime` +- `green` +- `emerald` +- `teal` +- `cyan` +- `sky` +- `blue` +- `indigo` +- `violet` +- `purple` +- `fuchsia` +- `pink` +- `rose` + +您还可以创建自己的自定义 `Color` 对象并传递它们。 + +### 核心大小 (Core Sizing) + +接下来的 3 个构造函数参数设置主题的大小,并且是 `gradio.themes.Size` 对象。在内部,这些 Size 对象包含从 `xxs` 到 `xxl` 的像素大小值。其他 CSS 变量是从这 3 个大小派生的。 + +- `spacing_size`:此设置了元素内部的填充和元素之间的间距。在默认主题中,此值设置为 `gradio.themes.sizes.spacing_md`。 +- `radius_size`:此设置了元素的圆角弧度。在默认主题中,此值设置为 `gradio.themes.sizes.radius_md`。 +- `text_size`:此设置了文本的字体大小。在默认主题中,此值设置为 `gradio.themes.sizes.text_md`。 + +您可以使用字符串快捷方式修改这些值,例如 + +```python +with gr.Blocks(theme=gr.themes.Default(spacing_size="sm", radius_size="none")) as demo: + ... +``` + +或者直接使用 `Size` 对象,如下所示: + +```python +with gr.Blocks(theme=gr.themes.Default(spacing_size=gr.themes.sizes.spacing_sm, radius_size=gr.themes.sizes.radius_none)) as demo: + ... +``` + +
+ +
+ +预定义的大小对象包括: + +- `radius_none` +- `radius_sm` +- `radius_md` +- `radius_lg` +- `spacing_sm` +- `spacing_md` +- `spacing_lg` +- `text_sm` +- `text_md` +- `text_lg` + +您还可以创建自己的自定义 `Size` 对象并传递它们。 + +### 核心字体(Core Fonts) + +最后的 2 个构造函数参数设置主题的字体。您可以将一系列字体传递给这些参数,以指定回退字体。如果提供了字符串,它将被加载为系统字体。如果提供了 `gradio.themes.GoogleFont`,则将从 Google Fonts 加载该字体。 + +- `font`:此设置主题的主要字体。在默认主题中,此值设置为 `gradio.themes.GoogleFont("IBM Plex Sans")`。 +- `font_mono`:此设置主题的等宽字体。在默认主题中,此值设置为 `gradio.themes.GoogleFont("IBM Plex Mono")`。 + +您可以修改这些值,例如以下方式: + +```python +with gr.Blocks(theme=gr.themes.Default(font=[gr.themes.GoogleFont("Inconsolata"), "Arial", "sans-serif"])) as demo: + ... +``` + +
+ +
+ +## 通过 `.set()` 扩展主题 + +主题加载后,您还可以修改 CSS 变量的值。为此,请使用主题对象的 `.set()` 方法来访问 CSS 变量。例如: + +```python +theme = gr.themes.Default(primary_hue="blue").set( loader_color="#FF0000", slider_color="#FF0000",) +使用`gr.Blocks(theme=theme)`创建演示块 ... +``` + +在上面的示例中,我们将 `loader_color` 和 `slider_color` 变量设置为`#FF0000`,尽管整体 `primary_color` 使用蓝色调色板。您可以以这种方式设置主题中定义的任何 CSS 变量。 +您的 IDE 类型提示应该帮助您导航这些变量。由于有很多 CSS 变量,让我们看一下这些变量的命名和组织方式。 + +### CSS 变量命名规范 + +CSS 变量名可能会变得很长,例如 `button_primary_background_fill_hover_dark`!但是它们遵循一种常见的命名约定,使得理解变量功能和查找您要查找的变量变得容易。变量名由下划线分隔,由以下组成: + +1. 目标元素,例如 `button`、`slider` 或 `block`。2. 目标元素类型或子元素,例如 `button_primary` 或 `block_label`。3. 属性,例如 `button_primary_background_fill` 或 `block_label_border_width`。4. 任何相关状态,例如 `button_primary_background_fill_hover`。5. 如果在暗模式中值不同,则使用后缀 `_dark`。例如,`input_border_color_focus_dark`。 + 当然,许多 CSS 变量名都比这个短,例如 `table_border_color` 或 `input_shadow`。 + +### CSS 变量组织 + +虽然有数百个 CSS 变量,但并不需要为每个变量都指定单独的值。它们通过引用一组核心变量和彼此引用来获取值。这样做可以仅修改少量变量以改变整个主题的外观和感觉,同时也可以更精细地控制我们可能想要修改的个别元素。 + +#### 引用核心变量 + +要引用其中一个核心构造函数变量,请在变量名前加上星号。要引用核心颜色,请使用`*primary_`、`*secondary_` 或`*neutral_` 前缀,后跟亮度值。例如: + +```python +theme = gr.themes.Default(primary_hue="blue").set( + button_primary_background_fill="*primary_200", + button_primary_background_fill_hover="*primary_300", +) +``` + +在上面的示例中,我们将 `button_primary_background_fill` 和 `button_primary_background_fill_hover` 变量分别设置为`*primary_200` 和`*primary_300`。这些变量将分别设置为蓝色主色调调色板的 200 和 300 亮度值。 +同样地,要引用核心大小,请使用`*spacing_`、`*radius_` 或`*text_` 前缀,后跟大小值。例如: + +```python +theme = gr.themes.Default(radius_size="md").set( + button_primary_border_radius="*radius_xl", +) +``` + +在上面的示例中,我们将 `button_primary_border_radius` 变量设置为`*radius_xl`。此变量将设置为中等半径大小范围的 `xl` 设置。 + +#### 引用其他变量 + +变量也可以引用彼此。例如,请看下面的示例: + +```python +theme = gr.themes.Default().set( + button_primary_background_fill="#FF0000", + button_primary_background_fill_hover="#FF0000", + button_primary_border="#FF0000", +) +``` + +将这些值设置为相同的颜色有点繁琐。相反,我们可以在 `button_primary_background_fill_hover` 和 `button_primary_border` 变量中使用`*` 前缀引用 `button_primary_background_fill` 变量。 + +```python +theme = gr.themes.Default().set( + button_primary_background_fill="#FF0000", + button_primary_background_fill_hover="*button_primary_background_fill", + button_primary_border="*button_primary_background_fill", +) +``` + +现在,如果我们更改 `button_primary_background_fill` 变量,`button_primary_background_fill_hover` 和 `button_primary_border` 变量将自动更新。 +如果您打算共享主题,这将非常有用- 它使得修改主题变得容易,而无需更改每个变量。 +请注意,暗模式变量自动相互引用。例如: + +```python +theme = gr.themes.Default().set( + button_primary_background_fill="#FF0000", + button_primary_background_fill_dark="#AAAAAA", + button_primary_border="*button_primary_background_fill", + button_primary_border_dark="*button_primary_background_fill_dark", +) +``` + +`button_primary_border_dark` 将从 `button_primary_background_fill_dark` 获取其值,因为暗模式总是使用变量的暗版本。 + +## 创建一个完整的主题 + +假设您想从头开始创建一个主题!我们将逐步进行 - 您还可以参考 gradio 源代码库中预构建主题的源代码,请看这里的示例:[Monochrome theme 的源代码](https://github.com/gradio-app/gradio/blob/main/gradio/themes/monochrome.py) +我们的新主题类将继承自 `gradio.themes.Base`,这是一个设置了许多方便默认值的主题。让我们创建一个名为 Seafoam 的简单演示,以及使用它的简单应用程序。 +$code_theme_new_step_1 + +
+ +
+ +Base 主题非常简洁,使用 `gr.themes.Blue` 作为其主要颜色-由于此原因,主按钮和加载动画都是蓝色的。让我们改变应用程序的默认核心参数。我们将覆盖构造函数并传递新的默认值给核心构造函数参数。 +我们将使用 `gr.themes.Emerald` 作为我们的主要颜色,并将次要和中性色调设置为 `gr.themes.Blue`。我们将使用 `text_lg` 使文本更大。我们将使用 `Quicksand` 作为我们的默认字体,从 Google Fonts 加载。 +$code_theme_new_step_2 + +
+ +
+ +注意到主按钮和加载动画现在是绿色的了吗?这些 CSS 变量与 `primary_hue` 相关联。 +我们来直接修改主题。我们将调用 `set()` 方法来明确覆盖 CSS 变量值。我们可以使用任何 CSS 逻辑,并使用`*` 前缀引用我们的核心构造函数的参数。 + +$code_theme_new_step_3 + +
+ +
+ +看看我们的主题现在多么有趣!仅通过几个变量的更改,我们的主题完全改变了。 + +您可能会发现探索[其他预建主题的源代码](https://github.com/gradio-app/gradio/blob/main/gradio/themes)会很有帮助,以了解他们如何修改基本主题。您还可以使用浏览器的检查工具,选择 UI 中的元素并查看在样式面板中使用的 CSS 变量。 + +## 分享主题 + +在创建主题后,您可以将其上传到 HuggingFace Hub,让其他人查看、使用和构建主题! + +### 上传主题 + +有两种上传主题的方式,通过主题类实例或命令行。我们将使用之前创建的“seafoam”主题来介绍这两种方式。 + +- 通过类实例 + +每个主题实例都有一个名为“push_to_hub”的方法,我们可以使用它来将主题上传到 HuggingFace Hub。 + +```python +seafoam.push_to_hub(repo_name="seafoam", + version="0.0.1", + hf_token="") +``` + +- 通过命令行 + +首先将主题保存到磁盘 + +```python +seafoam.dump(filename="seafoam.json") +``` + +然后使用“upload_theme”命令: + +```bash +upload_theme\ +"seafoam.json"\ +"seafoam"\ +--version "0.0.1"\ +--hf_token "" +``` + +要上传主题,您必须拥有一个 HuggingFace 账户,并通过 `hf_token` 参数传递您的[访问令牌](https://huggingface.co/docs/huggingface_hub/quick-start#login)。 +但是,如果您通过[HuggingFace 命令行](https://huggingface.co/docs/huggingface_hub/quick-start#login)登录(与 `gradio` 一起安装), +那么您可以省略 `hf_token` 参数。 + +`version` 参数允许您为主题指定一个有效的[语义版本](https://www.geeksforgeeks.org/introduction-semantic-versioning/)字符串。 +这样,您的用户就可以在他们的应用程序中指定要使用的主题版本。这还允许您发布主题更新而不必担心 +以前创建的应用程序的外观如何更改。`version` 参数是可选的。如果省略,下一个修订版本将自动应用。 + +### 主题预览 + +通过调用 `push_to_hub` 或 `upload_theme`,主题资源将存储在[HuggingFace 空间](https://huggingface.co/docs/hub/spaces-overview)中。 + +我们的 seafoam 主题的预览在这里:[seafoam 预览](https://huggingface.co/spaces/gradio/seafoam)。 + +
+ +
+ +### 发现主题 + +[主题库](https://huggingface.co/spaces/gradio/theme-gallery)显示了所有公开的 gradio 主题。在发布主题之后, +它将在几分钟后自动显示在主题库中。 + +您可以按照空间上点赞的数量以及按创建时间从最近到最近对主题进行排序,也可以在浅色和深色模式之间切换主题。 + +
+ +
+ +### 下载 + +要使用 Hub 中的主题,请在 `ThemeClass` 上使用 `from_hub` 方法,然后将其传递给您的应用程序: + +```python +my_theme = gr.Theme.from_hub("gradio/seafoam") + +with gr.Blocks(theme=my_theme) as demo: + .... +``` + +您也可以直接将主题字符串传递给 `Blocks` 或 `Interface`(`gr.Blocks(theme="gradio/seafoam")`) + +您可以通过使用语义版本表达式将您的应用程序固定到上游主题版本。 + +例如,以下内容将确保我们从“seafoam”仓库中加载的主题位于 `0.0.1` 和 `0.1.0` 版本之间: + +```python +with gr.Blocks(theme="gradio/seafoam@>=0.0.1,<0.1.0") as demo: + .... +``` + +享受创建自己的主题吧!如果您制作了一个自豪的主题,请将其上传到 Hub 与世界分享! +如果在[Twitter](https://twitter.com/gradio)上标记我们,我们可以给您的主题一个宣传! + + diff --git a/guides/cn/07_other-tutorials/using-flagging.md b/guides/cn/07_other-tutorials/using-flagging.md new file mode 100644 index 0000000..f25ae47 --- /dev/null +++ b/guides/cn/07_other-tutorials/using-flagging.md @@ -0,0 +1,197 @@ +# 使用标记 + +相关空间:https://huggingface.co/spaces/gradio/calculator-flagging-crowdsourced, https://huggingface.co/spaces/gradio/calculator-flagging-options, https://huggingface.co/spaces/gradio/calculator-flag-basic +标签:标记,数据 + +## 简介 + +当您演示一个机器学习模型时,您可能希望收集试用模型的用户的数据,特别是模型行为不如预期的数据点。捕获这些“困难”数据点是有价值的,因为它允许您改进机器学习模型并使其更可靠和稳健。 + +Gradio 通过在每个“界面”中包含一个**标记**按钮来简化这些数据的收集。这使得用户或测试人员可以轻松地将数据发送回运行演示的机器。样本会保存在一个 CSV 日志文件中(默认情况下)。如果演示涉及图像、音频、视频或其他类型的文件,则这些文件会单独保存在一个并行目录中,并且这些文件的路径会保存在 CSV 文件中。 + +## 在 `gradio.Interface` 中使用**标记**按钮 + +使用 Gradio 的 `Interface` 进行标记特别简单。默认情况下,在输出组件下方有一个标记为**标记**的按钮。当用户测试您的模型时,如果看到有趣的输出,他们可以点击标记按钮将输入和输出数据发送回运行演示的机器。样本会保存在一个 CSV 日志文件中(默认情况下)。如果演示涉及图像、音频、视频或其他类型的文件,则这些文件会单独保存在一个并行目录中,并且这些文件的路径会保存在 CSV 文件中。 + +在 `gradio.Interface` 中有[四个参数](https://gradio.app/docs/interface#initialization)控制标记的工作方式。我们将详细介绍它们。 + +- `allow_flagging`:此参数可以设置为 `"manual"`(默认值),`"auto"` 或 `"never"`。 + - `manual`:用户将看到一个标记按钮,只有在点击按钮时样本才会被标记。 + - `auto`:用户将不会看到一个标记按钮,但每个样本都会自动被标记。 + - `never`:用户将不会看到一个标记按钮,并且不会标记任何样本。 +- `flagging_options`:此参数可以是 `None`(默认值)或字符串列表。 + - 如果是 `None`,则用户只需点击**标记**按钮,不会显示其他选项。 + - 如果提供了一个字符串列表,则用户会看到多个按钮,对应于提供的每个字符串。例如,如果此参数的值为`[" 错误 ", " 模糊 "]`,则会显示标记为**标记为错误**和**标记为模糊**的按钮。这仅适用于 `allow_flagging` 为 `"manual"` 的情况。 + - 所选选项将与输入和输出一起记录。 +- `flagging_dir`:此参数接受一个字符串。 + - 它表示标记数据存储的目录名称。 +- `flagging_callback`:此参数接受 `FlaggingCallback` 类的子类的实例 + - 使用此参数允许您编写在点击标记按钮时运行的自定义代码 + - 默认情况下,它设置为 `gr.CSVLogger` 的一个实例 + - 一个示例是将其设置为 `gr.HuggingFaceDatasetSaver` 的一个实例,这样您可以将任何标记的数据导入到 HuggingFace 数据集中(参见下文)。 + +## 标记的数据会发生什么? + +在 `flagging_dir` 参数提供的目录中,将记录标记的数据的 CSV 文件。 + +以下是一个示例:下面的代码创建了嵌入其中的计算器界面: + +```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": + return num1 / num2 + + +iface = gr.Interface( + calculator, + ["number", gr.Radio(["add", "subtract", "multiply", "divide"]), "number"], + "number", + allow_flagging="manual" +) + +iface.launch() +``` + + + +当您点击上面的标记按钮时,启动界面的目录将包括一个新的标记子文件夹,其中包含一个 CSV 文件。该 CSV 文件包括所有被标记的数据。 + +```directory ++-- flagged/ +| +-- logs.csv +``` + +_flagged/logs.csv_ + +```csv +num1,operation,num2,Output,timestamp +5,add,7,12,2022-01-31 11:40:51.093412 +6,subtract,1.5,4.5,2022-01-31 03:25:32.023542 +``` + +如果界面涉及文件数据,例如图像和音频组件,还将创建文件夹来存储这些标记的数据。例如,将 `image` 输入到 `image` 输出界面将创建以下结构。 + +```directory ++-- flagged/ +| +-- logs.csv +| +-- image/ +| | +-- 0.png +| | +-- 1.png +| +-- Output/ +| | +-- 0.png +| | +-- 1.png +``` + +_flagged/logs.csv_ + +```csv +im,Output timestamp +im/0.png,Output/0.png,2022-02-04 19:49:58.026963 +im/1.png,Output/1.png,2022-02-02 10:40:51.093412 +``` + +如果您希望用户为标记提供一个原因,您可以将字符串列表传递给 Interface 的 `flagging_options` 参数。用户在标记时必须选择其中一项,选项将作为附加列保存在 CSV 文件中。 + +如果我们回到计算器示例,下面的代码将创建嵌入其中的界面。 + +```python +iface = gr.Interface( + calculator, + ["number", gr.Radio(["add", "subtract", "multiply", "divide"]), "number"], + "number", + allow_flagging="manual", + flagging_options=["wrong sign", "off by one", "other"] +) + +iface.launch() +``` + + + +当用户点击标记按钮时,CSV 文件现在将包括指示所选选项的列。 + +_flagged/logs.csv_ + +```csv +num1,operation,num2,Output,flag,timestamp +5,add,7,-12,wrong sign,2022-02-04 11:40:51.093412 +6,subtract,1.5,3.5,off by one,2022-02-04 11:42:32.062512 +``` + +## HuggingFaceDatasetSaver 回调 + +有时,将数据保存到本地 CSV 文件是不合理的。例如,在 Hugging Face Spaces 上 +,开发者通常无法访问托管 Gradio 演示的底层临时机器。这就是为什么,默认情况下,在 Hugging Face Space 中关闭标记的原因。然而, +您可能希望对标记的数据做其他处理。 +you may want to do something else with the flagged data. + +通过 `flagging_callback` 参数,我们使这变得非常简单。 + +例如,下面我们将会将标记的数据从我们的计算器示例导入到 Hugging Face 数据集中,以便我们可以构建一个“众包”数据集: + +```python +import os + +HF_TOKEN = os.getenv('HF_TOKEN') +hf_writer = gr.HuggingFaceDatasetSaver(HF_TOKEN, "crowdsourced-calculator-demo") + +iface = gr.Interface( + calculator, + ["number", gr.Radio(["add", "subtract", "multiply", "divide"]), "number"], + "number", + description="Check out the crowd-sourced dataset at: [https://huggingface.co/datasets/aliabd/crowdsourced-calculator-demo](https://huggingface.co/datasets/aliabd/crowdsourced-calculator-demo)", + allow_flagging="manual", + flagging_options=["wrong sign", "off by one", "other"], + flagging_callback=hf_writer +) + +iface.launch() +``` + +注意,我们使用我们的 Hugging Face 令牌和 +要保存样本的数据集的名称,定义了我们自己的 +`gradio.HuggingFaceDatasetSaver` 的实例。此外,我们还将 `allow_flagging="manual"` 设置为了 +,因为在 Hugging Face Spaces 中,`allow_flagging` 默认设置为 `"never"`。这是我们的演示: + + + +您现在可以在这个[公共的 Hugging Face 数据集](https://huggingface.co/datasets/aliabd/crowdsourced-calculator-demo)中看到上面标记的所有示例。 + +![flagging callback hf](/assets/guides/flagging-callback-hf.png) + +我们创建了 `gradio.HuggingFaceDatasetSaver` 类,但只要它继承自[此文件](https://github.com/gradio-app/gradio/blob/master/gradio/flagging.py)中定义的 `FlaggingCallback`,您可以传递自己的自定义类。如果您创建了一个很棒的回调,请将其贡献给该存储库! + +## 使用 Blocks 进行标记 + +如果您正在使用 `gradio.Blocks`,又该怎么办呢?一方面,使用 Blocks 您拥有更多的灵活性 +--您可以编写任何您想在按钮被点击时运行的 Python 代码, +并使用 Blocks 中的内置事件分配它。 + +同时,您可能希望使用现有的 `FlaggingCallback` 来避免编写额外的代码。 +这需要两个步骤: + +1. 您必须在代码中的某个位置运行您的回调的 `.setup()` 方法 + 在第一次标记数据之前 +2. 当点击标记按钮时,您触发回调的 `.flag()` 方法, + 确保正确收集参数并禁用通常的预处理。 + +下面是一个使用默认的 `CSVLogger` 标记图像怀旧滤镜 Blocks 演示的示例: +data using the default `CSVLogger`: + +$code_blocks_flag +$demo_blocks_flag + +## 隐私 + +重要提示:请确保用户了解他们提交的数据何时被保存以及您计划如何处理它。当您使用 `allow_flagging=auto`(当通过演示提交的所有数据都被标记时),这一点尤为重要 + +### 这就是全部!祝您建设愉快 :) diff --git a/guides/cn/CONTRIBUTING.md b/guides/cn/CONTRIBUTING.md new file mode 100644 index 0000000..d43e06d --- /dev/null +++ b/guides/cn/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Contributing a Guide + +Want to help teach Gradio? Consider contributing a Guide! 🤗 + +Broadly speaking, there are two types of guides: + +- **Use cases**: guides that cover step-by-step how to build a particular type of machine learning demo or app using Gradio. Here's an example: [_Creating a Chatbot_](https://github.com/gradio-app/gradio/blob/master/guides/creating_a_chatbot.md) +- **Feature explanation**: guides that describe in detail a particular feature of Gradio. Here's an example: [_Using Flagging_](https://github.com/gradio-app/gradio/blob/master/guides/using_flagging.md) + +We encourage you to submit either type of Guide! (Looking for ideas? We may also have open [issues](https://github.com/gradio-app/gradio/issues?q=is%3Aopen+is%3Aissue+label%3Aguides) where users have asked for guides on particular topics) + +## Guide Structure + +As you can see with the previous examples, Guides are standard markdown documents. They usually: + +- start with an Introduction section describing the topic +- include subheadings to make articles easy to navigate +- include real code snippets that make it easy to follow along and implement the Guide +- include embedded Gradio demos to make them more interactive and provide immediate demonstrations of the topic being discussed. These Gradio demos are hosted on [Hugging Face Spaces](https://huggingface.co/spaces) and are embedded using the standard \ tag. + +## How to Contribute a Guide + +1. Clone or fork this `gradio` repo +2. Add a new markdown document with a descriptive title to the `/guides` folder +3. Write your Guide in standard markdown! Embed Gradio demos wherever helpful +4. Add a list of `related_spaces` at the top of the markdown document (see the previously linked Guides for how to do this) +5. Add 3 `tags` at the top of the markdown document to help users find your guide (again, see the previously linked Guides for how to do this) +6. Open a PR to have your guide reviewed + +That's it! We're looking forward to reading your Guide 🥳 diff --git a/guides/cn/assets/access_token.png b/guides/cn/assets/access_token.png new file mode 100644 index 0000000..7e7d8da Binary files /dev/null and b/guides/cn/assets/access_token.png differ diff --git a/guides/cn/assets/annotated.png b/guides/cn/assets/annotated.png new file mode 100644 index 0000000..e8db15f Binary files /dev/null and b/guides/cn/assets/annotated.png differ diff --git a/guides/cn/assets/embed_this_space.png b/guides/cn/assets/embed_this_space.png new file mode 100644 index 0000000..8b37e80 Binary files /dev/null and b/guides/cn/assets/embed_this_space.png differ diff --git a/guides/cn/assets/flagging-callback-hf.png b/guides/cn/assets/flagging-callback-hf.png new file mode 100644 index 0000000..5e049fb Binary files /dev/null and b/guides/cn/assets/flagging-callback-hf.png differ diff --git a/guides/cn/assets/hf_demo.mp4 b/guides/cn/assets/hf_demo.mp4 new file mode 100644 index 0000000..deb6d28 Binary files /dev/null and b/guides/cn/assets/hf_demo.mp4 differ diff --git a/guides/cn/assets/secrets.png b/guides/cn/assets/secrets.png new file mode 100644 index 0000000..b14d9d1 Binary files /dev/null and b/guides/cn/assets/secrets.png differ diff --git a/guides/cn/assets/sharing.svg b/guides/cn/assets/sharing.svg new file mode 100644 index 0000000..334e0d0 --- /dev/null +++ b/guides/cn/assets/sharing.svg @@ -0,0 +1,487 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + lion + lion + HOST + REMOTE USERS + diff --git a/guides/cn/assets/use_via_api.png b/guides/cn/assets/use_via_api.png new file mode 100644 index 0000000..1e17e8f Binary files /dev/null and b/guides/cn/assets/use_via_api.png differ diff --git a/home/runner/work/gradio/gradio/client/js/src/client.d.ts b/home/runner/work/gradio/gradio/client/js/src/client.d.ts new file mode 100644 index 0000000..aed6efd --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/client.d.ts @@ -0,0 +1,77 @@ +import type { ApiData, ApiInfo, ClientOptions, Config, DuplicateOptions, EndpointInfo, JsApiData, PredictReturn, SpaceStatus, Status, UploadResponse, SubmitIterable, GradioEvent } from "./types"; +import { FileData } from "./upload"; +export declare class Client { + app_reference: string; + options: ClientOptions; + deep_link: string | null; + config: Config | undefined; + api_prefix: string; + api_info: ApiInfo | undefined; + api_map: Record; + session_hash: string; + jwt: string | false; + last_status: Record; + private cookies; + stream_status: { + open: boolean; + }; + closed: boolean; + pending_stream_messages: Record; + pending_diff_streams: Record; + event_callbacks: Record Promise>; + unclosed_events: Set; + heartbeat_event: EventSource | null; + abort_controller: AbortController | null; + stream_instance: EventSource | null; + current_payload: any; + get_url_config(url?: string | null): Config; + get_page_config(page: string): Config; + fetch(input: RequestInfo | URL, init?: RequestInit): Promise; + stream(url: URL): EventSource; + view_api: () => Promise>; + upload_files: (root_url: string, files: (Blob | File)[], upload_id?: string) => Promise; + 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) => Promise; + post_data: (url: string, body: unknown, additional_headers?: any) => Promise; + submit: (endpoint: string | number, data: unknown[] | Record | undefined, event_data?: unknown, trigger_id?: number | null, all_events?: boolean) => SubmitIterable; + predict: (endpoint: string | number, data: unknown[] | Record | undefined, event_data?: unknown) => Promise>; + open_stream: () => Promise; + private resolve_config; + private resolve_cookies; + constructor(app_reference: string, options?: ClientOptions); + private init; + _resolve_heartbeat(_config: Config): Promise; + static connect(app_reference: string, options?: ClientOptions): Promise; + reconnect(): Promise<"connected" | "broken" | "changed">; + close(): void; + set_current_payload(payload: any): void; + static duplicate(app_reference: string, options?: DuplicateOptions): Promise; + private _resolve_config; + private config_success; + handle_space_success(status: SpaceStatus): Promise; + component_server(component_id: number, fn_name: string, data: unknown | { + binary: boolean; + data: Record; + }): Promise; + set_cookies(raw_cookies: string): void; + private prepare_return_obj; +} +/** + * @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} A promise that resolves to a `Client` instance. + */ +export declare function client(app_reference: string, options?: ClientOptions): Promise; +/** + * @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} A promise that resolves to a `Client` instance. + */ +export declare function duplicate_space(app_reference: string, options: DuplicateOptions): Promise; +export type ClientInstance = Client; diff --git a/home/runner/work/gradio/gradio/client/js/src/constants.d.ts b/home/runner/work/gradio/gradio/client/js/src/constants.d.ts new file mode 100644 index 0000000..3d93f43 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/constants.d.ts @@ -0,0 +1,33 @@ +export declare const HOST_URL = "host"; +export declare const API_URL = "predict/"; +export declare const SSE_URL_V0 = "queue/join"; +export declare const SSE_DATA_URL_V0 = "queue/data"; +export declare const SSE_URL = "queue/data"; +export declare const SSE_DATA_URL = "queue/join"; +export declare const UPLOAD_URL = "upload"; +export declare const LOGIN_URL = "login"; +export declare const CONFIG_URL = "config"; +export declare const API_INFO_URL = "info"; +export declare const RUNTIME_URL = "runtime"; +export declare const SLEEPTIME_URL = "sleeptime"; +export declare const HEARTBEAT_URL = "heartbeat"; +export declare const COMPONENT_SERVER_URL = "component_server"; +export declare const RESET_URL = "reset"; +export declare const CANCEL_URL = "cancel"; +export declare const APP_ID_URL = "app_id"; +export declare const RAW_API_INFO_URL = "info?serialize=False"; +export declare const SPACE_FETCHER_URL = "https://gradio-space-api-fetcher-v2.hf.space/api"; +export declare const SPACE_URL = "https://hf.space/{}"; +export declare const QUEUE_FULL_MSG = "This application is currently busy. Please try again. "; +export declare const BROKEN_CONNECTION_MSG = "Connection errored out. "; +export declare const CONFIG_ERROR_MSG = "Could not resolve app config. "; +export declare const SPACE_STATUS_ERROR_MSG = "Could not get space status. "; +export declare const API_INFO_ERROR_MSG = "Could not get API info. "; +export declare const SPACE_METADATA_ERROR_MSG = "Space metadata could not be loaded. "; +export declare const INVALID_URL_MSG = "Invalid URL. A full URL path is required."; +export declare const UNAUTHORIZED_MSG = "Not authorized to access this space. "; +export declare const INVALID_CREDENTIALS_MSG = "Invalid credentials. Could not login. "; +export declare const MISSING_CREDENTIALS_MSG = "Login credentials are required to access this space."; +export declare const NODEJS_FS_ERROR_MSG = "File system access is only available in Node.js environments"; +export declare const ROOT_URL_ERROR_MSG = "Root URL not found in client config"; +export declare const FILE_PROCESSING_ERROR_MSG = "Error uploading file"; diff --git a/home/runner/work/gradio/gradio/client/js/src/helpers/api_info.d.ts b/home/runner/work/gradio/gradio/client/js/src/helpers/api_info.d.ts new file mode 100644 index 0000000..df3a787 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/helpers/api_info.d.ts @@ -0,0 +1,47 @@ +import type { ApiData, ApiInfo, Config, JsApiData, EndpointInfo, Status } from "../types"; +export declare const RE_SPACE_NAME: RegExp; +export declare const RE_SPACE_DOMAIN: RegExp; +export declare function process_endpoint(app_reference: string, token?: `hf_${string}`): Promise<{ + space_id: string | false; + host: string; + ws_protocol: "ws" | "wss"; + http_protocol: "http:" | "https:"; +}>; +export declare const join_urls: (...urls: string[]) => string; +export declare function transform_api_info(api_info: ApiInfo, config: Config, api_map: Record): ApiInfo; +export declare function get_type(type: { + type: any; + description: string; +}, component: string, serializer: string, signature_type: "return" | "parameter"): string | undefined; +export declare function get_description(type: { + type: any; + description: string; +}, serializer: string): string; +export declare 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; +}; +/** + * 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} 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 declare const map_data_to_params: (data: (unknown[] | Record) | undefined, endpoint_info: EndpointInfo) => unknown[]; diff --git a/home/runner/work/gradio/gradio/client/js/src/helpers/data.d.ts b/home/runner/work/gradio/gradio/client/js/src/helpers/data.d.ts new file mode 100644 index 0000000..78d8c6f --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/helpers/data.d.ts @@ -0,0 +1,20 @@ +import { type ApiData, type BlobRef, type Config, type EndpointInfo, type JsApiData, type DataType, Command, type Dependency, type ComponentMeta } from "../types"; +import { FileData } from "../upload"; +export declare function update_object(object: { + [x: string]: any; +}, newValue: any, stack: (string | number)[]): void; +export declare function walk_and_store_blobs(data: DataType, type?: string | undefined, path?: string[], root?: boolean, endpoint_info?: EndpointInfo | undefined): Promise; +export declare function skip_queue(id: number, config: Config): boolean; +export declare function post_message(message: any, origin: string): Promise; +export declare function handle_file(file_or_url: File | string | Blob | Buffer): FileData | Blob | Command; +/** + * 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 declare function handle_payload(resolved_payload: unknown[], dependency: Dependency, components: ComponentMeta[], type: "input" | "output", with_null_state?: boolean): unknown[]; diff --git a/home/runner/work/gradio/gradio/client/js/src/helpers/init_helpers.d.ts b/home/runner/work/gradio/gradio/client/js/src/helpers/init_helpers.d.ts new file mode 100644 index 0000000..34c0a9d --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/helpers/init_helpers.d.ts @@ -0,0 +1,24 @@ +import type { Config } from "../types"; +import { Client } from ".."; +/** + * 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 declare function resolve_root(base_url: string, root_path: string, prioritize_base: boolean): string; +export declare function get_jwt(space: string, token: `hf_${string}`, cookies?: string | null): Promise; +export declare function map_names_to_ids(fns: Config["dependencies"]): Record; +export declare function resolve_config(this: Client, endpoint: string): Promise; +export declare function resolve_cookies(this: Client): Promise; +export declare function get_cookie_header(http_protocol: string, host: string, auth: [string, string], _fetch: typeof fetch, token?: `hf_${string}`): Promise; +export declare function determine_protocol(endpoint: string): { + ws_protocol: "ws" | "wss"; + http_protocol: "http:" | "https:"; + host: string; +}; +export declare const parse_and_set_cookies: (cookie_header: string) => string[]; diff --git a/home/runner/work/gradio/gradio/client/js/src/helpers/spaces.d.ts b/home/runner/work/gradio/gradio/client/js/src/helpers/spaces.d.ts new file mode 100644 index 0000000..cf2d2cb --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/helpers/spaces.d.ts @@ -0,0 +1,7 @@ +import type { SpaceStatusCallback } from "../types"; +export declare function check_space_status(id: string, type: "subdomain" | "space_name", status_callback: SpaceStatusCallback): Promise; +export declare const check_and_wake_space: (space_id: string, status_callback: SpaceStatusCallback) => Promise; +export declare function discussions_enabled(space_id: string): Promise; +export declare function get_space_hardware(space_id: string, token?: `hf_${string}` | undefined): Promise<(typeof hardware_types)[number]>; +export declare function set_space_timeout(space_id: string, timeout: number, token?: `hf_${string}`): Promise; +export declare const hardware_types: readonly ["cpu-basic", "cpu-upgrade", "cpu-xl", "t4-small", "t4-medium", "a10g-small", "a10g-large", "a10g-largex2", "a10g-largex4", "a100-large", "zero-a10g", "h100", "h100x8"]; diff --git a/home/runner/work/gradio/gradio/client/js/src/helpers/zerogpu.d.ts b/home/runner/work/gradio/gradio/client/js/src/helpers/zerogpu.d.ts new file mode 100644 index 0000000..2e76ef8 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/helpers/zerogpu.d.ts @@ -0,0 +1,2 @@ +export declare function get_zerogpu_origin(hostname: string): string | null; +export declare function initialize_zerogpu_handshake(): void; diff --git a/home/runner/work/gradio/gradio/client/js/src/index.d.ts b/home/runner/work/gradio/gradio/client/js/src/index.d.ts new file mode 100644 index 0000000..61d5b98 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/index.d.ts @@ -0,0 +1,10 @@ +export { Client } from "./client"; +export { predict } from "./utils/predict"; +export { submit } from "./utils/submit"; +export { upload_files } from "./utils/upload_files"; +export { FileData, upload, prepare_files } from "./upload"; +export { handle_file } from "./helpers/data"; +export type { SpaceStatus, StatusMessage, Status, client_return, UploadResponse, RenderMessage, LogMessage, Payload, Config, ValidationError } from "./types"; +export { MISSING_CREDENTIALS_MSG } from "./constants"; +export { client } from "./client"; +export { duplicate_space as duplicate } from "./client"; diff --git a/home/runner/work/gradio/gradio/client/js/src/types.d.ts b/home/runner/work/gradio/gradio/client/js/src/types.d.ts new file mode 100644 index 0000000..c4e39f1 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/types.d.ts @@ -0,0 +1,347 @@ +import { hardware_types } from "./helpers/spaces"; +import type { SvelteComponent } from "svelte"; +import type { ComponentType } from "svelte"; +export interface ApiData { + label: string; + parameter_name: string; + parameter_default?: any; + parameter_has_default?: boolean; + type: { + type: any; + description: string; + }; + component: string; + example_input?: any; + python_type: { + type: string; + description: string; + }; + serializer: string; +} +export interface JsApiData { + label: string; + parameter_name: string; + parameter_default?: any; + parameter_has_default?: boolean; + type: string; + description: string; + component: string; + example_input?: any; + serializer: string; + python_type: { + type: string; + description: string; + }; +} +export interface EndpointInfo { + parameters: T[]; + returns: T[]; + type?: DependencyTypes; +} +export interface ApiInfo { + named_endpoints: Record>; + unnamed_endpoints: Record>; +} +export interface BlobRef { + path: string[]; + type: string | undefined; + blob: Blob | File | false; +} +export type DataType = string | Buffer | Record | any[]; +export declare class Command { + type: string; + command: string; + meta: { + path: string; + name: string; + orig_path: string; + }; + fileData?: FileData; + constructor(command: string, meta: { + path: string; + name: string; + orig_path: string; + }); +} +export type SubmitFunction = (endpoint: string | number, data?: unknown[] | Record, event_data?: unknown, trigger_id?: number | null, all_events?: boolean, additional_headers?: Record) => SubmitIterable; +export type PredictFunction = (endpoint: string | number, data?: unknown[] | Record, event_data?: unknown) => Promise>; +export type client_return = { + config: Config | undefined; + predict: PredictFunction; + submit: SubmitFunction; + component_server: (component_id: number, fn_name: string, data: unknown[]) => any; + view_api: (_fetch: typeof fetch) => Promise>; +}; +export interface SubmitIterable extends AsyncIterable { + [Symbol.asyncIterator](): SubmitIterable; + next: () => Promise>; + throw: (value: unknown) => Promise>; + return: () => Promise>; + cancel: () => Promise; + event_id: () => string; + send_chunk: (payload: Record) => void; + wait_for_id: () => Promise; + close_stream: () => void; +} +export type PredictReturn = { + type: EventType; + time: Date; + data: T; + endpoint: string; + fn_index: number; +}; +export type SpaceStatus = SpaceStatusNormal | SpaceStatusError; +export interface SpaceStatusNormal { + status: "sleeping" | "running" | "building" | "error" | "stopped" | "starting"; + detail: "SLEEPING" | "RUNNING" | "RUNNING_BUILDING" | "BUILDING" | "APP_STARTING" | "NOT_FOUND"; + load_status: "pending" | "error" | "complete" | "generating"; + message: string; +} +export interface SpaceStatusError { + status: "space_error" | "paused"; + detail: "NO_APP_FILE" | "CONFIG_ERROR" | "BUILD_ERROR" | "RUNTIME_ERROR" | "PAUSED"; + load_status: "error"; + message: string; + discussions_enabled: boolean; +} +export type SpaceStatusCallback = (a: SpaceStatus) => void; +export interface Config { + deep_link_state?: "none" | "valid" | "invalid"; + auth_required?: true; + app_id?: string; + analytics_enabled: boolean; + connect_heartbeat: boolean; + dev_mode: boolean; + vibe_mode: boolean; + auth_message: string; + components: ComponentMeta[]; + css: string | null; + js: string | null; + head: string | null; + dependencies: Dependency[]; + enable_queue: boolean; + show_error: boolean; + layout: any; + mode: "blocks" | "interface" | "chat_interface"; + root: string; + root_url?: string; + theme: string; + title: string; + version: string; + space_id: string | null; + is_space: boolean; + is_colab: boolean; + footer_links: string[]; + stylesheets: string[]; + current_page: string; + page: Record; + pages: [string, string, boolean][]; + protocol: "sse_v3" | "sse_v2.1" | "sse_v2" | "sse_v1" | "sse" | "ws"; + max_file_size?: number; + theme_hash?: number; + username: string | null; + api_prefix?: string; + fill_height?: boolean; + fill_width?: boolean; + pwa?: boolean; + i18n_translations?: Record> | null; + mcp_server?: boolean; +} +export interface ComponentMeta { + type: string; + id: number; + has_modes: boolean; + props: SharedProps; + instance: SvelteComponent; + component: ComponentType; + documentation?: Documentation; + children?: ComponentMeta[]; + parent?: ComponentMeta; + value?: any; + component_class_id: string; + key: string | number | null; + rendered_in?: number; +} +interface SharedProps { + elem_id?: string; + elem_classes?: string[]; + components?: string[]; + server_fns?: string[]; + interactive: boolean; + visible: boolean | "hidden"; + [key: string]: unknown; + root_url?: string; +} +export interface Documentation { + type?: TypeDescription; + description?: TypeDescription; + example_data?: string; +} +interface TypeDescription { + input_payload?: string; + response_object?: string; + payload?: string; +} +export interface Dependency { + id: number; + targets: [number, string][]; + inputs: number[]; + outputs: number[]; + backend_fn: boolean; + js: string | null; + scroll_to_output: boolean; + trigger: "click" | "load" | string; + max_batch_size: number; + show_progress: "full" | "minimal" | "hidden"; + show_progress_on: number[] | null; + frontend_fn: ((...args: unknown[]) => Promise) | null; + status?: string; + queue: boolean | null; + every: number | null; + batch: boolean; + api_name: string | null; + cancels: number[]; + types: DependencyTypes; + collects_event_data: boolean; + pending_request?: boolean; + trigger_after?: number; + trigger_only_on_success?: boolean; + trigger_only_on_failure?: boolean; + trigger_mode: "once" | "multiple" | "always_last"; + final_event: Payload | null; + api_visibility: "public" | "private" | "undocumented"; + rendered_in: number | null; + render_id: number | null; + connection: "stream" | "sse"; + time_limit: number; + stream_every: number; + like_user_message: boolean; + event_specific_args: string[]; + component_prop_inputs: number[]; + js_implementation: string | null; +} +export interface DependencyTypes { + generator: boolean; + cancel: boolean; +} +export interface Payload { + fn_index: number; + data: unknown[]; + time?: Date; + event_data?: unknown; + trigger_id?: number | null; +} +export interface PostResponse { + error?: string; + [x: string]: any; +} +export interface UploadResponse { + error?: string; + files?: string[]; +} +export interface DuplicateOptions extends ClientOptions { + private?: boolean; + hardware?: (typeof hardware_types)[number]; + timeout?: number; +} +export interface ClientOptions { + token?: `hf_${string}`; + status_callback?: SpaceStatusCallback | null; + auth?: [string, string] | null; + with_null_state?: boolean; + events?: EventType[]; + headers?: Record | Headers; + query_params?: Record; + session_hash?: string; + cookies?: string; +} +export interface FileData { + name: string; + orig_name?: string; + size?: number; + data: string; + blob?: File; + is_file?: boolean; + mime_type?: string; + alt_text?: string; +} +export type EventType = "data" | "status" | "log" | "render"; +export interface EventMap { + data: PayloadMessage; + status: StatusMessage; + log: LogMessage; + render: RenderMessage; +} +export type GradioEvent = { + [P in EventType]: EventMap[P]; +}[EventType]; +export interface Log { + log: string; + title: string; + level: "warning" | "info" | "success" | "error"; +} +export interface Render { + data: { + components: any[]; + layout: any; + dependencies: Dependency[]; + render_id: number; + }; +} +export interface ValidationError { + is_valid: boolean; + message: string; +} +export interface Status { + queue: boolean; + code?: string; + success?: boolean; + stage: "pending" | "error" | "complete" | "generating" | "streaming"; + duration?: number; + visible?: boolean; + broken?: boolean; + size?: number; + position?: number; + eta?: number; + title?: string; + message?: string | ValidationError[]; + progress_data?: { + progress: number | null; + index: number | null; + length: number | null; + unit: string | null; + desc: string | null; + }[]; + time?: Date; + changed_state_ids?: number[]; + time_limit?: number; + session_not_found?: boolean; +} +export interface StatusMessage extends Status { + type: "status"; + endpoint: string; + fn_index: number; + original_msg?: string; +} +export interface PayloadMessage extends Payload { + type: "data"; + endpoint: string; + fn_index: number; +} +export interface LogMessage extends Log { + type: "log"; + endpoint: string; + fn_index: number; + duration: number | null; + visible: boolean; +} +export interface RenderMessage extends Render { + type: "render"; + endpoint: string; + fn_index: number; +} +export {}; diff --git a/home/runner/work/gradio/gradio/client/js/src/upload.d.ts b/home/runner/work/gradio/gradio/client/js/src/upload.d.ts new file mode 100644 index 0000000..44bdfe5 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/upload.d.ts @@ -0,0 +1,28 @@ +import type { Client } from "./client"; +export declare function upload(this: Client, file_data: FileData[], root_url: string, upload_id?: string, max_file_size?: number): Promise<(FileData | null)[] | null>; +export declare function prepare_files(files: File[], is_stream?: boolean): Promise; +export declare class FileData { + path: string; + url?: string; + orig_name?: string; + size?: number; + blob?: File; + is_stream?: boolean; + mime_type?: string; + alt_text?: string; + b64?: string; + readonly meta: { + _type: string; + }; + constructor({ path, url, orig_name, size, blob, is_stream, mime_type, alt_text, b64 }: { + path: string; + url?: string; + orig_name?: string; + size?: number; + blob?: File; + is_stream?: boolean; + mime_type?: string; + alt_text?: string; + b64?: string; + }); +} diff --git a/home/runner/work/gradio/gradio/client/js/src/utils/duplicate.d.ts b/home/runner/work/gradio/gradio/client/js/src/utils/duplicate.d.ts new file mode 100644 index 0000000..2d7cd24 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/utils/duplicate.d.ts @@ -0,0 +1,3 @@ +import type { DuplicateOptions } from "../types"; +import { Client } from "../client"; +export declare function duplicate(app_reference: string, options: DuplicateOptions): Promise; diff --git a/home/runner/work/gradio/gradio/client/js/src/utils/filesize.d.ts b/home/runner/work/gradio/gradio/client/js/src/utils/filesize.d.ts new file mode 100644 index 0000000..914e5e0 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/utils/filesize.d.ts @@ -0,0 +1,14 @@ +type SPEC = { + readonly radix: number; + readonly unit: string[]; +}; +export declare const SPECS: Record; +/** + * file size from https://github.com/hustcc/filesize.js + * @param bytes - The number of bytes to convert to human-readable format + * @param fixed - Number of decimal places to display (default: 1) + * @param spec - Size specification to use: "si", "iec", or "jedec" (default: "jedec") + * @returns Human-readable file size string + */ +export declare function filesize(bytes: number, fixed?: number, spec?: string): string; +export {}; diff --git a/home/runner/work/gradio/gradio/client/js/src/utils/handle_blob.d.ts b/home/runner/work/gradio/gradio/client/js/src/utils/handle_blob.d.ts new file mode 100644 index 0000000..a5c1acb --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/utils/handle_blob.d.ts @@ -0,0 +1,4 @@ +import { type ApiData, type EndpointInfo, type JsApiData } from "../types"; +import type { Client } from ".."; +export declare function handle_blob(this: Client, endpoint: string, data: unknown[], api_info: EndpointInfo): Promise; +export declare function process_local_file_commands(client: Client, data: unknown[]): Promise; diff --git a/home/runner/work/gradio/gradio/client/js/src/utils/post_data.d.ts b/home/runner/work/gradio/gradio/client/js/src/utils/post_data.d.ts new file mode 100644 index 0000000..444c393 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/utils/post_data.d.ts @@ -0,0 +1,3 @@ +import type { PostResponse } from "../types"; +import { Client } from ".."; +export declare function post_data(this: Client, url: string, body: unknown, additional_headers?: any): Promise<[PostResponse, number]>; diff --git a/home/runner/work/gradio/gradio/client/js/src/utils/predict.d.ts b/home/runner/work/gradio/gradio/client/js/src/utils/predict.d.ts new file mode 100644 index 0000000..2559358 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/utils/predict.d.ts @@ -0,0 +1,3 @@ +import { Client } from "../client"; +import type { PredictReturn } from "../types"; +export declare function predict(this: Client, endpoint: string | number, data?: unknown[] | Record): Promise>; diff --git a/home/runner/work/gradio/gradio/client/js/src/utils/stream.d.ts b/home/runner/work/gradio/gradio/client/js/src/utils/stream.d.ts new file mode 100644 index 0000000..c6a069b --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/utils/stream.d.ts @@ -0,0 +1,8 @@ +import type { Client } from "../client"; +export declare function open_stream(this: Client): Promise; +export declare function close_stream(stream_status: { + open: boolean; +}, abort_controller: AbortController | null): void; +export declare function apply_diff_stream(pending_diff_streams: Record, event_id: string, data: any): void; +export declare function apply_diff(obj: any, diff: [string, (number | string)[], any][]): any; +export declare function readable_stream(input: RequestInfo | URL, init?: RequestInit): EventSource; diff --git a/home/runner/work/gradio/gradio/client/js/src/utils/submit.d.ts b/home/runner/work/gradio/gradio/client/js/src/utils/submit.d.ts new file mode 100644 index 0000000..e8b3c25 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/utils/submit.d.ts @@ -0,0 +1,3 @@ +import type { GradioEvent, SubmitIterable } from "../types"; +import { Client } from "../client"; +export declare function submit(this: Client, endpoint: string | number, data?: unknown[] | Record, event_data?: unknown, trigger_id?: number | null, all_events?: boolean, additional_headers?: Record): SubmitIterable; diff --git a/home/runner/work/gradio/gradio/client/js/src/utils/upload_files.d.ts b/home/runner/work/gradio/gradio/client/js/src/utils/upload_files.d.ts new file mode 100644 index 0000000..a357a73 --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/utils/upload_files.d.ts @@ -0,0 +1,3 @@ +import type { Client } from ".."; +import type { UploadResponse } from "../types"; +export declare function upload_files(this: Client, root_url: string, files: (Blob | File)[], upload_id?: string): Promise; diff --git a/home/runner/work/gradio/gradio/client/js/src/utils/view_api.d.ts b/home/runner/work/gradio/gradio/client/js/src/utils/view_api.d.ts new file mode 100644 index 0000000..33fa48f --- /dev/null +++ b/home/runner/work/gradio/gradio/client/js/src/utils/view_api.d.ts @@ -0,0 +1,2 @@ +import { Client } from "../client"; +export declare function view_api(this: Client): Promise; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/Block.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/Block.svelte.d.ts new file mode 100644 index 0000000..8c31f4c --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/Block.svelte.d.ts @@ -0,0 +1,51 @@ +interface $$__sveltets_2_IsomorphicComponent = any, Events extends Record = any, Slots extends Record = any, Exports = {}, Bindings = string> { + new (options: import('svelte').ComponentConstructorOptions): import('svelte').SvelteComponent & { + $$bindings?: Bindings; + } & Exports; + (internal: unknown, props: Props & { + $$events?: Events; + $$slots?: Slots; + }): Exports & { + $set?: any; + $on?: any; + }; + z_$$bindings?: Bindings; +} +type $$__sveltets_2_PropsWithChildren = Props & (Slots extends { + default: any; +} ? Props extends Record ? any : { + children?: any; +} : {}); +declare const Block: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<{ + height?: number | string | undefined; + min_height?: number | string | undefined; + max_height?: number | string | undefined; + width?: number | string | undefined; + elem_id?: string; + elem_classes?: string[]; + variant?: "solid" | "dashed" | "none"; + border_mode?: "base" | "focus" | "contrast"; + padding?: boolean; + type?: "normal" | "fieldset"; + test_id?: string | undefined; + explicit_call?: boolean; + container?: boolean; + visible?: boolean | "hidden"; + allow_overflow?: boolean; + overflow_behavior?: "visible" | "auto"; + scale?: number | null; + min_width?: number; + flex?: boolean; + resizable?: boolean; + rtl?: boolean; + fullscreen?: boolean; + label?: string | undefined; +}, { + default: {}; +}>, { + [evt: string]: CustomEvent; +}, { + default: {}; +}, {}, string>; +type Block = InstanceType; +export default Block; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/BlockLabel.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/BlockLabel.svelte.d.ts new file mode 100644 index 0000000..f4d91d7 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/BlockLabel.svelte.d.ts @@ -0,0 +1,25 @@ +interface $$__sveltets_2_IsomorphicComponent = any, Events extends Record = any, Slots extends Record = any, Exports = {}, Bindings = string> { + new (options: import('svelte').ComponentConstructorOptions): import('svelte').SvelteComponent & { + $$bindings?: Bindings; + } & Exports; + (internal: unknown, props: Props & { + $$events?: Events; + $$slots?: Slots; + }): Exports & { + $set?: any; + $on?: any; + }; + z_$$bindings?: Bindings; +} +declare const BlockLabel: $$__sveltets_2_IsomorphicComponent<{ + label?: string | null; + Icon: any; + show_label?: boolean; + disable?: boolean; + float?: boolean; + rtl?: boolean; +}, { + [evt: string]: CustomEvent; +}, {}, {}, string>; +type BlockLabel = InstanceType; +export default BlockLabel; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/BlockTitle.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/BlockTitle.svelte.d.ts new file mode 100644 index 0000000..6133c74 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/BlockTitle.svelte.d.ts @@ -0,0 +1,31 @@ +interface $$__sveltets_2_IsomorphicComponent = any, Events extends Record = any, Slots extends Record = any, Exports = {}, Bindings = string> { + new (options: import('svelte').ComponentConstructorOptions): import('svelte').SvelteComponent & { + $$bindings?: Bindings; + } & Exports; + (internal: unknown, props: Props & { + $$events?: Events; + $$slots?: Slots; + }): Exports & { + $set?: any; + $on?: any; + }; + z_$$bindings?: Bindings; +} +type $$__sveltets_2_PropsWithChildren = Props & (Slots extends { + default: any; +} ? Props extends Record ? any : { + children?: any; +} : {}); +declare const BlockTitle: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<{ + show_label?: boolean; + info?: string | undefined; + rtl?: boolean; +}, { + default: {}; +}>, { + [evt: string]: CustomEvent; +}, { + default: {}; +}, {}, string>; +type BlockTitle = InstanceType; +export default BlockTitle; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/CustomButton.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/CustomButton.svelte.d.ts new file mode 100644 index 0000000..8d42822 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/CustomButton.svelte.d.ts @@ -0,0 +1,4 @@ +import type { CustomButton } from "@gradio/utils"; +declare const CustomButton: any; +type CustomButton = InstanceType; +export default CustomButton; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/DownloadLink.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/DownloadLink.svelte.d.ts new file mode 100644 index 0000000..a39f25d --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/DownloadLink.svelte.d.ts @@ -0,0 +1,30 @@ +import type { HTMLAnchorAttributes } from "svelte/elements"; +interface $$__sveltets_2_IsomorphicComponent = any, Events extends Record = any, Slots extends Record = any, Exports = {}, Bindings = string> { + new (options: import('svelte').ComponentConstructorOptions): import('svelte').SvelteComponent & { + $$bindings?: Bindings; + } & Exports; + (internal: unknown, props: Props & { + $$events?: Events; + $$slots?: Slots; + }): Exports & { + $set?: any; + $on?: any; + }; + z_$$bindings?: Bindings; +} +type $$__sveltets_2_PropsWithChildren = Props & (Slots extends { + default: any; +} ? Props extends Record ? any : { + children?: any; +} : {}); +declare const DownloadLink: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren & { + download: NonNullable; +}, { + default: {}; +}>, { + [evt: string]: CustomEvent; +}, { + default: {}; +}, {}, string>; +type DownloadLink = InstanceType; +export default DownloadLink; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/Empty.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/Empty.svelte.d.ts new file mode 100644 index 0000000..4c3b0da --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/Empty.svelte.d.ts @@ -0,0 +1,30 @@ +interface $$__sveltets_2_IsomorphicComponent = any, Events extends Record = any, Slots extends Record = any, Exports = {}, Bindings = string> { + new (options: import('svelte').ComponentConstructorOptions): import('svelte').SvelteComponent & { + $$bindings?: Bindings; + } & Exports; + (internal: unknown, props: Props & { + $$events?: Events; + $$slots?: Slots; + }): Exports & { + $set?: any; + $on?: any; + }; + z_$$bindings?: Bindings; +} +type $$__sveltets_2_PropsWithChildren = Props & (Slots extends { + default: any; +} ? Props extends Record ? any : { + children?: any; +} : {}); +declare const Empty: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<{ + size?: "small" | "large"; + unpadded_box?: boolean; +}, { + default: {}; +}>, { + [evt: string]: CustomEvent; +}, { + default: {}; +}, {}, string>; +type Empty = InstanceType; +export default Empty; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/FullscreenButton.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/FullscreenButton.svelte.d.ts new file mode 100644 index 0000000..d1e9cfe --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/FullscreenButton.svelte.d.ts @@ -0,0 +1,7 @@ +type $$ComponentProps = { + fullscreen: boolean; + onclick: (fullscreen: boolean) => void; +}; +declare const FullscreenButton: import("svelte").Component<$$ComponentProps, {}, "">; +type FullscreenButton = ReturnType; +export default FullscreenButton; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/IconButton.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/IconButton.svelte.d.ts new file mode 100644 index 0000000..1497771 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/IconButton.svelte.d.ts @@ -0,0 +1,21 @@ +import { type Component, type Snippet } from "svelte"; +type $$ComponentProps = { + Icon: Component; + label?: string; + show_label?: boolean; + pending?: boolean; + size?: "x-small" | "small" | "large" | "medium"; + padded?: boolean; + highlight?: boolean; + disabled?: boolean; + hasPopup?: boolean; + color?: string; + transparent?: boolean; + background?: string; + border?: string; + onclick?: (event: MouseEvent) => void; + children?: Snippet; +}; +declare const IconButton: Component<$$ComponentProps, {}, "">; +type IconButton = ReturnType; +export default IconButton; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/IconButtonWrapper.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/IconButtonWrapper.svelte.d.ts new file mode 100644 index 0000000..06e4f53 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/IconButtonWrapper.svelte.d.ts @@ -0,0 +1,13 @@ +import type { CustomButton as CustomButtonType } from "@gradio/utils"; +import type { Snippet } from "svelte"; +type $$ComponentProps = { + top_panel?: boolean; + display_top_corner?: boolean; + show_background?: boolean; + buttons?: (string | CustomButtonType)[] | null; + on_custom_button_click?: ((id: number) => void) | null; + children?: Snippet; +}; +declare const IconButtonWrapper: import("svelte").Component<$$ComponentProps, {}, "">; +type IconButtonWrapper = ReturnType; +export default IconButtonWrapper; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/Info.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/Info.svelte.d.ts new file mode 100644 index 0000000..a54b4d8 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/Info.svelte.d.ts @@ -0,0 +1,20 @@ +interface $$__sveltets_2_IsomorphicComponent = any, Events extends Record = any, Slots extends Record = any, Exports = {}, Bindings = string> { + new (options: import('svelte').ComponentConstructorOptions): import('svelte').SvelteComponent & { + $$bindings?: Bindings; + } & Exports; + (internal: unknown, props: Props & { + $$events?: Events; + $$slots?: Slots; + }): Exports & { + $set?: any; + $on?: any; + }; + z_$$bindings?: Bindings; +} +declare const Info: $$__sveltets_2_IsomorphicComponent<{ + info: string; +}, { + [evt: string]: CustomEvent; +}, {}, {}, string>; +type Info = InstanceType; +export default Info; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/ScrollFade.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/ScrollFade.svelte.d.ts new file mode 100644 index 0000000..4f7667e --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/ScrollFade.svelte.d.ts @@ -0,0 +1,7 @@ +type $$ComponentProps = { + visible: boolean; + position?: "sticky" | "absolute"; +}; +declare const ScrollFade: import("svelte").Component<$$ComponentProps, {}, "">; +type ScrollFade = ReturnType; +export default ScrollFade; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/SelectSource.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/SelectSource.svelte.d.ts new file mode 100644 index 0000000..dfd9b77 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/SelectSource.svelte.d.ts @@ -0,0 +1,10 @@ +type source_types = "upload" | "microphone" | "webcam" | "clipboard" | "webcam-video" | null; +type $$ComponentProps = { + sources: Partial[]; + active_source?: Partial; + handle_clear?: () => void; + handle_select?: (source_type: Partial) => void; +}; +declare const SelectSource: import("svelte").Component<$$ComponentProps, {}, "active_source">; +type SelectSource = ReturnType; +export default SelectSource; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/ShareButton.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/ShareButton.svelte.d.ts new file mode 100644 index 0000000..9e623fc --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/ShareButton.svelte.d.ts @@ -0,0 +1,27 @@ +import type { ShareData } from "@gradio/utils"; +import type { I18nFormatter } from "@gradio/utils"; +interface $$__sveltets_2_IsomorphicComponent = any, Events extends Record = any, Slots extends Record = any, Exports = {}, Bindings = string> { + new (options: import('svelte').ComponentConstructorOptions): import('svelte').SvelteComponent & { + $$bindings?: Bindings; + } & Exports; + (internal: unknown, props: Props & { + $$events?: Events; + $$slots?: Slots; + }): Exports & { + $set?: any; + $on?: any; + }; + z_$$bindings?: Bindings; +} +declare const ShareButton: $$__sveltets_2_IsomorphicComponent<{ + formatter: (arg0: any) => Promise; + value: any; + i18n: I18nFormatter; +}, { + share: CustomEvent; + error: CustomEvent; +} & { + [evt: string]: CustomEvent; +}, {}, {}, string>; +type ShareButton = InstanceType; +export default ShareButton; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/Toolbar.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/Toolbar.svelte.d.ts new file mode 100644 index 0000000..05d3320 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/Toolbar.svelte.d.ts @@ -0,0 +1,29 @@ +interface $$__sveltets_2_IsomorphicComponent = any, Events extends Record = any, Slots extends Record = any, Exports = {}, Bindings = string> { + new (options: import('svelte').ComponentConstructorOptions): import('svelte').SvelteComponent & { + $$bindings?: Bindings; + } & Exports; + (internal: unknown, props: Props & { + $$events?: Events; + $$slots?: Slots; + }): Exports & { + $set?: any; + $on?: any; + }; + z_$$bindings?: Bindings; +} +type $$__sveltets_2_PropsWithChildren = Props & (Slots extends { + default: any; +} ? Props extends Record ? any : { + children?: any; +} : {}); +declare const Toolbar: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<{ + show_border?: boolean; +}, { + default: {}; +}>, { + [evt: string]: CustomEvent; +}, { + default: {}; +}, {}, string>; +type Toolbar = InstanceType; +export default Toolbar; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/UploadText.svelte.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/UploadText.svelte.d.ts new file mode 100644 index 0000000..1c5a7f7 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/UploadText.svelte.d.ts @@ -0,0 +1,26 @@ +import type { I18nFormatter } from "@gradio/utils"; +interface $$__sveltets_2_IsomorphicComponent = any, Events extends Record = any, Slots extends Record = any, Exports = {}, Bindings = string> { + new (options: import('svelte').ComponentConstructorOptions): import('svelte').SvelteComponent & { + $$bindings?: Bindings; + } & Exports; + (internal: unknown, props: Props & { + $$events?: Events; + $$slots?: Slots; + }): Exports & { + $set?: any; + $on?: any; + }; + z_$$bindings?: Bindings; +} +declare const UploadText: $$__sveltets_2_IsomorphicComponent<{ + type?: "video" | "image" | "audio" | "file" | "csv" | "clipboard" | "gallery"; + i18n: I18nFormatter; + message?: string | undefined; + mode?: "full" | "short"; + hovered?: boolean; + placeholder?: string | undefined; +}, { + [evt: string]: CustomEvent; +}, {}, {}, string>; +type UploadText = InstanceType; +export default UploadText; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/index.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/index.d.ts new file mode 100644 index 0000000..e5b3094 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/index.d.ts @@ -0,0 +1,16 @@ +export { default as Block } from "./Block.svelte"; +export { default as BlockTitle } from "./BlockTitle.svelte"; +export { default as BlockLabel } from "./BlockLabel.svelte"; +export { default as DownloadLink } from "./DownloadLink.svelte"; +export { default as IconButton } from "./IconButton.svelte"; +export { default as Empty } from "./Empty.svelte"; +export { default as Info } from "./Info.svelte"; +export { default as ShareButton } from "./ShareButton.svelte"; +export { default as UploadText } from "./UploadText.svelte"; +export { default as Toolbar } from "./Toolbar.svelte"; +export { default as SelectSource } from "./SelectSource.svelte"; +export { default as IconButtonWrapper } from "./IconButtonWrapper.svelte"; +export { default as FullscreenButton } from "./FullscreenButton.svelte"; +export { default as CustomButton } from "./CustomButton.svelte"; +export { default as ScrollFade } from "./ScrollFade.svelte"; +export declare const BLOCK_KEY: {}; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/inline-markdown.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/inline-markdown.d.ts new file mode 100644 index 0000000..1ba4952 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/inline-markdown.d.ts @@ -0,0 +1,9 @@ +export declare const INLINE_CODE_RE: RegExp; +export declare const LINK_RE: RegExp; +export declare const BOLD_ASTERISK_RE: RegExp; +export declare const BOLD_UNDERSCORE_RE: RegExp; +export declare const ITALIC_ASTERISK_RE: RegExp; +export declare const ITALIC_UNDERSCORE_RE: RegExp; +export declare const PROTOCOL_RE: RegExp; +export declare function escape_html(text: string): string; +export declare function render_inline_markdown(text: string): string; diff --git a/home/runner/work/gradio/gradio/js/atoms/src/utils/parse_placeholder.d.ts b/home/runner/work/gradio/gradio/js/atoms/src/utils/parse_placeholder.d.ts new file mode 100644 index 0000000..fb20ace --- /dev/null +++ b/home/runner/work/gradio/gradio/js/atoms/src/utils/parse_placeholder.d.ts @@ -0,0 +1 @@ +export declare function inject(text: string): [string | false, string | false]; diff --git a/home/runner/work/gradio/gradio/js/audio/shared/types.d.ts b/home/runner/work/gradio/gradio/js/audio/shared/types.d.ts new file mode 100644 index 0000000..b41727f --- /dev/null +++ b/home/runner/work/gradio/gradio/js/audio/shared/types.d.ts @@ -0,0 +1,56 @@ +import { FileData } from "@gradio/client"; +import type { CustomButton } from "@gradio/utils"; +export type WaveformOptions = { + waveform_color?: string; + waveform_progress_color?: string; + show_controls?: boolean; + skip_length?: number; + trim_region_color?: string; + show_recording_waveform?: boolean; + sample_rate?: number; +}; +export interface SubtitleData { + start: number; + end: number; + text: string; +} +export interface AudioProps { + sources: ["microphone"] | ["upload"] | ["microphone", "upload"] | ["upload", "microphone"]; + value: FileData | null; + type: "numpy" | "filepath"; + autoplay: boolean; + buttons: ("play" | "download" | "share" | CustomButton)[]; + recording: boolean; + loop: boolean; + subtitles: FileData | SubtitleData[] | null; + waveform_options: WaveformOptions; + editable: boolean; + pending: boolean; + streaming: boolean; + stream_every: number; + input_ready: boolean; + minimal?: boolean; + playback_position: number; +} +export interface AudioEvents { + change: any; + upload: any; + stream: any; + clear: any; + play: any; + pause: any; + stop: any; + start_recording: any; + pause_recording: any; + stop_recording: any; + input: any; + error: any; + warning: any; + clear_status: any; + close_stream: any; + edit: any; + share: any; + custom_button_click: { + id: number; + }; +} diff --git a/home/runner/work/gradio/gradio/js/core/src/gradio_helper.d.ts b/home/runner/work/gradio/gradio/js/core/src/gradio_helper.d.ts new file mode 100644 index 0000000..c49f20e --- /dev/null +++ b/home/runner/work/gradio/gradio/js/core/src/gradio_helper.d.ts @@ -0,0 +1,4 @@ +export { Gradio } from "@gradio/utils"; +export type I18nFormatter = typeof formatter; +export declare function formatter(value: string | null | undefined): string; +export declare const reactive_formatter: import("svelte/store").Readable; diff --git a/home/runner/work/gradio/gradio/js/statustracker/index.d.ts b/home/runner/work/gradio/gradio/js/statustracker/index.d.ts new file mode 100644 index 0000000..bdccc9e --- /dev/null +++ b/home/runner/work/gradio/gradio/js/statustracker/index.d.ts @@ -0,0 +1,7 @@ +export { default as StatusTracker } from "./static/index.svelte"; +export { default as Toast } from "./static/Toast.svelte"; +export { default as Loader } from "./static/Loader.svelte"; +export { default as StreamingBar } from "./static/StreamingBar.svelte"; +export type * from "./static/types"; +export { default } from "./static/index.svelte"; +export { LoadingStatus } from "./static/state.svelte.js"; diff --git a/home/runner/work/gradio/gradio/js/statustracker/static/Loader.svelte.d.ts b/home/runner/work/gradio/gradio/js/statustracker/static/Loader.svelte.d.ts new file mode 100644 index 0000000..9325cba --- /dev/null +++ b/home/runner/work/gradio/gradio/js/statustracker/static/Loader.svelte.d.ts @@ -0,0 +1,6 @@ +interface Props { + margin?: boolean; +} +declare const Loader: import("svelte").Component; +type Loader = ReturnType; +export default Loader; diff --git a/home/runner/work/gradio/gradio/js/statustracker/static/StreamingBar.svelte.d.ts b/home/runner/work/gradio/gradio/js/statustracker/static/StreamingBar.svelte.d.ts new file mode 100644 index 0000000..7cfe1ab --- /dev/null +++ b/home/runner/work/gradio/gradio/js/statustracker/static/StreamingBar.svelte.d.ts @@ -0,0 +1,6 @@ +interface Props { + time_limit: number | null; +} +declare const StreamingBar: import("svelte").Component; +type StreamingBar = ReturnType; +export default StreamingBar; diff --git a/home/runner/work/gradio/gradio/js/statustracker/static/Toast.svelte.d.ts b/home/runner/work/gradio/gradio/js/statustracker/static/Toast.svelte.d.ts new file mode 100644 index 0000000..3e1d118 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/statustracker/static/Toast.svelte.d.ts @@ -0,0 +1,8 @@ +import type { ToastMessage } from "./types"; +interface Props { + messages?: ToastMessage[]; + on_close: (id: number) => void; +} +declare const Toast: import("svelte").Component; +type Toast = ReturnType; +export default Toast; diff --git a/home/runner/work/gradio/gradio/js/statustracker/static/ToastContent.svelte.d.ts b/home/runner/work/gradio/gradio/js/statustracker/static/ToastContent.svelte.d.ts new file mode 100644 index 0000000..1a1ec67 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/statustracker/static/ToastContent.svelte.d.ts @@ -0,0 +1,11 @@ +import type { ToastMessage } from "./types"; +interface Props { + type: ToastMessage["type"]; + messages?: ToastMessage[]; + expanded?: boolean; + ontoggle?: () => void; + onclose?: (id: number) => void; +} +declare const ToastContent: import("svelte").Component; +type ToastContent = ReturnType; +export default ToastContent; diff --git a/home/runner/work/gradio/gradio/js/statustracker/static/index.svelte.d.ts b/home/runner/work/gradio/gradio/js/statustracker/static/index.svelte.d.ts new file mode 100644 index 0000000..168b9a4 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/statustracker/static/index.svelte.d.ts @@ -0,0 +1,45 @@ +import type { LoadingStatus } from "./types"; +import type { I18nFormatter } from "@gradio/utils"; +interface Props { + i18n: I18nFormatter; + eta?: number | null; + queue_position: number | null; + queue_size: number | null; + status: "complete" | "pending" | "error" | "generating" | "streaming" | null; + scroll_to_output?: boolean; + timer?: boolean; + show_progress?: "full" | "minimal" | "hidden"; + message?: string | null; + progress?: LoadingStatus["progress"] | null | undefined; + variant?: "default" | "center"; + loading_text?: string; + absolute?: boolean; + translucent?: boolean; + border?: boolean; + autoscroll: boolean; + validation_error?: string | null; + show_validation_error?: boolean; + type?: "input" | "output" | null; + on_clear_status?: () => void; +} +interface $$__sveltets_2_IsomorphicComponent = any, Events extends Record = any, Slots extends Record = any, Exports = {}, Bindings = string> { + new (options: import('svelte').ComponentConstructorOptions): import('svelte').SvelteComponent & { + $$bindings?: Bindings; + } & Exports; + (internal: unknown, props: Props & { + $$events?: Events; + $$slots?: Slots; + }): Exports & { + $set?: any; + $on?: any; + }; + z_$$bindings?: Bindings; +} +declare const Index: $$__sveltets_2_IsomorphicComponent; +}, { + 'additional-loading-text': {}; + error: {}; +}, {}, "">; +type Index = InstanceType; +export default Index; diff --git a/home/runner/work/gradio/gradio/js/statustracker/static/state.svelte.d.ts b/home/runner/work/gradio/gradio/js/statustracker/static/state.svelte.d.ts new file mode 100644 index 0000000..1309737 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/statustracker/static/state.svelte.d.ts @@ -0,0 +1,31 @@ +import type { ILoadingStatus, LoadingStatusArgs } from "./types.js"; +export declare class LoadingStatus { + current: Record; + fn_outputs: Record; + fn_inputs: Record; + pending_outputs: Map; + fn_status: Record; + show_progress: Record; + register(dependency_id: number, outputs: number[], inputs: number[], show_progress: "full" | "minimal" | "hidden"): void; + clear(id: number): void; + update(args: LoadingStatusArgs): void; + set_status(id: number, status: ILoadingStatus["status"]): void; + resolve_args(args: LoadingStatusArgs): { + id: number; + queue_position: number | null; + queue_size: number | undefined; + eta: number | null; + status: "pending" | "error" | "complete" | "generating" | "streaming"; + message: string | null; + progress: { + progress: number | null; + index: number | null; + length: number | null; + unit: string | null; + desc: string | null; + }[] | null; + stream_state: "open" | "closed" | "waiting" | null; + time_limit: number | null; + type: "input" | "output" | "skip"; + }[]; +} diff --git a/home/runner/work/gradio/gradio/js/statustracker/static/types.d.ts b/home/runner/work/gradio/gradio/js/statustracker/static/types.d.ts new file mode 100644 index 0000000..7759ab4 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/statustracker/static/types.d.ts @@ -0,0 +1,50 @@ +export interface ILoadingStatus { + eta: number | null; + status: "pending" | "error" | "complete" | "generating" | "streaming"; + queue: boolean; + queue_position: number | null; + queue_size?: number; + fn_index: number; + message?: string | null; + scroll_to_output?: boolean; + show_progress?: "full" | "minimal" | "hidden"; + time_limit?: number | null | undefined; + progress?: { + progress: number | null; + index: number | null; + length: number | null; + unit: string | null; + desc: string | null; + }[]; + validation_error?: string | null; + type: "input" | "output"; + stream_state: "open" | "closed" | "waiting" | null; +} +export interface LoadingStatusArgs { + fn_index: ILoadingStatus["fn_index"]; + status: ILoadingStatus["status"]; + queue?: ILoadingStatus["queue"]; + size?: ILoadingStatus["queue_size"]; + position?: ILoadingStatus["queue_position"]; + eta?: ILoadingStatus["eta"]; + message?: ILoadingStatus["message"]; + progress_data?: ILoadingStatus["progress"]; + time_limit?: ILoadingStatus["time_limit"]; + type?: "input" | "output"; + stream_state: "open" | "closed" | "waiting" | null; + validation_error?: string; + show_validation_error?: boolean; +} +export interface ToastMessage { + type: "error" | "warning" | "info" | "success"; + title: string; + message: string; + id: number; + duration: number | null; + visible: boolean; +} +export interface GroupedToastMessage { + type: "error" | "warning" | "info" | "success"; + messages: ToastMessage[]; + expanded: boolean; +} diff --git a/home/runner/work/gradio/gradio/js/statustracker/static/utils.d.ts b/home/runner/work/gradio/gradio/js/statustracker/static/utils.d.ts new file mode 100644 index 0000000..ad38c46 --- /dev/null +++ b/home/runner/work/gradio/gradio/js/statustracker/static/utils.d.ts @@ -0,0 +1 @@ +export declare function pretty_si(num: number): string; diff --git a/js/.npmrc b/js/.npmrc new file mode 100644 index 0000000..fa4e095 --- /dev/null +++ b/js/.npmrc @@ -0,0 +1 @@ +strict-peer-dependencies=false \ No newline at end of file diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..4b85de9 --- /dev/null +++ b/js/README.md @@ -0,0 +1,108 @@ +# gradio-ui + +This folder contains all of the Gradio UI and component source code. + +- [set up](#setup) +- [running the application](#running-the-application) +- [local development](#local-development) +- [building for production](#building-for-production) +- [quality checks](#quality-checks) +- [ci checks](#ci-checks) + +## setup + +This folder is managed as 'monorepo' a multi-package repository which make dependency management very simple. In order to do this we use `pnpm` as our package manager. + +Make sure [`pnpm`](https://pnpm.io/) is installed by [following the installation instructions for your system](https://pnpm.io/installation). + +You will also need `node` which you probably already have + +## running the application + +Install all dependencies: + +```bash +pnpm i +``` + +This will install the dependencies for all packages and link any local packages + +## local development + +To develop locally, open two terminal tabs from the root of the repository. + +Run the python test server, from the root directory: + +```bash +cd demo/kitchen_sink +python run.py +``` + +This will start a development server on port `7860` that the web app is expecting. + +Run the web app: + +```bash +pnpm dev +``` + +## building for production + +Run the build: + +```bash +pnpm build +``` + +This will create the necessary files in `js/app/public` and also in `gradio/templates/frontend`. + +## quality checks + +The repos currently has two quality checks that can be run locally and are run in CI. + +### formatting + +Formatting is handled by [`prettier`](https://prettier.io/) to ensure consistent formatting and prevent style-focused conversations. Formatting failures will fails CI and should be reoslve before merging. + +To check formatting: + +```bash +pnpm format:check +``` + +If you have formatting failures then you can run the following command to fix them: + +```bash +pnpm format:write +``` + +### type checking + +We use [TypeScript](https://www.typescriptlang.org/) to provide static types to javascript code. These checks are also run in CI. + +to typecheck the code: + +```bash +pnpm ts:check +``` + +## ci checks + +Currently the following checks are run in CI: + +### static checks + +- Format check (`pnpm format:check`) +- Build css (`pnpm css`) +- Build client (`pnpm build`) +- Type check (`pnpm ts:check`) +- Unit tests (`pnpm test:run`) + +### functional test + +``` +pnpm exec playwright install chromium firefox +pnpm exec playwright install-deps chromium firefox +pnpm --filter @gradio/utils --filter @gradio/theme package +pnpm test:browser:full +``` diff --git a/js/_cdn-test/favicon.svg b/js/_cdn-test/favicon.svg new file mode 100644 index 0000000..de4aedd --- /dev/null +++ b/js/_cdn-test/favicon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/js/_cdn-test/index.html b/js/_cdn-test/index.html new file mode 100644 index 0000000..21dfe14 --- /dev/null +++ b/js/_cdn-test/index.html @@ -0,0 +1,320 @@ + + + + + + + + + + + + + Vite App + + + + + + + + + + + + + + + + + diff --git a/js/_cdn-test/package.json b/js/_cdn-test/package.json new file mode 100644 index 0000000..730e771 --- /dev/null +++ b/js/_cdn-test/package.json @@ -0,0 +1,11 @@ +{ + "name": "@self/cdn-test", + "private": true, + "version": "0.0.1", + "scripts": { + "dev": "vite --port 3001", + "build": "vite build", + "preview": "vite preview --port 3001" + }, + "devDependencies": {} +} diff --git a/js/_cdn-test/style.css b/js/_cdn-test/style.css new file mode 100644 index 0000000..5ad8c6b --- /dev/null +++ b/js/_cdn-test/style.css @@ -0,0 +1,8 @@ +#app { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + margin-top: 60px; + color: #2c3e50; + font-family: Avenir, Helvetica, Arial, sans-serif; + text-align: center; +} diff --git a/js/_spaces-test/.gitignore b/js/_spaces-test/.gitignore new file mode 100644 index 0000000..6635cf5 --- /dev/null +++ b/js/_spaces-test/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/js/_spaces-test/.npmrc b/js/_spaces-test/.npmrc new file mode 100644 index 0000000..0c05da4 --- /dev/null +++ b/js/_spaces-test/.npmrc @@ -0,0 +1,2 @@ +engine-strict=true +resolution-mode=highest diff --git a/js/_spaces-test/.prettierignore b/js/_spaces-test/.prettierignore new file mode 100644 index 0000000..3897265 --- /dev/null +++ b/js/_spaces-test/.prettierignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/js/_spaces-test/CHANGELOG.md b/js/_spaces-test/CHANGELOG.md new file mode 100644 index 0000000..7842790 --- /dev/null +++ b/js/_spaces-test/CHANGELOG.md @@ -0,0 +1,441 @@ +# @self/spaces-test + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.3.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.3.0 +- @gradio/theme@0.6.2 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.2.2 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.2.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.2.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.1.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.0.4 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.0.3 +- @gradio/theme@0.6.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.0.2 +- @gradio/theme@0.6.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.0.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.0.0-dev.2 + +## 0.0.1 + +### Dependency updates + +- @gradio/theme@0.5.0-dev.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.0.0-dev.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@2.0.0-dev.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.19.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.19.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.18.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.17.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.17.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.16.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.15.7 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.15.6 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.15.5 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.15.4 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.15.3 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.15.2 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.15.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.15.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.14.2 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.14.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.14.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.13.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.13.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/theme@0.4.0 +- @gradio/client@1.12.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.11.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.10.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.9.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.8.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.7.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.7.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.6.0-beta.4 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.6.0-beta.3 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.6.0-beta.2 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.6.0-beta.1 +- @gradio/theme@0.3.0-beta.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.5.1 +- @gradio/theme@0.2.4 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.5.0 +- @gradio/form@0.1.23 + +## 0.0.1 + +### Dependency updates + +- @gradio/form@0.1.22 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.4.0 +- @gradio/form@0.1.21 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.3.0 +- @gradio/form@0.1.20 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.2.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.2.0 +- @gradio/form@0.1.19 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.1.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.1.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@1.0.0 +- @gradio/form@0.1.18 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@0.20.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@0.20.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/theme@0.2.3 +- @gradio/client@0.19.4 +- @gradio/form@0.1.18 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@0.19.3 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@0.19.2 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@0.19.1 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@0.19.0 +- @gradio/form@0.1.17 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@0.18.0 +- @gradio/form@0.1.16 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@0.17.0 +- @gradio/form@0.1.15 + +## 0.0.1 + +### Dependency updates + +- @gradio/theme@0.2.2 +- @gradio/client@0.16.0 +- @gradio/form@0.1.14 + +## 0.0.1 + +### Dependency updates + +- @gradio/theme@0.2.1 +- @gradio/client@0.15.1 +- @gradio/form@0.1.13 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@0.15.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/form@0.1.12 + +## 0.0.1 + +### Dependency updates + +- @gradio/form@0.1.11 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@0.14.0 + +## 0.0.1 + +### Dependency updates + +- @gradio/client@0.13.0 \ No newline at end of file diff --git a/js/_spaces-test/README.md b/js/_spaces-test/README.md new file mode 100644 index 0000000..5c91169 --- /dev/null +++ b/js/_spaces-test/README.md @@ -0,0 +1,38 @@ +# create-svelte + +Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npm create svelte@latest + +# create a new project in my-app +npm create svelte@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/js/_spaces-test/package.json b/js/_spaces-test/package.json new file mode 100644 index 0000000..7fd44a1 --- /dev/null +++ b/js/_spaces-test/package.json @@ -0,0 +1,27 @@ +{ + "name": "@self/spaces-test", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch", + "lint": "prettier --plugin-search-dir . --check .", + "format": "prettier --plugin-search-dir . --write ." + }, + "type": "module", + "dependencies": { + "@gradio/client": "workspace:^", + "@gradio/form": "workspace:^", + "@gradio/theme": "workspace:^", + "@self/spa": "workspace:^", + "@sveltejs/adapter-auto": "^6.1.1", + "@sveltejs/kit": "^2.46.2", + "iframe-resizer": "^5.5.7" + }, + "devDependencies": { + "@types/iframe-resizer": "^4.0.0" + } +} diff --git a/js/_spaces-test/src/app.d.ts b/js/_spaces-test/src/app.d.ts new file mode 100644 index 0000000..f59b884 --- /dev/null +++ b/js/_spaces-test/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/js/_spaces-test/src/app.html b/js/_spaces-test/src/app.html new file mode 100644 index 0000000..77ec85d --- /dev/null +++ b/js/_spaces-test/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/js/_spaces-test/src/lib/EndpointInputs.svelte b/js/_spaces-test/src/lib/EndpointInputs.svelte new file mode 100644 index 0000000..6b0f509 --- /dev/null +++ b/js/_spaces-test/src/lib/EndpointInputs.svelte @@ -0,0 +1,86 @@ + + +

Request Inputs

+ +{#each app_info as { type, label, component }, i} + {#if type === "string"} + + {:else if type === "number"} + + {:else if type === "boolean"} + + {:else if type === "number"} + + {:else if type === "string[]"} + + {:else if ["Image", "Audio", "Video"].includes(component)} + + {/if} +{/each} + + diff --git a/js/_spaces-test/src/lib/ResponsePreview.svelte b/js/_spaces-test/src/lib/ResponsePreview.svelte new file mode 100644 index 0000000..61114c7 --- /dev/null +++ b/js/_spaces-test/src/lib/ResponsePreview.svelte @@ -0,0 +1,90 @@ + + +
+
+

Response Outputs

+ {#if status === "pending" || status === "generating"} + + {:else if status === "error"} + + {:else if status === "complete"} + + {/if} +
+ {#each app_info as { type, label, component }, i} + {#if type === "string"} + + {:else if type === "number"} + + {:else if type === "boolean"} + + {:else if type === "number"} + + {:else if type === "string[]"} + + {/if} + {/each} + +

JSON

+
{JSON.stringify(
+				response_data.data.length ? response_data : {},
+				null,
+				2
+			)}
+
+ + diff --git a/js/_spaces-test/src/lib/Spinner.svelte b/js/_spaces-test/src/lib/Spinner.svelte new file mode 100644 index 0000000..3c402f3 --- /dev/null +++ b/js/_spaces-test/src/lib/Spinner.svelte @@ -0,0 +1,43 @@ + + + diff --git a/js/_spaces-test/src/lib/Success.svelte b/js/_spaces-test/src/lib/Success.svelte new file mode 100644 index 0000000..abe7c7f --- /dev/null +++ b/js/_spaces-test/src/lib/Success.svelte @@ -0,0 +1,27 @@ + + + diff --git a/js/_spaces-test/src/lib/Warning.svelte b/js/_spaces-test/src/lib/Warning.svelte new file mode 100644 index 0000000..41f8866 --- /dev/null +++ b/js/_spaces-test/src/lib/Warning.svelte @@ -0,0 +1,30 @@ + + + diff --git a/js/_spaces-test/src/lib/theme.css b/js/_spaces-test/src/lib/theme.css new file mode 100644 index 0000000..0dda9e1 --- /dev/null +++ b/js/_spaces-test/src/lib/theme.css @@ -0,0 +1,508 @@ +:root { + --name: default; + --primary-50: #fff7ed; + --primary-100: #ffedd5; + --primary-200: #fed7aa; + --primary-300: #fdba74; + --primary-400: #fb923c; + --primary-500: #f97316; + --primary-600: #ea580c; + --primary-700: #c2410c; + --primary-800: #9a3412; + --primary-900: #7c2d12; + --primary-950: #6c2e12; + --secondary-50: #eff6ff; + --secondary-100: #dbeafe; + --secondary-200: #bfdbfe; + --secondary-300: #93c5fd; + --secondary-400: #60a5fa; + --secondary-500: #3b82f6; + --secondary-600: #2563eb; + --secondary-700: #1d4ed8; + --secondary-800: #1e40af; + --secondary-900: #1e3a8a; + --secondary-950: #1d3660; + --neutral-50: #f9fafb; + --neutral-100: #f3f4f6; + --neutral-200: #e5e7eb; + --neutral-300: #d1d5db; + --neutral-400: #9ca3af; + --neutral-500: #6b7280; + --neutral-600: #4b5563; + --neutral-700: #374151; + --neutral-800: #1f2937; + --neutral-900: #111827; + --neutral-950: #0b0f19; + --spacing-xxs: 1px; + --spacing-xs: 2px; + --spacing-sm: 4px; + --spacing-md: 6px; + --spacing-lg: 8px; + --spacing-xl: 10px; + --spacing-xxl: 16px; + --radius-xxs: 1px; + --radius-xs: 2px; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --radius-xxl: 22px; + --text-xxs: 9px; + --text-xs: 10px; + --text-sm: 12px; + --text-md: 14px; + --text-lg: 16px; + --text-xl: 22px; + --text-xxl: 26px; + --font: "IBM Plex Sans", "ui-sans-serif", "system-ui", sans-serif; + --font-mono: "IBM Plex Mono", "ui-monospace", "Consolas", monospace; + --body-background-fill: var(--background-fill-primary); + --body-text-color: var(--neutral-800); + --body-text-size: var(--text-md); + --body-text-weight: 400; + --embed-radius: var(--radius-lg); + --color-accent: var(--primary-500); + --color-accent-soft: var(--primary-50); + --background-fill-primary: white; + --background-fill-secondary: var(--neutral-50); + --border-color-accent: var(--primary-300); + --border-color-primary: var(--neutral-200); + --link-text-color: var(--secondary-600); + --link-text-color-active: var(--secondary-600); + --link-text-color-hover: var(--secondary-700); + --link-text-color-visited: var(--secondary-500); + --body-text-color-subdued: var(--neutral-400); + --accordion-text-color: var(--body-text-color); + --table-text-color: var(--body-text-color); + --shadow-drop: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px; + --shadow-drop-lg: + 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-inset: rgba(0, 0, 0, 0.05) 0px 2px 4px 0px inset; + --shadow-spread: 3px; + --block-background-fill: var(--background-fill-primary); + --block-border-color: var(--border-color-primary); + --block-border-width: 1px; + --block-info-text-color: var(--body-text-color-subdued); + --block-info-text-size: var(--text-sm); + --block-info-text-weight: 400; + --block-label-background-fill: var(--background-fill-primary); + --block-label-border-color: var(--border-color-primary); + --block-label-border-width: 1px; + --block-label-shadow: var(--block-shadow); + --block-label-text-color: var(--neutral-500); + --block-label-margin: 0; + --block-label-padding: var(--spacing-sm) var(--spacing-lg); + --block-label-radius: calc(var(--radius-lg) - 1px) 0 + calc(var(--radius-lg) - 1px) 0; + --block-label-right-radius: 0 calc(var(--radius-lg) - 1px) 0 + calc(var(--radius-lg) - 1px); + --block-label-text-size: var(--text-sm); + --block-label-text-weight: 400; + --block-padding: var(--spacing-xl) calc(var(--spacing-xl) + 2px); + --block-radius: var(--radius-lg); + --block-shadow: var(--shadow-drop); + --block-title-background-fill: none; + --block-title-border-color: none; + --block-title-border-width: 0px; + --block-title-text-color: var(--neutral-500); + --block-title-padding: 0; + --block-title-radius: none; + --block-title-text-size: var(--text-md); + --block-title-text-weight: 400; + --container-radius: var(--radius-lg); + --form-gap-width: 1px; + --layout-gap: var(--spacing-xxl); + --panel-background-fill: var(--background-fill-secondary); + --panel-border-color: var(--border-color-primary); + --panel-border-width: 0; + --section-header-text-size: var(--text-md); + --section-header-text-weight: 400; + --border-color-accent-subdued: var(--primary-200); + --code-background-fill: var(--neutral-100); + --checkbox-background-color: var(--background-fill-primary); + --checkbox-background-color-focus: var(--checkbox-background-color); + --checkbox-background-color-hover: var(--checkbox-background-color); + --checkbox-background-color-selected: var(--secondary-600); + --checkbox-border-color: var(--neutral-300); + --checkbox-border-color-focus: var(--secondary-500); + --checkbox-border-color-hover: var(--neutral-300); + --checkbox-border-color-selected: var(--secondary-600); + --checkbox-border-radius: var(--radius-sm); + --checkbox-border-width: var(--input-border-width); + --checkbox-label-background-fill: linear-gradient( + to top, + var(--neutral-50), + white + ); + --checkbox-label-background-fill-hover: linear-gradient( + to top, + var(--neutral-100), + white + ); + --checkbox-label-background-fill-selected: var( + --checkbox-label-background-fill + ); + --checkbox-label-border-color: var(--border-color-primary); + --checkbox-label-border-color-hover: var(--checkbox-label-border-color); + --checkbox-label-border-width: var(--input-border-width); + --checkbox-label-gap: var(--spacing-lg); + --checkbox-label-padding: var(--spacing-md) calc(2 * var(--spacing-md)); + --checkbox-label-shadow: var(--shadow-drop); + --checkbox-label-text-size: var(--text-md); + --checkbox-label-text-weight: 400; + --checkbox-check: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); + --radio-circle: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); + --checkbox-shadow: var(--input-shadow); + --checkbox-label-text-color: var(--body-text-color); + --checkbox-label-text-color-selected: var(--checkbox-label-text-color); + --error-background-fill: #fef2f2; + --error-border-color: #b91c1c; + --error-border-width: 1px; + --error-text-color: #b91c1c; + --error-icon-color: #b91c1c; + --input-background-fill: white; + --input-background-fill-focus: var(--secondary-500); + --input-background-fill-hover: var(--input-background-fill); + --input-border-color: var(--border-color-primary); + --input-border-color-focus: var(--secondary-300); + --input-border-color-hover: var(--input-border-color); + --input-border-width: 1px; + --input-padding: var(--spacing-xl); + --input-placeholder-color: var(--neutral-400); + --input-radius: var(--radius-lg); + --input-shadow: 0 0 0 var(--shadow-spread) transparent, var(--shadow-inset); + --input-shadow-focus: + 0 0 0 var(--shadow-spread) var(--secondary-50), var(--shadow-inset); + --input-text-size: var(--text-md); + --input-text-weight: 400; + --loader-color: var(--color-accent); + --prose-text-size: var(--text-md); + --prose-text-weight: 400; + --prose-header-text-weight: 600; + --slider-color: #2563eb; + --stat-background-fill: linear-gradient( + to right, + var(--primary-400), + var(--primary-200) + ); + --table-border-color: var(--neutral-300); + --table-even-background-fill: white; + --table-odd-background-fill: var(--neutral-50); + --table-radius: var(--radius-lg); + --table-row-focus: var(--color-accent-soft); + --button-border-width: var(--input-border-width); + --button-cancel-background-fill: linear-gradient( + to bottom right, + #fee2e2, + #fecaca + ); + --button-cancel-background-fill-hover: linear-gradient( + to bottom right, + #fee2e2, + #fee2e2 + ); + --button-cancel-border-color: #fecaca; + --button-cancel-border-color-hover: var(--button-cancel-border-color); + --button-cancel-text-color: #dc2626; + --button-cancel-text-color-hover: var(--button-cancel-text-color); + --button-large-padding: var(--spacing-lg) calc(2 * var(--spacing-lg)); + --button-large-radius: var(--radius-lg); + --button-large-text-size: var(--text-lg); + --button-large-text-weight: 600; + --button-primary-background-fill: linear-gradient( + to bottom right, + var(--primary-100), + var(--primary-300) + ); + --button-primary-background-fill-hover: linear-gradient( + to bottom right, + var(--primary-100), + var(--primary-200) + ); + --button-primary-border-color: var(--primary-200); + --button-primary-border-color-hover: var(--button-primary-border-color); + --button-primary-text-color: var(--primary-600); + --button-primary-text-color-hover: var(--button-primary-text-color); + --button-secondary-background-fill: linear-gradient( + to bottom right, + var(--neutral-100), + var(--neutral-200) + ); + --button-secondary-background-fill-hover: linear-gradient( + to bottom right, + var(--neutral-100), + var(--neutral-100) + ); + --button-secondary-border-color: var(--neutral-200); + --button-secondary-border-color-hover: var(--button-secondary-border-color); + --button-secondary-text-color: var(--neutral-700); + --button-secondary-text-color-hover: var(--button-secondary-text-color); + --button-shadow: var(--shadow-drop); + --button-shadow-active: var(--shadow-inset); + --button-shadow-hover: var(--shadow-drop-lg); + --button-small-padding: var(--spacing-sm) calc(2 * var(--spacing-sm)); + --button-small-radius: var(--radius-lg); + --button-small-text-size: var(--text-md); + --button-small-text-weight: 400; + --button-transition: none; +} +.dark { + --body-background-fill: var(--background-fill-primary); + --body-text-color: var(--neutral-100); + --color-accent-soft: var(--neutral-700); + --background-fill-primary: var(--neutral-950); + --background-fill-secondary: var(--neutral-900); + --border-color-accent: var(--neutral-600); + --border-color-primary: var(--neutral-700); + --link-text-color-active: var(--secondary-500); + --link-text-color: var(--secondary-500); + --link-text-color-hover: var(--secondary-400); + --link-text-color-visited: var(--secondary-600); + --body-text-color-subdued: var(--neutral-400); + --accordion-text-color: var(--body-text-color); + --table-text-color: var(--body-text-color); + --shadow-spread: 1px; + --block-background-fill: var(--neutral-800); + --block-border-color: var(--border-color-primary); + --block_border_width: None; + --block-info-text-color: var(--body-text-color-subdued); + --block-label-background-fill: var(--background-fill-secondary); + --block-label-border-color: var(--border-color-primary); + --block_label_border_width: None; + --block-label-text-color: var(--neutral-200); + --block_shadow: None; + --block_title_background_fill: None; + --block_title_border_color: None; + --block_title_border_width: None; + --block-title-text-color: var(--neutral-200); + --panel-background-fill: var(--background-fill-secondary); + --panel-border-color: var(--border-color-primary); + --panel_border_width: None; + --border-color-accent-subdued: var(--border-color-accent); + --code-background-fill: var(--neutral-800); + --checkbox-background-color: var(--neutral-800); + --checkbox-background-color-focus: var(--checkbox-background-color); + --checkbox-background-color-hover: var(--checkbox-background-color); + --checkbox-background-color-selected: var(--secondary-600); + --checkbox-border-color: var(--neutral-700); + --checkbox-border-color-focus: var(--secondary-500); + --checkbox-border-color-hover: var(--neutral-600); + --checkbox-border-color-selected: var(--secondary-600); + --checkbox-border-width: var(--input-border-width); + --checkbox-label-background-fill: linear-gradient( + to top, + var(--neutral-900), + var(--neutral-800) + ); + --checkbox-label-background-fill-hover: linear-gradient( + to top, + var(--neutral-900), + var(--neutral-800) + ); + --checkbox-label-background-fill-selected: var( + --checkbox-label-background-fill + ); + --checkbox-label-border-color: var(--border-color-primary); + --checkbox-label-border-color-hover: var(--checkbox-label-border-color); + --checkbox-label-border-width: var(--input-border-width); + --checkbox-label-text-color: var(--body-text-color); + --checkbox-label-text-color-selected: var(--checkbox-label-text-color); + --error-background-fill: var(--neutral-900); + --error-border-color: #ef4444; + --error_border_width: None; + --error-text-color: #fef2f2; + --error-icon-color: #ef4444; + --input-background-fill: var(--neutral-800); + --input-background-fill-focus: var(--secondary-600); + --input-background-fill-hover: var(--input-background-fill); + --input-border-color: var(--border-color-primary); + --input-border-color-focus: var(--neutral-700); + --input-border-color-hover: var(--input-border-color); + --input_border_width: None; + --input-placeholder-color: var(--neutral-500); + --input_shadow: None; + --input-shadow-focus: + 0 0 0 var(--shadow-spread) var(--neutral-700), var(--shadow-inset); + --loader_color: None; + --slider_color: None; + --stat-background-fill: linear-gradient( + to right, + var(--primary-400), + var(--primary-600) + ); + --table-border-color: var(--neutral-700); + --table-even-background-fill: var(--neutral-950); + --table-odd-background-fill: var(--neutral-900); + --table-row-focus: var(--color-accent-soft); + --button-border-width: var(--input-border-width); + --button-cancel-background-fill: linear-gradient( + to bottom right, + #dc2626, + #b91c1c + ); + --button-cancel-background-fill-hover: linear-gradient( + to bottom right, + #dc2626, + #dc2626 + ); + --button-cancel-border-color: #dc2626; + --button-cancel-border-color-hover: var(--button-cancel-border-color); + --button-cancel-text-color: white; + --button-cancel-text-color-hover: var(--button-cancel-text-color); + --button-primary-background-fill: linear-gradient( + to bottom right, + var(--primary-500), + var(--primary-600) + ); + --button-primary-background-fill-hover: linear-gradient( + to bottom right, + var(--primary-500), + var(--primary-500) + ); + --button-primary-border-color: var(--primary-500); + --button-primary-border-color-hover: var(--button-primary-border-color); + --button-primary-text-color: white; + --button-primary-text-color-hover: var(--button-primary-text-color); + --button-secondary-background-fill: linear-gradient( + to bottom right, + var(--neutral-600), + var(--neutral-700) + ); + --button-secondary-background-fill-hover: linear-gradient( + to bottom right, + var(--neutral-600), + var(--neutral-600) + ); + --button-secondary-border-color: var(--neutral-600); + --button-secondary-border-color-hover: var(--button-secondary-border-color); + --button-secondary-text-color: white; + --button-secondary-text-color-hover: var(--button-secondary-text-color); + --name: default; + --primary-50: #fff7ed; + --primary-100: #ffedd5; + --primary-200: #fed7aa; + --primary-300: #fdba74; + --primary-400: #fb923c; + --primary-500: #f97316; + --primary-600: #ea580c; + --primary-700: #c2410c; + --primary-800: #9a3412; + --primary-900: #7c2d12; + --primary-950: #6c2e12; + --secondary-50: #eff6ff; + --secondary-100: #dbeafe; + --secondary-200: #bfdbfe; + --secondary-300: #93c5fd; + --secondary-400: #60a5fa; + --secondary-500: #3b82f6; + --secondary-600: #2563eb; + --secondary-700: #1d4ed8; + --secondary-800: #1e40af; + --secondary-900: #1e3a8a; + --secondary-950: #1d3660; + --neutral-50: #f9fafb; + --neutral-100: #f3f4f6; + --neutral-200: #e5e7eb; + --neutral-300: #d1d5db; + --neutral-400: #9ca3af; + --neutral-500: #6b7280; + --neutral-600: #4b5563; + --neutral-700: #374151; + --neutral-800: #1f2937; + --neutral-900: #111827; + --neutral-950: #0b0f19; + --spacing-xxs: 1px; + --spacing-xs: 2px; + --spacing-sm: 4px; + --spacing-md: 6px; + --spacing-lg: 8px; + --spacing-xl: 10px; + --spacing-xxl: 16px; + --radius-xxs: 1px; + --radius-xs: 2px; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --radius-xxl: 22px; + --text-xxs: 9px; + --text-xs: 10px; + --text-sm: 12px; + --text-md: 14px; + --text-lg: 16px; + --text-xl: 22px; + --text-xxl: 26px; + --font: "IBM Plex Sans", "ui-sans-serif", "system-ui", sans-serif; + --font-mono: "IBM Plex Mono", "ui-monospace", "Consolas", monospace; + --body-text-size: var(--text-md); + --body-text-weight: 400; + --embed-radius: var(--radius-lg); + --color-accent: var(--primary-500); + --shadow-drop: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px; + --shadow-drop-lg: + 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-inset: rgba(0, 0, 0, 0.05) 0px 2px 4px 0px inset; + --block-border-width: 1px; + --block-info-text-size: var(--text-sm); + --block-info-text-weight: 400; + --block-label-border-width: 1px; + --block-label-shadow: var(--block-shadow); + --block-label-margin: 0; + --block-label-padding: var(--spacing-sm) var(--spacing-lg); + --block-label-radius: calc(var(--radius-lg) - 1px) 0 + calc(var(--radius-lg) - 1px) 0; + --block-label-right-radius: 0 calc(var(--radius-lg) - 1px) 0 + calc(var(--radius-lg) - 1px); + --block-label-text-size: var(--text-sm); + --block-label-text-weight: 400; + --block-padding: var(--spacing-xl) calc(var(--spacing-xl) + 2px); + --block-radius: var(--radius-lg); + --block-shadow: var(--shadow-drop); + --block-title-background-fill: none; + --block-title-border-color: none; + --block-title-border-width: 0px; + --block-title-padding: 0; + --block-title-radius: none; + --block-title-text-size: var(--text-md); + --block-title-text-weight: 400; + --container-radius: var(--radius-lg); + --form-gap-width: 1px; + --layout-gap: var(--spacing-xxl); + --panel-border-width: 0; + --section-header-text-size: var(--text-md); + --section-header-text-weight: 400; + --checkbox-border-radius: var(--radius-sm); + --checkbox-label-gap: var(--spacing-lg); + --checkbox-label-padding: var(--spacing-md) calc(2 * var(--spacing-md)); + --checkbox-label-shadow: var(--shadow-drop); + --checkbox-label-text-size: var(--text-md); + --checkbox-label-text-weight: 400; + --checkbox-check: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); + --radio-circle: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); + --checkbox-shadow: var(--input-shadow); + --error-border-width: 1px; + --input-border-width: 1px; + --input-padding: var(--spacing-xl); + --input-radius: var(--radius-lg); + --input-shadow: 0 0 0 var(--shadow-spread) transparent, var(--shadow-inset); + --input-text-size: var(--text-md); + --input-text-weight: 400; + --loader-color: var(--color-accent); + --prose-text-size: var(--text-md); + --prose-text-weight: 400; + --prose-header-text-weight: 600; + --slider-color: #2563eb; + --table-radius: var(--radius-lg); + --button-large-padding: var(--spacing-lg) calc(2 * var(--spacing-lg)); + --button-large-radius: var(--radius-lg); + --button-large-text-size: var(--text-lg); + --button-large-text-weight: 600; + --button-shadow: var(--shadow-drop); + --button-shadow-active: var(--shadow-inset); + --button-shadow-hover: var(--shadow-drop-lg); + --button-small-padding: var(--spacing-sm) calc(2 * var(--spacing-sm)); + --button-small-radius: var(--radius-lg); + --button-small-text-size: var(--text-md); + --button-small-text-weight: 400; + --button-transition: none; +} diff --git a/js/_spaces-test/src/routes/+layout.svelte b/js/_spaces-test/src/routes/+layout.svelte new file mode 100644 index 0000000..afe0a7f --- /dev/null +++ b/js/_spaces-test/src/routes/+layout.svelte @@ -0,0 +1,76 @@ + + + + + + + + + +
+
    + {#each links as [url, name]} +
  • {name}
  • + {/each} +
+ +
+ + diff --git a/js/_spaces-test/src/routes/+page.svelte b/js/_spaces-test/src/routes/+page.svelte new file mode 100644 index 0000000..d393e37 --- /dev/null +++ b/js/_spaces-test/src/routes/+page.svelte @@ -0,0 +1 @@ +

Embeds

diff --git a/js/_spaces-test/src/routes/client-browser/+page.svelte b/js/_spaces-test/src/routes/client-browser/+page.svelte new file mode 100644 index 0000000..dd8ab29 --- /dev/null +++ b/js/_spaces-test/src/routes/client-browser/+page.svelte @@ -0,0 +1,299 @@ + + +

Client Browser

+ +

+ Enter a space user-space/name to test the client in a browser environment + with any space. +

+

+ You may optionally provide a hf_token to test a private space +

+ +
+ + + +
+ + + +{#if named.length || unnamed.length} +
+
+

Named endpoints

+ {#if named.length} + {#each named as endpoint} + + {/each} + {:else} +

There are no named endpoints

+ {/if} +
+ +
+

Unnamed endpoints

+ + {#if unnamed.length} + {#each unnamed as endpoint} + + {/each} + {:else} +

There are no unnamed endpoints

+ {/if} +
+
+{/if} + +{#if app_info} +
+

+ This endpoint accepts {app_info.parameters.length + ? app_info.parameters.length + : "no"} piece{app_info.parameters.length < 1 || + app_info.parameters.length > 1 + ? "s" + : ""} of data and returns {app_info.returns.length + ? app_info.returns.length + : "no"} piece{app_info.returns.length < 1 || app_info.returns.length > 1 + ? "s" + : ""} of data. {endpoint_type_text} +

+
+
+
+ + + {#if app_info.type.generator || app_info.type.continuous} + + {/if} +
+
+ +
+
+{/if} + + diff --git a/js/_spaces-test/src/routes/client-node/+page.svelte b/js/_spaces-test/src/routes/client-node/+page.svelte new file mode 100644 index 0000000..690c371 --- /dev/null +++ b/js/_spaces-test/src/routes/client-node/+page.svelte @@ -0,0 +1,3 @@ +

Client Node

+ +

coming soon.

diff --git a/js/_spaces-test/src/routes/embeds/+page.svelte b/js/_spaces-test/src/routes/embeds/+page.svelte new file mode 100644 index 0000000..11d2ddc --- /dev/null +++ b/js/_spaces-test/src/routes/embeds/+page.svelte @@ -0,0 +1,64 @@ + + +
+ +
+ + diff --git a/js/_spaces-test/src/styles.css b/js/_spaces-test/src/styles.css new file mode 100644 index 0000000..e3510a9 --- /dev/null +++ b/js/_spaces-test/src/styles.css @@ -0,0 +1,5 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} diff --git a/js/_spaces-test/static/favicon.png b/js/_spaces-test/static/favicon.png new file mode 100644 index 0000000..825b9e6 Binary files /dev/null and b/js/_spaces-test/static/favicon.png differ diff --git a/js/_spaces-test/svelte.config.js b/js/_spaces-test/svelte.config.js new file mode 100644 index 0000000..9d75973 --- /dev/null +++ b/js/_spaces-test/svelte.config.js @@ -0,0 +1,31 @@ +import adapter from "@sveltejs/adapter-auto"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import { sveltePreprocess } from "svelte-preprocess"; +import global_data from "@csstools/postcss-global-data"; +import custom_media from "postcss-custom-media"; +import { resolve } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); + +const theme_token_path = resolve(__dirname, "../theme/src/tokens.css"); + +// /** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter() + }, + preprocess: [ + vitePreprocess(), + sveltePreprocess({ + postcss: { + plugins: [global_data({ files: [theme_token_path] }), custom_media()] + } + }) + ] +}; + +export default config; diff --git a/js/_spaces-test/vite.config.js b/js/_spaces-test/vite.config.js new file mode 100644 index 0000000..3b099b1 --- /dev/null +++ b/js/_spaces-test/vite.config.js @@ -0,0 +1,134 @@ +import { defineConfig } from "vite"; +import { svelte, vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import { sveltekit } from "@sveltejs/kit/vite"; +import { sveltePreprocess } from "svelte-preprocess"; +// @ts-ignore +import custom_media from "postcss-custom-media"; +import global_data from "@csstools/postcss-global-data"; +// @ts-ignore +import prefixer from "postcss-prefix-selector"; +import { readFileSync } from "fs"; +import { resolve } from "path"; + +const version_path = resolve(__dirname, "../../gradio/package.json"); +const theme_token_path = resolve(__dirname, "../theme/src/tokens.css"); +const version_raw = JSON.parse( + readFileSync(version_path, { encoding: "utf-8" }) +).version.trim(); +const version = version_raw.replace(/\./g, "-"); + +const client_version_path = resolve( + __dirname, + "../../client/python/gradio_client/package.json" +); +const client_version_raw = JSON.parse( + readFileSync(client_version_path, { + encoding: "utf-8" + }) +).version.trim(); + +import { + inject_ejs, + generate_cdn_entry, + generate_dev_entry, + handle_ce_css, + inject_component_loader, + resolve_svelte, + mock_modules +} from "@self/build"; + +// const GRADIO_VERSION = version_raw || "asd_stub_asd"; +// const CDN_BASE = "https://gradio.s3-us-west-2.amazonaws.com"; + +//@ts-ignore +export default defineConfig(({ mode, isSsrBuild }) => { + const production = mode === "production"; + const development = mode === "development"; + return { + // base: "./", + server: { + open: "/" + }, + build: { + sourcemap: true, + target: "esnext", + minify: production + // outDir: "../../gradio/templates/frontend", + // rollupOptions: { + // external: ["./svelte/svelte.js"], + // makeAbsoluteExternalsRelative: false + // } + }, + define: { + BROWSER_BUILD: JSON.stringify(true), + BUILD_MODE: production ? JSON.stringify("prod") : JSON.stringify("dev"), + BACKEND_URL: production + ? JSON.stringify("") + : JSON.stringify("http://localhost:7860/"), + GRADIO_VERSION: JSON.stringify(version) + }, + css: { + postcss: { + plugins: [ + prefixer({ + prefix: `.gradio-container-${version}`, + // @ts-ignore + transform(prefix, selector, prefixedSelector, fileName) { + if (selector.indexOf("gradio-container") > -1) { + return prefix; + } else if ( + selector.indexOf(":root") > -1 || + selector.indexOf("dark") > -1 || + selector.indexOf("body") > -1 || + fileName.indexOf(".svelte") > -1 + ) { + return selector; + } + return prefixedSelector; + } + }), + custom_media() + ] + } + }, + plugins: [ + // resolve_svelte(development), + sveltekit(), + // svelte({ + // inspector: false, + // compilerOptions: { + // dev: true, + // discloseVersion: false, + // accessors: true + // }, + // hot: !process.env.VITEST && !production, + // preprocess: [ + // vitePreprocess(), + // sveltePreprocess({ + // postcss: { + // plugins: [ + // global_data({ files: [theme_token_path] }), + // custom_media() + // ] + // } + // }) + // ] + // }), + resolve_svelte(true), + // generate_dev_entry({ + // enable: !development && mode !== "test" + // }), + // inject_ejs(), + // generate_cdn_entry({ version: GRADIO_VERSION, cdn_base: CDN_BASE }), + // handle_ce_css(), + inject_component_loader({ mode }), + mode === "test" && mock_modules() + ], + optimizeDeps: { + exclude: ["@ffmpeg/ffmpeg", "@ffmpeg/util", "@self/spa", "@self/core"] + }, + resolve: { + conditions: ["gradio"] + } + }; +}); diff --git a/js/_website/.gitignore b/js/_website/.gitignore new file mode 100644 index 0000000..4341872 --- /dev/null +++ b/js/_website/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +src/lib/json/ +.vercel +src/lib/templates_* +src/lib/templates/docs.json + diff --git a/js/_website/.npmrc b/js/_website/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/js/_website/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/js/_website/CHANGELOG.md b/js/_website/CHANGELOG.md new file mode 100644 index 0000000..9d2501e --- /dev/null +++ b/js/_website/CHANGELOG.md @@ -0,0 +1,1818 @@ +# website + +## 0.79.0 + +### Features + +- [#13547](https://github.com/gradio-app/gradio/pull/13547) [`370725b`](https://github.com/gradio-app/gradio/commit/370725bb433b15da0c303be8eb04638dc3100b26) - website build: add fallback to unversioned templates. Thanks @hannahblair! + +### Dependency updates + +- @gradio/tabs@0.9.0 +- @gradio/html@0.13.1 +- @gradio/code@0.18.1 +- @gradio/statustracker@0.15.1 +- @gradio/paramviewer@0.10.1 +- @gradio/button@0.8.1 +- @gradio/tabitem@0.8.2 + +## 0.78.1 + +### Dependency updates + +- @gradio/statustracker@0.15.0 +- @gradio/button@0.8.0 +- @gradio/code@0.18.0 +- @gradio/html@0.13.0 +- @gradio/paramviewer@0.10.0 +- @gradio/tabs@0.8.0 +- @gradio/tabitem@0.8.1 + +## 0.78.0 + +### Features + +- [#13504](https://github.com/gradio-app/gradio/pull/13504) [`a6141ee`](https://github.com/gradio-app/gradio/commit/a6141ee3ce8260a249a6230e18b14fcaa9b2ac4c) - Fix website build checks. Thanks @abidlabs! + +### Dependency updates + +- @gradio/tabs@0.7.0 +- @gradio/button@0.7.0 +- @gradio/tabitem@0.8.0 + +## 0.77.1 + +### Fixes + +- [#13460](https://github.com/gradio-app/gradio/pull/13460) [`980db6c`](https://github.com/gradio-app/gradio/commit/980db6cc29b5e26a5f90334b0f4fd000b7443edc) - Offset guide heading anchors so the sticky header no longer covers the section title when clicking a table-of-contents link. Thanks @ShirGanon! + +### Dependency updates + +- @gradio/tabs@0.6.0 +- @gradio/tabitem@0.7.0 + +## 0.77.0 + +### Features + +- [#13411](https://github.com/gradio-app/gradio/pull/13411) [`51497ae`](https://github.com/gradio-app/gradio/commit/51497ae8e5bab0ff5731d16df4148a024224cdf1) - add MiniMax ChatInterface example to the LLM providers guide. Thanks @octo-patch! +- [#13368](https://github.com/gradio-app/gradio/pull/13368) [`da45b72`](https://github.com/gradio-app/gradio/commit/da45b72da424ad77e55a2ca99375dbda9eb1c5b3) - add embedded workflow to docs. Thanks @hannahblair! + +### Dependency updates + +- @gradio/html@0.12.4 + +## 0.76.0 + +### Features + +- [#13317](https://github.com/gradio-app/gradio/pull/13317) [`34e5b74`](https://github.com/gradio-app/gradio/commit/34e5b746596e53fb9f83cc8d7bfbc40891be714b) - Markdown negotiation. Thanks @pngwn! +- [#13320](https://github.com/gradio-app/gradio/pull/13320) [`fffd4c6`](https://github.com/gradio-app/gradio/commit/fffd4c661f5d3becb864d4b78d93d24eb3c94aab) - Fix website CORS errors. Thanks @pngwn! + +## 0.75.0 + +### Features + +- [#13211](https://github.com/gradio-app/gradio/pull/13211) [`7617801`](https://github.com/gradio-app/gradio/commit/7617801ab602a0e1d937f721d4cf241a6b0026d6) - improve preview and metadata for community themes. Thanks @hannahblair! +- [#13261](https://github.com/gradio-app/gradio/pull/13261) [`3462368`](https://github.com/gradio-app/gradio/commit/34623682aea502f78e116ba2c1e77014fd857cd5) - Add docs for gr.cache and gr.Cache. Thanks @abidlabs! + +### Dependency updates + +- @gradio/statustracker@0.14.1 +- @gradio/tabs@0.5.10 +- @gradio/code@0.17.8 +- @gradio/html@0.12.3 +- @gradio/paramviewer@0.9.9 + +## 0.74.1 + +### Dependency updates + +- @gradio/statustracker@0.14.0 +- @gradio/code@0.17.7 +- @gradio/html@0.12.2 +- @gradio/paramviewer@0.9.8 + +## 0.74.0 + +### Features + +- [#13152](https://github.com/gradio-app/gradio/pull/13152) [`30bf54c`](https://github.com/gradio-app/gradio/commit/30bf54c187478cdafa70037f088e353f5629a995) - Fix cloudflare functions so forwarding works. Thanks @aliabd! +- [#13135](https://github.com/gradio-app/gradio/pull/13135) [`b55fd8a`](https://github.com/gradio-app/gradio/commit/b55fd8a41e03ba846ad5b13e8dcec8528003b322) - Fix Website Build. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/statustracker@0.13.1 +- @gradio/tabs@0.5.9 +- @gradio/code@0.17.6 +- @gradio/html@0.12.1 +- @gradio/paramviewer@0.9.7 + +## 0.73.0 + +### Features + +- [#12924](https://github.com/gradio-app/gradio/pull/12924) [`84d409f`](https://github.com/gradio-app/gradio/commit/84d409f258d97982c7216d6bab7c03cf71de6606) - Support markdown for docs and guides. Thanks @aliabd! +- [#13123](https://github.com/gradio-app/gradio/pull/13123) [`26be3ad`](https://github.com/gradio-app/gradio/commit/26be3ade4cf23a73fe9cee119cafc22f274f1ef3) - More efficient cloudflare functions for markdown support. Thanks @aliabd! +- [#12976](https://github.com/gradio-app/gradio/pull/12976) [`7fb33fc`](https://github.com/gradio-app/gradio/commit/7fb33fc3b80b421817c1d1ddea19c8858a9f2924) - Add sidebar to the right guides. Thanks @freddyaboulton! +- [#12973](https://github.com/gradio-app/gradio/pull/12973) [`aaba8ec`](https://github.com/gradio-app/gradio/commit/aaba8ec130872a7357132990d6a068f2e882661e) - Add theme gallery to docs. Thanks @hannahblair! +- [#12986](https://github.com/gradio-app/gradio/pull/12986) [`429a9fa`](https://github.com/gradio-app/gradio/commit/429a9fad5207fb27648d860a4802ff52a5b38746) - Better docs for BarPlot, LinePlot and ScatterPlot. Thanks @aliabd! +- [#12991](https://github.com/gradio-app/gradio/pull/12991) [`6f8a053`](https://github.com/gradio-app/gradio/commit/6f8a0533ad0247e709fefe406961b864473d344d) - improve mobile menu in docs. Thanks @hannahblair! + +### Dependency updates + +- @gradio/statustracker@0.13.0 +- @gradio/html@0.12.0 +- @gradio/button@0.6.6 +- @gradio/code@0.17.5 +- @gradio/paramviewer@0.9.6 + +## 0.72.0 + +### Features + +- [#12972](https://github.com/gradio-app/gradio/pull/12972) [`4a0fe6e`](https://github.com/gradio-app/gradio/commit/4a0fe6e5aec1df710bd843f2f328d43fb7cfa7ef) - Fix Custom HTML Gallery URL. Thanks @freddyaboulton! +- [#12959](https://github.com/gradio-app/gradio/pull/12959) [`84a9235`](https://github.com/gradio-app/gradio/commit/84a923531490d4defe903985c2496a2d6412092b) - Fix table styling on the docs. Thanks @aliabd! + +### Fixes + +- [#12966](https://github.com/gradio-app/gradio/pull/12966) [`673feb4`](https://github.com/gradio-app/gradio/commit/673feb4cf21beb7df03a6721d29757a2fb265c5d) - Fix website local font paths and font-weight CSS. Thanks @hannahblair! +- [#12941](https://github.com/gradio-app/gradio/pull/12941) [`50e9c84`](https://github.com/gradio-app/gradio/commit/50e9c84749aff9da985a79c8a88292fac62fa1d6) - HTML Gallery Tweaks + Docs. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/button@0.6.5 +- @gradio/code@0.17.4 +- @gradio/html@0.11.1 +- @gradio/paramviewer@0.9.5 +- @gradio/statustracker@0.12.5 +- @gradio/tabitem@0.6.6 +- @gradio/tabs@0.5.8 + +## 0.71.1 + +### Dependency updates + +- @gradio/html@0.11.0 + +## 0.71.0 + +### Features + +- [#12917](https://github.com/gradio-app/gradio/pull/12917) [`a0fff5c`](https://github.com/gradio-app/gradio/commit/a0fff5cb0e4cc0f8cc3fff7b5fbe18a031c7cc27) - Add push_to_hub method to gr.HTML. Add a gallery to view notable custom HTML components. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/button@0.6.4 +- @gradio/html@0.10.0 +- @gradio/tabs@0.5.7 + +## 0.70.0 + +### Features + +- [#12700](https://github.com/gradio-app/gradio/pull/12700) [`b01c95a`](https://github.com/gradio-app/gradio/commit/b01c95a58be8e18bb4ddef7f2ee238a7774e5be9) - Rewrite behavior section of docs. Thanks @aliabd! +- [#12823](https://github.com/gradio-app/gradio/pull/12823) [`32b1d6f`](https://github.com/gradio-app/gradio/commit/32b1d6ffe9e753ad563cc3f4f77a0bfbf89c022c) - I’ve updated the render decorator documentation to be clearer and more practical. Thanks @Ankith34! + +### Fixes + +- [#12888](https://github.com/gradio-app/gradio/pull/12888) [`3f835cf`](https://github.com/gradio-app/gradio/commit/3f835cf9c6cdf570a107233e2a87e0dc5cd751cb) - Remove type parameter from ChatInterface Docs. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/statustracker@0.12.4 +- @gradio/tabs@0.5.6 +- @gradio/code@0.17.3 +- @gradio/paramviewer@0.9.4 + +## 0.69.0 + +### Dependency updates + +- @gradio/statustracker@0.12.3 + +## 0.69.0 + +### Features + +- [#12793](https://github.com/gradio-app/gradio/pull/12793) [`47ffecf`](https://github.com/gradio-app/gradio/commit/47ffecfdebf59a47f884089335fecc4ee6ee8d6e) - Add Gradio 5 docs to website. Thanks @aliabd! +- [#12755](https://github.com/gradio-app/gradio/pull/12755) [`78a8dd4`](https://github.com/gradio-app/gradio/commit/78a8dd46018951c194fa05259db63dacd712aa37) - Add controlling width to Row docs. Thanks @aliabd! +- [#12790](https://github.com/gradio-app/gradio/pull/12790) [`fb1aec5`](https://github.com/gradio-app/gradio/commit/fb1aec5b9ec7616d9cc5f3602825119ea187a96f) - Add ElevenLabs winner to hackathon winners page. Thanks @aliabd! + +### Fixes + +- [#12794](https://github.com/gradio-app/gradio/pull/12794) [`0954db4`](https://github.com/gradio-app/gradio/commit/0954db4d618e72a6404bd3904988e5307f22d7d0) - Fix Dependency docs. Thanks @aliabd! +- [#12630](https://github.com/gradio-app/gradio/pull/12630) [`44817db`](https://github.com/gradio-app/gradio/commit/44817dbe89612578b5d5c02caa33f48a1fe08dfc) - fix(website): improve Event Listeners table dark mode readability. Thanks @DanielDerefaka! +- [#12791](https://github.com/gradio-app/gradio/pull/12791) [`7f2cf84`](https://github.com/gradio-app/gradio/commit/7f2cf84fb79bf4f1d9157acdef4ab4a7024678b2) - Add better related guides section to docs. Thanks @aliabd! + +### Dependency updates + +- @gradio/button@0.6.3 +- @gradio/code@0.17.1 +- @gradio/paramviewer@0.9.2 +- @gradio/statustracker@0.12.2 +- @gradio/tabitem@0.6.5 +- @gradio/tabs@0.5.5 + +## 0.68.0 + +### Features + +- [#12685](https://github.com/gradio-app/gradio/pull/12685) [`3c831d3`](https://github.com/gradio-app/gradio/commit/3c831d35e041ac7cc30c76a975341fcc2d905903) - Create MCP birthday winners page. Thanks @aliabd! +- [#12691](https://github.com/gradio-app/gradio/pull/12691) [`d81ff6f`](https://github.com/gradio-app/gradio/commit/d81ff6fce23ab3bfef14e626f1e15b4301ac65fe) - Add Community choice award winner to website. Thanks @freddyaboulton! +- [#12624](https://github.com/gradio-app/gradio/pull/12624) [`06d657b`](https://github.com/gradio-app/gradio/commit/06d657b27143538c58a64f425943f7dfa9a5e032) - Remove type parameter from Chatbot Docs. Thanks @freddyaboulton! +- [#12678](https://github.com/gradio-app/gradio/pull/12678) [`4eb5d63`](https://github.com/gradio-app/gradio/commit/4eb5d633e00b18d84f8beb1d76896c803325e5eb) - Extend Python syntax highlighting patterns in the docs. Thanks @hannahblair! + +### Dependency updates + +- @gradio/button@0.6.2 + +## 0.67.2 + +### Dependency updates + +- @gradio/statustracker@0.12.1 +- @gradio/button@0.6.1 +- @gradio/code@0.17.0 +- @gradio/paramviewer@0.9.1 +- @gradio/tabitem@0.6.4 +- @gradio/tabs@0.5.4 + +## 0.67.1 + +### Fixes + +- [#12495](https://github.com/gradio-app/gradio/pull/12495) [`0c5cec1`](https://github.com/gradio-app/gradio/commit/0c5cec175815344fcc6c8dfbdacf71b85fd3ad37) - Remove Hackathon deadline banner on website. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/button@0.6.0 +- @gradio/tabs@0.5.3 +- @gradio/tabitem@0.6.3 + +## 0.67.0 + +### Features + +- [#12479](https://github.com/gradio-app/gradio/pull/12479) [`28b49e3`](https://github.com/gradio-app/gradio/commit/28b49e3e74fc73dff938a2b7f53325d554fdf680) - add announcement pills to docs landing page. Thanks @hannahblair! + +### Fixes + +- [#12481](https://github.com/gradio-app/gradio/pull/12481) [`99756eb`](https://github.com/gradio-app/gradio/commit/99756ebf689c8ae1a8d38b764d68f70544c35b93) - Fix and bring back cc gallery on website. Thanks @aliabd! + +## 0.66.0 + +### Features + +- [#12436](https://github.com/gradio-app/gradio/pull/12436) [`ee0cf6c`](https://github.com/gradio-app/gradio/commit/ee0cf6c8bc5929701872c1b5e7e5d252412b86cd) - Fix website after changes +- [#12162](https://github.com/gradio-app/gradio/pull/12162) [`81278c4`](https://github.com/gradio-app/gradio/commit/81278c41110b94e7bee9b4fe493b5df72afd4b13) - Fix huggingface-hub issue in lite +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Improve llms txt, add few guides at the top and organize +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Remove lite across website and use embedded spaces +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Fix website to work with svelte 5 +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - docs redesign: landing page and nav bar +- [#12362](https://github.com/gradio-app/gradio/pull/12362) [`e3f80b8`](https://github.com/gradio-app/gradio/commit/e3f80b88ad8c69ef2f85ee68bc89c5019a40ebfd) - Add kickoff stream link to website banner +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - fix publish +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Fix website with param viewer +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - docs redesign: docs and api pages +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Fix website on 6.0 +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Add copy to markdown buttons to the guides +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Add 6.0 migration guide to llms.txt +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Fix broken python client and third party docs +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - 🌙 Add dark mode to gradio docs + +### Dependencies + +- @gradio/code@0.16.0 +- @gradio/paramviewer@0.9.0 +- @gradio/statustracker@0.12.0 +- @gradio/tabitem@0.6.2 +- @gradio/tabs@0.5.2 +- @gradio/button@0.5.14 + +## 0.66.0-dev.4 + +### Features + +- [#12398](https://github.com/gradio-app/gradio/pull/12398) [`04e2581`](https://github.com/gradio-app/gradio/commit/04e258106f6f6a07facbacd9f43a307fc0cb64c9) - fix publish. Thanks @pngwn! +- [#12362](https://github.com/gradio-app/gradio/pull/12362) [`e3f80b8`](https://github.com/gradio-app/gradio/commit/e3f80b88ad8c69ef2f85ee68bc89c5019a40ebfd) - Add kickoff stream link to website banner. Thanks @aliabd! + +### Dependency updates + +- @gradio/statustracker@0.12.0-dev.1 +- @gradio/code@0.16.0-dev.3 +- @gradio/paramviewer@0.9.0-dev.1 +- @gradio/tabitem@0.6.2-dev.0 +- @gradio/tabs@0.5.2-dev.0 + +## 0.66.0-dev.3 + +### Dependency updates + +- @gradio/code@0.16.0-dev.2 + +## 0.66.0-dev.2 + +### Features + +- [#12325](https://github.com/gradio-app/gradio/pull/12325) [`47a0855`](https://github.com/gradio-app/gradio/commit/47a08559e125dbf68662c20557dd43c87b263c65) - Remove lite across website and use embedded spaces. Thanks @aliabd! + +## 0.66.0-dev.1 + +### Features + +- [#12217](https://github.com/gradio-app/gradio/pull/12217) [`681fa11`](https://github.com/gradio-app/gradio/commit/681fa11357de520f0b05605cbd300d9e276d8736) - Update ZeroGPU guide to reflect best practices on manually passing an IP token. Thanks @dawoodkhan82! +- [#12106](https://github.com/gradio-app/gradio/pull/12106) [`26bbdf8`](https://github.com/gradio-app/gradio/commit/26bbdf8b6a16d370d0314b03b3fa49de922ad2ff) - 🌙 Add dark mode to gradio docs. Thanks @hannahblair! +- [#12164](https://github.com/gradio-app/gradio/pull/12164) [`924ad65`](https://github.com/gradio-app/gradio/commit/924ad65ac8c664845e0f8b2542902469308c4d27) - Fix huggingface-hub issue in lite. Thanks @vedo-avattay! + +### Dependency updates + +- @gradio/tabs@0.5.2-dev.0 +- @gradio/statustracker@0.12.0-dev.0 +- @gradio/paramviewer@0.9.0-dev.0 +- @gradio/code@0.15.1-dev.1 +- @gradio/tabitem@0.6.2-dev.0 + +## 0.65.1-dev.0 + +### Dependency updates + +- @gradio/code@0.15.1-dev.0 + +## 0.65.0 + +### Features + +- [#11986](https://github.com/gradio-app/gradio/pull/11986) [`cf9aefd`](https://github.com/gradio-app/gradio/commit/cf9aefd0ae04ecc19bf7e69ec8d2ae4a081ef842) - More vibe editor issues and features. Thanks @aliabd! + +## 0.64.1 + +### Fixes + +- [#11966](https://github.com/gradio-app/gradio/pull/11966) [`cde255a`](https://github.com/gradio-app/gradio/commit/cde255a731cb7b18d8aff0a0b82374f7d111cb01) - Fix website release. Thanks @aliabd! + +## 0.64.0 + +### Features + +- [#11932](https://github.com/gradio-app/gradio/pull/11932) [`d67029a`](https://github.com/gradio-app/gradio/commit/d67029a81ed8846c34f3f84b66962bbe65d9ae10) - Remove references to deleted lite guide - Fix website build. Thanks @aliabd! + +### Fixes + +- [#11940](https://github.com/gradio-app/gradio/pull/11940) [`13fdfa6`](https://github.com/gradio-app/gradio/commit/13fdfa6d549ee4e31308af69b75140085ea51fc4) - Upgrade Prismjs. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/paramviewer@0.8.1 + +## 0.63.0 + +### Features + +- [#11858](https://github.com/gradio-app/gradio/pull/11858) [`3f8ea13`](https://github.com/gradio-app/gradio/commit/3f8ea13a8ca92abf0ad34392e403a449fda3c6c2) - remove lite. Thanks @pngwn! +- [#11916](https://github.com/gradio-app/gradio/pull/11916) [`03024f8`](https://github.com/gradio-app/gradio/commit/03024f8cae5d1a35be8fe6a988ae3917eefba662) - Fix broken website build. Thanks @aliabd! + +### Fixes + +- [#11864](https://github.com/gradio-app/gradio/pull/11864) [`3df62ba`](https://github.com/gradio-app/gradio/commit/3df62ba698ed283b2e92599af0447222045bf242) - Add walkthrough to the docs. Thanks @aliabd! +- [#11865](https://github.com/gradio-app/gradio/pull/11865) [`be70c9b`](https://github.com/gradio-app/gradio/commit/be70c9b68bbbe0792a84efa4c988f8a987149197) - Support per-page navbar configuration in multipage apps. Thanks @abidlabs! + +### Dependency updates + +- @gradio/paramviewer@0.8.0 +- @gradio/code@0.15.0 +- @gradio/statustracker@0.11.1 +- @gradio/tabitem@0.6.1 +- @gradio/tabs@0.5.1 + +## 0.62.1 + +### Fixes + +- [#11838](https://github.com/gradio-app/gradio/pull/11838) [`231a448`](https://github.com/gradio-app/gradio/commit/231a448b5b5322d218c07329cd18cb1ba268af67) - Suppress assertion error when gr.OauthProfile is used without login button. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/statustracker@0.11.0 +- @gradio/tabitem@0.6.0 +- @gradio/tabs@0.5.0 +- @gradio/code@0.14.16 +- @gradio/paramviewer@0.7.16 + +## 0.62.0 + +### Dependency updates + +- @gradio/statustracker@0.10.18 + +## 0.62.0 + +### Features + +- [#11766](https://github.com/gradio-app/gradio/pull/11766) [`dfc0e03`](https://github.com/gradio-app/gradio/commit/dfc0e0305068dbd16ce905afc73d139e1e51eb46) - Add JS Dataframe documentation to docs. Thanks @pngwn! +- [#11774](https://github.com/gradio-app/gradio/pull/11774) [`5a4ae49`](https://github.com/gradio-app/gradio/commit/5a4ae4926b9dadefd5cf9d0225f5e8ed69eebb51) - Fix markdown link rendering in docs descriptions. Thanks @aliabd! + +## 0.61.2 + +### Features + +- [#11704](https://github.com/gradio-app/gradio/pull/11704) [`0189862`](https://github.com/gradio-app/gradio/commit/0189862a61af54b718f43badf57ecee11b5deae4) - chore: remove no-op setting. Thanks @benmccann! + +### Dependency updates + +- @gradio/code@0.14.15 +- @gradio/statustracker@0.10.17 +- @gradio/paramviewer@0.7.15 + +## 0.61.1 + +### Dependency updates + +- @gradio/code@0.14.14 +- @gradio/paramviewer@0.7.14 +- @gradio/statustracker@0.10.16 + +## 0.61.0 + +### Features + +- [#11674](https://github.com/gradio-app/gradio/pull/11674) [`27d9aef`](https://github.com/gradio-app/gradio/commit/27d9aef4831d99ec46f9b5553fc7e149afe23f5f) - Add dialogue component to docs. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.14.13 + +## 0.60.2 + +### Dependency updates + +- @gradio/code@0.14.12 + +## 0.60.1 + +### Dependency updates + +- @gradio/statustracker@0.10.15 +- @gradio/code@0.14.11 +- @gradio/paramviewer@0.7.13 + +## 0.60.0 + +### Features + +- [#11427](https://github.com/gradio-app/gradio/pull/11427) [`6b2bcd0`](https://github.com/gradio-app/gradio/commit/6b2bcd097ae5ef999a7fb273ecf7c7e4c0eab305) - Improve load times of the Gradio front-end. Thanks @pngwn! +- [#11505](https://github.com/gradio-app/gradio/pull/11505) [`3c20f9f`](https://github.com/gradio-app/gradio/commit/3c20f9f1007f8b7b305c92bbcbb95d2c44ee17ad) - Add back theme.css to fix website build. Thanks @aliabd! +- [#11517](https://github.com/gradio-app/gradio/pull/11517) [`40c87f1`](https://github.com/gradio-app/gradio/commit/40c87f180759434367538506df8310769d99346a) - Add more hackathon winners to gallery. Thanks @aliabd! + +### Fixes + +- [#11430](https://github.com/gradio-app/gradio/pull/11430) [`4d4cd4b`](https://github.com/gradio-app/gradio/commit/4d4cd4b08d6daafc66e0189a2b274916da00d480) - fix exports + add browser build. Thanks @pngwn! + +### Dependency updates + +- @gradio/statustracker@0.10.14 +- @gradio/tabs@0.4.5 +- @gradio/tabitem@0.5.0 +- @gradio/code@0.14.10 +- @gradio/paramviewer@0.7.12 + +## 0.59.0 + +### Features + +- [#11437](https://github.com/gradio-app/gradio/pull/11437) [`7d98414`](https://github.com/gradio-app/gradio/commit/7d984145e7c0051424cd3f8f20152703966192b9) - Add support for markdown in tips and warnings. Thanks @aliabd! + +### Dependency updates + +- @gradio/tabitem@0.4.6 +- @gradio/code@0.14.9 + +## 0.58.0 + +### Features + +- [#11410](https://github.com/gradio-app/gradio/pull/11410) [`8607c35`](https://github.com/gradio-app/gradio/commit/8607c35acf75661ae6b9ac5acc81071796e6b681) - Add gallery of hackathon winners to the website. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.14.8 +- @gradio/paramviewer@0.7.11 +- @gradio/statustracker@0.10.13 + +## 0.57.4 + +### Fixes + +- [#11370](https://github.com/gradio-app/gradio/pull/11370) [`992f84b`](https://github.com/gradio-app/gradio/commit/992f84bc0147ebacbf76bc6ca7d931ccdeb06839) - Fix zerogu-guide. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/statustracker@0.10.12 +- @gradio/code@0.14.7 +- @gradio/paramviewer@0.7.10 +- @gradio/tabitem@0.4.5 +- @gradio/tabs@0.4.5 + +## 0.57.3 + +### Dependency updates + +- @gradio/code@0.14.7 + +## 0.57.2 + +### Dependency updates + +- @gradio/tabs@0.4.5 +- @gradio/tabitem@0.4.5 +- @gradio/code@0.14.6 + +## 0.57.1 + +### Fixes + +- [#11315](https://github.com/gradio-app/gradio/pull/11315) [`86f2feb`](https://github.com/gradio-app/gradio/commit/86f2feb20b406fb9b7357c8ce2d2ffb69c21c1b6) - Update tweets on the landing page. Thanks @aliabd! + +## 0.57.0 + +### Dependency updates + +- @gradio/statustracker@0.10.12 + +## 0.57.0 + +### Features + +- [#11268](https://github.com/gradio-app/gradio/pull/11268) [`7bf3161`](https://github.com/gradio-app/gradio/commit/7bf3161c0688e664053b9288a3867d001a306ba0) - Add hackathon to website banner. Thanks @aliabd! + +### Dependency updates + +- @gradio/statustracker@0.10.12 +- @gradio/code@0.14.5 +- @gradio/paramviewer@0.7.10 + +## 0.56.2 + +### Dependency updates + +- @gradio/code@0.14.4 +- @gradio/paramviewer@0.7.9 +- @gradio/statustracker@0.10.11 + +## 0.56.1 + +### Features + +- [#11173](https://github.com/gradio-app/gradio/pull/11173) [`d023b2e`](https://github.com/gradio-app/gradio/commit/d023b2e2ffdf872f531e22b0310bc7855a756c7d) - Adds docs for `gr.api()` which were previously missing from the website. Thanks @abidlabs! + +### Dependency updates + +- @gradio/tabs@0.4.4 +- @gradio/tabitem@0.4.4 + +## 0.56.0 + +### Features + +- [#11120](https://github.com/gradio-app/gradio/pull/11120) [`b2118d4`](https://github.com/gradio-app/gradio/commit/b2118d4b613f93b34e02c809e7d0faaa7545b875) - Docs mcp server. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.14.3 + +## 0.55.0 + +### Features + +- [#11027](https://github.com/gradio-app/gradio/pull/11027) [`eff532b`](https://github.com/gradio-app/gradio/commit/eff532b913a3c8f06f10a4f9471d3177e3744053) - Add new `ImageSlider` component. Thanks @pngwn! + +### Dependency updates + +- @gradio/code@0.14.2 +- @gradio/paramviewer@0.7.8 +- @gradio/statustracker@0.10.10 + +## 0.54.2 + +### Fixes + +- [#11005](https://github.com/gradio-app/gradio/pull/11005) [`3def0ed`](https://github.com/gradio-app/gradio/commit/3def0ed9edc5a9194d69456948324ec4e2740b7d) - Ensure that the `.select()` event in `gr.DataFrame` carries the `.row_value` and `.col_value`. Thanks @abidlabs! + +### Dependency updates + +- @gradio/code@0.14.1 +- @gradio/paramviewer@0.7.7 +- @gradio/statustracker@0.10.9 +- @gradio/tabitem@0.4.3 +- @gradio/tabs@0.4.3 + +## 0.54.1 + +### Features + +- [#10966](https://github.com/gradio-app/gradio/pull/10966) [`191b936`](https://github.com/gradio-app/gradio/commit/191b936c9233d20fa5d83de0d7101688209df8eb) - Load lite js correctly in playground. Thanks @aliabd! + +### Fixes + +- [#10979](https://github.com/gradio-app/gradio/pull/10979) [`975feee`](https://github.com/gradio-app/gradio/commit/975feee3925e5b65e52ae00da5c5066d8889ae6a) - improve webcam options for the ImageEditor. Thanks @pngwn! + +### Dependency updates + +- @gradio/statustracker@0.10.8 +- @gradio/code@0.14.0 +- @gradio/paramviewer@0.7.6 + +## 0.54.0 + +### Features + +- [#10635](https://github.com/gradio-app/gradio/pull/10635) [`2f68c9d`](https://github.com/gradio-app/gradio/commit/2f68c9d988dcbc53a0b8e53bdb1de49c9c8c65d8) - Refactor and redesign `ImageEditor` component. Thanks @pngwn! +- [#10952](https://github.com/gradio-app/gradio/pull/10952) [`a8dc9f4`](https://github.com/gradio-app/gradio/commit/a8dc9f469ba49eb1f39f81e8c5c3b4acc7cd482c) - update website banner with 1m MAU announcement. Thanks @yvrjsharma! +- [#10936](https://github.com/gradio-app/gradio/pull/10936) [`e8b8eef`](https://github.com/gradio-app/gradio/commit/e8b8eef7e5cb4474fcc109aee871e006d7dd17f3) - Fix broken css on some param tables. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.13.2 +- @gradio/paramviewer@0.7.5 +- @gradio/statustracker@0.10.7 + +## 0.53.2 + +### Features + +- [#10845](https://github.com/gradio-app/gradio/pull/10845) [`2521e8a`](https://github.com/gradio-app/gradio/commit/2521e8a1c76fe77253156bf8465500b9cb1db5c4) - Check if SharedWorker is available in the current runtime and fallback to DedicatedWorker if not available. Thanks @whitphx! +- [#10926](https://github.com/gradio-app/gradio/pull/10926) [`d81385b`](https://github.com/gradio-app/gradio/commit/d81385bdb939a81718339b806f0c3f71d64f6dc5) - Add status docs to MetadataDict. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.13.1 + +## 0.53.1 + +### Dependency updates + +- @gradio/statustracker@0.10.6 +- @gradio/code@0.13.0 +- @gradio/paramviewer@0.7.4 + +## 0.53.0 + +### Features + +- [#10733](https://github.com/gradio-app/gradio/pull/10733) [`731ab92`](https://github.com/gradio-app/gradio/commit/731ab92001c88d4cf1062acf0a4f1108a4513014) - Autocompletion on code editor component. Thanks @whitphx! + +### Dependency updates + +- @gradio/statustracker@0.10.5 +- @gradio/code@0.12.0 +- @gradio/paramviewer@0.7.3 + +## 0.52.0 + +### Features + +- [#10744](https://github.com/gradio-app/gradio/pull/10744) [`a89fa28`](https://github.com/gradio-app/gradio/commit/a89fa288f42022bb489bac655562a8d1eb5030a5) - Add missing docs and fix gaps in website. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.11.2 + +## 0.51.0 + +### Features + +- [#10677](https://github.com/gradio-app/gradio/pull/10677) [`92d5ac8`](https://github.com/gradio-app/gradio/commit/92d5ac8e67147e1dd7a701cc8c3e2a5193e832c6) - Static llms.txt route on the website. Thanks @aliabd! + +## 0.50.4 + +### Dependency updates + +- @gradio/code@0.11.1 + +## 0.50.3 + +### Features + +- [#10670](https://github.com/gradio-app/gradio/pull/10670) [`a15964d`](https://github.com/gradio-app/gradio/commit/a15964db8772d3cb34e730e75866c5d7460199ab) - Update banner to point to FastRTC. Thanks @abidlabs! + +## 0.50.2 + +### Fixes + +- [#10669](https://github.com/gradio-app/gradio/pull/10669) [`d1b063f`](https://github.com/gradio-app/gradio/commit/d1b063fb0c9d893062d9b47919ed1ba9241a6e07) - Add load_chat documentation. Thanks @aliabid94! + +### Dependency updates + +- @gradio/code@0.11.0 + +## 0.50.1 + +### Dependency updates + +- @gradio/code@0.10.18 +- @gradio/paramviewer@0.7.2 +- @gradio/statustracker@0.10.4 + +## 0.50.0 + +### Features + +- [#10511](https://github.com/gradio-app/gradio/pull/10511) [`c4aa886`](https://github.com/gradio-app/gradio/commit/c4aa8864dabec4caeb59af91f6f1aaaf50e33b67) - Semantic search in the playground. Thanks @aliabd! + +## 0.49.0 + +### Features + +- [#10552](https://github.com/gradio-app/gradio/pull/10552) [`ed25a10`](https://github.com/gradio-app/gradio/commit/ed25a1053a55ddd2cf7d3067c72bdf77185ada8d) - Add 1920px wide resolution for wide monitors. Thanks @Oncorporation! + +### Fixes + +- [#10510](https://github.com/gradio-app/gradio/pull/10510) [`71e4cd4`](https://github.com/gradio-app/gradio/commit/71e4cd483e9b43b2aa770db9f834036bb70f9420) - Fixed wrong example usage of docs/gradio/selectdata. Thanks @PatZer0! + +### Dependency updates + +- @gradio/code@0.10.17 +- @gradio/paramviewer@0.7.1 +- @gradio/statustracker@0.10.3 +- @gradio/tabitem@0.4.2 +- @gradio/tabs@0.4.2 + +## 0.48.0 + +### Features + +- [#10480](https://github.com/gradio-app/gradio/pull/10480) [`90f90b7`](https://github.com/gradio-app/gradio/commit/90f90b7989081b0e4422384ee699cb98781723d1) - Add sidebar to the docs. Thanks @aliabd! +- [#10495](https://github.com/gradio-app/gradio/pull/10495) [`35fda36`](https://github.com/gradio-app/gradio/commit/35fda36de9745757298cafb3d8b91cbc1a4358c9) - Add an `anchor_links` parameter to `gr.ParamViewer` that allows linking to specific parameters. Thanks @abidlabs! + +### Dependency updates + +- @gradio/paramviewer@0.7.0 +- @gradio/code@0.10.16 + +## 0.47.3 + +### Dependency updates + +- @gradio/code@0.10.15 + +## 0.47.2 + +### Features + +- [#10443](https://github.com/gradio-app/gradio/pull/10443) [`f40747c`](https://github.com/gradio-app/gradio/commit/f40747c9fd12d160ac9f7b3c5273be6be815efac) - Fix error with website build in chatbot.svx. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.10.14 + +## 0.47.1 + +### Fixes + +- [#10419](https://github.com/gradio-app/gradio/pull/10419) [`efd358a`](https://github.com/gradio-app/gradio/commit/efd358a54a0f3ec0b201f17f3d61a2b28a733bc1) - Update chatbot.svx for issue#10413. Thanks @NewJerseyStyle! + +## 0.47.0 + +### Features + +- [#10392](https://github.com/gradio-app/gradio/pull/10392) [`4d47e4b`](https://github.com/gradio-app/gradio/commit/4d47e4b4e2f07d4dd5b600f7d9180c7ad3e67a1f) - Add a `log` parameter to Chatbot metadata that allows displaying `str` content next to the thought title. Thanks @abidlabs! +- [#10346](https://github.com/gradio-app/gradio/pull/10346) [`43e05d7`](https://github.com/gradio-app/gradio/commit/43e05d72f43c9ac8dc18e4b885c9da08605b09e6) - Document additional helper classes for `gr.Chatbot`. Thanks @abidlabs! +- [#10388](https://github.com/gradio-app/gradio/pull/10388) [`812f2d8`](https://github.com/gradio-app/gradio/commit/812f2d8a3f6956546d7aae53ab3ee36f0c9f2857) - Playground fixes and refactoring. Thanks @aliabd! +- [#10355](https://github.com/gradio-app/gradio/pull/10355) [`070cab5`](https://github.com/gradio-app/gradio/commit/070cab5d2e5fd3aabf4b1020e46b892446308ace) - Expand token length for existing code and prompt in playground. Thanks @aliabd! + +### Fixes + +- [#10404](https://github.com/gradio-app/gradio/pull/10404) [`9dc5d15`](https://github.com/gradio-app/gradio/commit/9dc5d157aaf002f7699d3b2c1652f49d47e53e80) - Tweak behavior related to the `status` of `gr.Chatbot` thought messages. Thanks @abidlabs! + +### Dependency updates + +- @gradio/statustracker@0.10.2 +- @gradio/tabitem@0.4.1 +- @gradio/tabs@0.4.1 +- @gradio/code@0.10.13 +- @gradio/paramviewer@0.6.4 + +## 0.46.0 + +### Features + +- [#10305](https://github.com/gradio-app/gradio/pull/10305) [`be40307`](https://github.com/gradio-app/gradio/commit/be40307d1d11421e01bf91fa5e05ec4ab97b09d8) - Add support for thinking LLMs directly in `gr.ChatInterface`. Thanks @abidlabs! + +### Dependency updates + +- @gradio/code@0.10.12 +- @gradio/paramviewer@0.6.3 + +## 0.45.0 + +### Features + +- [`6edf038`](https://github.com/gradio-app/gradio/commit/6edf0380b9c407e052e29a7fb23511c41ed3e6d9) - AI Playground Auto Fix Errors. Thanks @aliabd! + +### Dependency updates + +- @gradio/tabs@0.4.0 +- @gradio/tabitem@0.4.0 +- @gradio/code@0.10.11 +- @gradio/paramviewer@0.6.2 + +## 0.44.0 + +### Features + +- [#10109](https://github.com/gradio-app/gradio/pull/10109) [`48e4aa9`](https://github.com/gradio-app/gradio/commit/48e4aa9d627b6958a0b215d6312de508845f669c) - adds a `run_examples_on_click` parameter to `gr.ChatInterface` mirroring the the `run_on_click` parameter in `gr.Examples`. Thanks @abidlabs! +- [#10126](https://github.com/gradio-app/gradio/pull/10126) [`a623faf`](https://github.com/gradio-app/gradio/commit/a623fafbcfef820243359fe942b356cbbaf09d9a) - Move requirements generation in playground to playground worker. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.10.10 +- @gradio/paramviewer@0.6.1 + +## 0.43.0 + +### Features + +- [#10096](https://github.com/gradio-app/gradio/pull/10096) [`ec10aa3`](https://github.com/gradio-app/gradio/commit/ec10aa3b9b42d8c3fe930aff9465c469626992d4) - Fix paramviewer descriptions to only render markdown links. Thanks @aliabd! +- [#10071](https://github.com/gradio-app/gradio/pull/10071) [`01b919f`](https://github.com/gradio-app/gradio/commit/01b919f04b69732fd8adb52f6d156e5683589221) - Support `additional_outputs` in `gr.ChatInterface`. Thanks @abidlabs! +- [#10073](https://github.com/gradio-app/gradio/pull/10073) [`873dab5`](https://github.com/gradio-app/gradio/commit/873dab5bce32de472523b851d443588106223e21) - Fix the Playground to ignore comments in the requirements text. Thanks @whitphx! +- [#9998](https://github.com/gradio-app/gradio/pull/9998) [`6cc13f5`](https://github.com/gradio-app/gradio/commit/6cc13f55e00ff29e833fd55493a53b654f23fbcc) - Playground exclude unavailable packages. Thanks @whitphx! + +### Dependency updates + +- @gradio/paramviewer@0.6.0 + +## 0.42.3 + +### Fixes + +- [#10025](https://github.com/gradio-app/gradio/pull/10025) [`368ba73`](https://github.com/gradio-app/gradio/commit/368ba731069583b22fcf7ddc9501db349f6a2953) - Update Chat Interface examples and add more LLM libraries and API providers. Thanks @abidlabs! + +## 0.42.2 + +### Dependency updates + +- @gradio/code@0.10.9 +- @gradio/paramviewer@0.5.8 +- @gradio/tabitem@0.3.5 +- @gradio/tabs@0.3.5 + +## 0.42.1 + +### Dependency updates + +- @gradio/code@0.10.8 +- @gradio/paramviewer@0.5.7 +- @gradio/tabitem@0.3.4 +- @gradio/tabs@0.3.4 + +## 0.42.0 + +### Features + +- [#9726](https://github.com/gradio-app/gradio/pull/9726) [`b6725cf`](https://github.com/gradio-app/gradio/commit/b6725cf6c1fe9667dc10e1988976ed36d84d73d3) - Lite auto-load imported modules with `pyodide.loadPackagesFromImports`. Thanks @whitphx! + +### Dependency updates + +- @gradio/code@0.10.7 +- @gradio/paramviewer@0.5.6 + +## 0.41.0 + +### Features + +- [#9811](https://github.com/gradio-app/gradio/pull/9811) [`7b6bd31`](https://github.com/gradio-app/gradio/commit/7b6bd3188199af1eac8f8d6d21b15a0bdc3d5619) - Fix the tab names in the playground. Thanks @whitphx! +- [#9647](https://github.com/gradio-app/gradio/pull/9647) [`7cce63e`](https://github.com/gradio-app/gradio/commit/7cce63e29f274b9fbd6c779914adeaab08ea60f7) - Ask LLM to generate the requirements.txt in the playground. Thanks @whitphx! + +### Dependency updates + +- @gradio/tabs@0.3.3 +- @gradio/tabitem@0.3.3 +- @gradio/code@0.10.6 +- @gradio/paramviewer@0.5.5 + +## 0.40.3 + +### Fixes + +- [#9653](https://github.com/gradio-app/gradio/pull/9653) [`61cd768`](https://github.com/gradio-app/gradio/commit/61cd768490a12f5d63101d5434092bcd1cfc43a8) - Ensures tabs with visible set to false are not visible. Thanks @hannahblair! +- [#9738](https://github.com/gradio-app/gradio/pull/9738) [`2ade59b`](https://github.com/gradio-app/gradio/commit/2ade59b95d4c3610a1a461cc95f020fbf9627305) - Export `Tabs` type from `@gradio/tabs` and fix the Playground to be compatible with the new Tabs API. Thanks @whitphx! + +### Dependency updates + +- @gradio/tabs@0.3.2 +- @gradio/tabitem@0.3.2 +- @gradio/code@0.10.5 +- @gradio/paramviewer@0.5.4 + +## 0.40.2 + +### Dependency updates + +- @gradio/code@0.10.4 +- @gradio/paramviewer@0.5.3 + +## 0.40.1 + +### Dependency updates + +- @gradio/tabs@0.3.1 +- @gradio/code@0.10.3 +- @gradio/paramviewer@0.5.2 +- @gradio/tabitem@0.3.1 + +## 0.40.0 + +### Features + +- [#9635](https://github.com/gradio-app/gradio/pull/9635) [`67e4044`](https://github.com/gradio-app/gradio/commit/67e4044c9ca8358eceeb1fa72fa415df03397d20) - Add website banner for gradio 5. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.10.2 + +## 0.39.1 + +### Features + +- [#9615](https://github.com/gradio-app/gradio/pull/9615) [`204f3e1`](https://github.com/gradio-app/gradio/commit/204f3e13e110fc0528032a102c9521057e18919d) - fixes to website. Thanks @pngwn! + +### Dependency updates + +- @gradio/code@0.10.1 +- @gradio/paramviewer@0.5.1 + +## 0.39.0 + +### Features + +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Start/stop recoding from the backend. Add guide on conversational chatbots +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Playground requirements tab +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Deprecate type='tuples for chatbot and focus chatbot docs on 'messages' type +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Expanding AI Playground Prompt for Qwen +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Remove grey background behind all components +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Fixes website build in 5.0-dev +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - 🔡 Update default core Gradio font +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Fix. Triggered dataframe change event for header change +- [#9606](https://github.com/gradio-app/gradio/pull/9606) [`9031324`](https://github.com/gradio-app/gradio/commit/90313243648883abf64a05361110e14e23616813) - Fixes website build +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Fixes annoying height bug in playground +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Refactoring playground +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Fix gradio.js aws path +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - 5.0 merge take 2 +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Improve UI on the Playground +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - File access security guide +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Add info about Powershell client +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Adds LLM to the Playground + +### Dependencies + +- @gradio/code@0.10.0 +- @gradio/tabs@0.3.0 +- @gradio/tabitem@0.3.0 + +## 0.39.0-beta.9 + +### Dependency updates + +- @gradio/code@0.10.0-beta.8 +- @gradio/paramviewer@0.4.22-beta.5 +- @gradio/tabitem@0.3.0-beta.4 +- @gradio/tabs@0.3.0-beta.3 + +## 0.39.0-beta.8 + +### Dependency updates + +- @gradio/tabitem@0.3.0-beta.4 +- @gradio/code@0.10.0-beta.7 +- @gradio/paramviewer@0.4.22-beta.4 + +## 0.39.0-beta.7 + +### Dependency updates + +- @gradio/code@0.10.0-beta.6 + +## 0.39.0-beta.6 + +### Features + +- [#9460](https://github.com/gradio-app/gradio/pull/9460) [`7352a89`](https://github.com/gradio-app/gradio/commit/7352a89722da91461c32fd33588531f3edce9c48) - Playground requirements tab. Thanks @whitphx! +- [#9452](https://github.com/gradio-app/gradio/pull/9452) [`3ec8e63`](https://github.com/gradio-app/gradio/commit/3ec8e636766cc629444bc3cbc6b53deaf65f5ab1) - Expanding AI Playground Prompt for Qwen. Thanks @aliabd! + +### Dependency updates + +- @gradio/tabs@0.3.0-beta.3 +- @gradio/tabitem@0.3.0-beta.3 +- @gradio/code@0.10.0-beta.5 +- @gradio/paramviewer@0.4.22-beta.3 + +## 0.39.0-beta.5 + +### Dependency updates + +- @gradio/code@0.10.0-beta.4 +- @gradio/paramviewer@0.4.22-beta.2 + +## 0.39.0-beta.4 + +### Features + +- [#9419](https://github.com/gradio-app/gradio/pull/9419) [`018c140`](https://github.com/gradio-app/gradio/commit/018c140ef86cacc8211df05b57b26924dab7fa08) - Start/stop recoding from the backend. Add guide on conversational chatbots. Thanks @freddyaboulton! +- [#9469](https://github.com/gradio-app/gradio/pull/9469) [`f7c3396`](https://github.com/gradio-app/gradio/commit/f7c3396f55a5b8364d3880a29d766bd092d7f840) - Fix. Triggered dataframe change event for header change. Thanks @Joodith! +- [#9426](https://github.com/gradio-app/gradio/pull/9426) [`4e54105`](https://github.com/gradio-app/gradio/commit/4e5410574002ea24067cf4e82b99a6a39f67632c) - Refactoring playground. Thanks @whitphx! +- [#9462](https://github.com/gradio-app/gradio/pull/9462) [`b622b1f`](https://github.com/gradio-app/gradio/commit/b622b1fcce888427e87aa1f70c9c2e60aa240e37) - Improve UI on the Playground. Thanks @aliabd! + +## 0.39.0-beta.3 + +### Dependency updates + +- @gradio/code@0.10.0-beta.3 + +## 0.39.0-beta.2 + +### Features + +- [#9326](https://github.com/gradio-app/gradio/pull/9326) [`7afb9a1`](https://github.com/gradio-app/gradio/commit/7afb9a14fa64310eb8b70f43a3bad373e46e36c1) - 5.0 merge take 2. Thanks @pngwn! +- [#9382](https://github.com/gradio-app/gradio/pull/9382) [`9e70832`](https://github.com/gradio-app/gradio/commit/9e7083286d5681b5fc623304a97d5a24fe6d6080) - Fixes website build in 5.0-dev. Thanks @aliabd! +- [#9379](https://github.com/gradio-app/gradio/pull/9379) [`0cad5f3`](https://github.com/gradio-app/gradio/commit/0cad5f348a846024b95b92fb48f88137ccfcd589) - Testing CI. Thanks @aliabd! +- [#9402](https://github.com/gradio-app/gradio/pull/9402) [`060acb3`](https://github.com/gradio-app/gradio/commit/060acb3b469530a3ea14275970b7028598052ef1) - Fixes annoying height bug in playground. Thanks @aliabd! +- [#9397](https://github.com/gradio-app/gradio/pull/9397) [`4be0933`](https://github.com/gradio-app/gradio/commit/4be0933d3a39099fd573d7db42416d7acef7f40f) - Fix gradio.js aws path. Thanks @aliabd! +- [#9343](https://github.com/gradio-app/gradio/pull/9343) [`322ac54`](https://github.com/gradio-app/gradio/commit/322ac5499ec5a8541039bf329e2525e9d24ed2cc) - Add info about Powershell client. Thanks @abidlabs! +- [#9233](https://github.com/gradio-app/gradio/pull/9233) [`9a85ccc`](https://github.com/gradio-app/gradio/commit/9a85cccf160118fccfb78dc1edcc7c51ff88de6c) - Adds LLM to the Playground. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.10.0-beta.2 +- @gradio/paramviewer@0.4.22-beta.2 + +## 0.39.0-beta.1 + +### Features + +- [#9204](https://github.com/gradio-app/gradio/pull/9204) [`3c73f00`](https://github.com/gradio-app/gradio/commit/3c73f00e3016b16917ebfe0bad390f2dff683457) - 🔡 Update default core Gradio font. Thanks @hannahblair! + +### Dependency updates + +- @gradio/code@0.9.1-beta.1 +- @gradio/paramviewer@0.4.22-beta.1 + +## 0.39.0-beta.0 + +### Features + +- [#9194](https://github.com/gradio-app/gradio/pull/9194) [`20c0836`](https://github.com/gradio-app/gradio/commit/20c0836ed0e0698dbc81d2a4bda04363fd857334) - Deprecate type='tuples for chatbot and focus chatbot docs on 'messages' type. Thanks @freddyaboulton! +- [#9213](https://github.com/gradio-app/gradio/pull/9213) [`ab4580b`](https://github.com/gradio-app/gradio/commit/ab4580bd5f755a07c9a9bd2a775220a9a2085f8c) - Remove grey background behind all components. Thanks @hannahblair! +- [#9206](https://github.com/gradio-app/gradio/pull/9206) [`bdbcf7b`](https://github.com/gradio-app/gradio/commit/bdbcf7b0e374c0769178767a1502cd310312278b) - Cloudflare migration. Thanks @aliabd! +- [#9156](https://github.com/gradio-app/gradio/pull/9156) [`8deeeb6`](https://github.com/gradio-app/gradio/commit/8deeeb6d1b83296e5174c2891b80fb317991289e) - File access security guide. Thanks @freddyaboulton! + +## 0.39.1 + +### Features + +- [#9379](https://github.com/gradio-app/gradio/pull/9379) [`0cad5f3`](https://github.com/gradio-app/gradio/commit/0cad5f348a846024b95b92fb48f88137ccfcd589) - Testing CI. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.9.1 +- @gradio/paramviewer@0.4.22 + +## 0.39.0 + +### Features + +- [#9291](https://github.com/gradio-app/gradio/pull/9291) [`bcb3a2b`](https://github.com/gradio-app/gradio/commit/bcb3a2b9a0e4f1a0195aed92f3ecfd1eda324464) - chore: update error.svx. Thanks @eltociear! + +## 0.38.1 + +### Features + +- [#9206](https://github.com/gradio-app/gradio/pull/9206) [`bdbcf7b`](https://github.com/gradio-app/gradio/commit/bdbcf7b0e374c0769178767a1502cd310312278b) - Cloudflare migration. Thanks @aliabd! + +### Fixes + +- [#9163](https://github.com/gradio-app/gradio/pull/9163) [`2b6cbf2`](https://github.com/gradio-app/gradio/commit/2b6cbf25908e42cf027324e54ef2cc0baad11a91) - fix exports and generate types. Thanks @pngwn! + +### Dependency updates + +- @gradio/paramviewer@0.4.22-beta.0 +- @gradio/code@0.9.1-beta.0 + +## 0.38.0 + +### Features + +- [#9102](https://github.com/gradio-app/gradio/pull/9102) [`efdc323`](https://github.com/gradio-app/gradio/commit/efdc3231a7bde38cfe45d10086d0d36a24c1b9b4) - Initial SSR refactor. Thanks @pngwn! +- [#9104](https://github.com/gradio-app/gradio/pull/9104) [`cf02f7d`](https://github.com/gradio-app/gradio/commit/cf02f7d7854b8ead864533581f7379a3fe61840f) - Fix chatinterface e2e test. Thanks @freddyaboulton! +- [#9075](https://github.com/gradio-app/gradio/pull/9075) [`3258968`](https://github.com/gradio-app/gradio/commit/325896837113d1c45de0dcff1972a8686730f695) - Add warning to guides and change styling of tip. Thanks @aliabd! +- [#9108](https://github.com/gradio-app/gradio/pull/9108) [`474102a`](https://github.com/gradio-app/gradio/commit/474102a8b404c23ffcfa7e1396a78bed621a9585) - Better text styling on docs. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.9.0 +- @gradio/paramviewer@0.4.21 + +## 0.37.0 + +### Features + +- [#8965](https://github.com/gradio-app/gradio/pull/8965) [`d30432e`](https://github.com/gradio-app/gradio/commit/d30432e9c6d4cc1e5cfd989a1a3ae4aba7e21290) - harden CI. Thanks @pngwn! +- [#9043](https://github.com/gradio-app/gradio/pull/9043) [`890bae3`](https://github.com/gradio-app/gradio/commit/890bae3942cc19f2b9040cfb6792adaa3cd478b0) - Filter out type ignore comments from demos on website. Thanks @aliabd! +- [#8857](https://github.com/gradio-app/gradio/pull/8857) [`6584aac`](https://github.com/gradio-app/gradio/commit/6584aace9866df582a6a3ff64dd045f1747aba42) - Website fixes for mobile. Thanks @aliabd! +- [#9067](https://github.com/gradio-app/gradio/pull/9067) [`f29aef4`](https://github.com/gradio-app/gradio/commit/f29aef4528ad93ab2cb1cf25bd5e3362bb562839) - Fix trailing slash link on docs. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.8.2 +- @gradio/paramviewer@0.4.20 + +## 0.36.0 + +### Features + +- [#8907](https://github.com/gradio-app/gradio/pull/8907) [`9b42ba8`](https://github.com/gradio-app/gradio/commit/9b42ba8f1006c05d60a62450d3036ce0d6784f86) - Update guides esp plots. Thanks @aliabid94! +- [#8871](https://github.com/gradio-app/gradio/pull/8871) [`7f1a78c`](https://github.com/gradio-app/gradio/commit/7f1a78c49ed69688ef1d39ef731c64ba934df645) - Add confirmation dialogue if leaving playground. Thanks @aliabd! +- [#8908](https://github.com/gradio-app/gradio/pull/8908) [`7c9fc9e`](https://github.com/gradio-app/gradio/commit/7c9fc9ebccf227fa54e3f28ee3abd1bd4f5cf412) - Add docs for Rust client to website. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.8.1 +- @gradio/paramviewer@0.4.19 + +## 0.35.0 + +### Features + +- [#8842](https://github.com/gradio-app/gradio/pull/8842) [`38c2ad4`](https://github.com/gradio-app/gradio/commit/38c2ad425a905431b1eb17b9498669f9e49f0dd5) - Add website to contributing readme. Thanks @aliabd! +- [#8784](https://github.com/gradio-app/gradio/pull/8784) [`2cc813a`](https://github.com/gradio-app/gradio/commit/2cc813a287ce326957f8b10106e574750b1db9be) - Fix OS detection for cross-browser compatibility. Thanks @lappemic! +- [#8825](https://github.com/gradio-app/gradio/pull/8825) [`b45d37f`](https://github.com/gradio-app/gradio/commit/b45d37f366ed4ef5dd77b2b4af90aa8174357298) - Fix param table rendering. Thanks @aliabd! +- [#8773](https://github.com/gradio-app/gradio/pull/8773) [`0b9e870`](https://github.com/gradio-app/gradio/commit/0b9e870f9cc45c2251806b3ac1654f6608ef27ed) - Hide embedded components while loading. Thanks @aliabd! +- [#8832](https://github.com/gradio-app/gradio/pull/8832) [`e75f2ca`](https://github.com/gradio-app/gradio/commit/e75f2ca2da4f41f25459b98bedaa940c887e6a93) - Fix build for pre-release. Thanks @pngwn! +- [#8618](https://github.com/gradio-app/gradio/pull/8618) [`aa4b7a7`](https://github.com/gradio-app/gradio/commit/aa4b7a71921fd5b7ad7e3c0cce7687a8f6d284da) - Improve styling of parameter tables in the docs. Thanks @abidlabs! +- [#8745](https://github.com/gradio-app/gradio/pull/8745) [`4030f28`](https://github.com/gradio-app/gradio/commit/4030f28af6ae9f3eb94bb4e9cae83fb7016cdaad) - Allows updating the dataset of a `gr.Examples`. Thanks @abidlabs! +- [#8757](https://github.com/gradio-app/gradio/pull/8757) [`6073736`](https://github.com/gradio-app/gradio/commit/60737366517f48d1a37ffce15425783a2887f305) - Document `FileData` class in docs. Thanks @hannahblair! + +### Fixes + +- [#8823](https://github.com/gradio-app/gradio/pull/8823) [`7b049e0`](https://github.com/gradio-app/gradio/commit/7b049e03577aac9853cd2cc1683d9e0b1e2f8d36) - Fix DateTime docs. Thanks @aliabd! +- [#8854](https://github.com/gradio-app/gradio/pull/8854) [`d1f0441`](https://github.com/gradio-app/gradio/commit/d1f044145ae93e5838042d9fb25f4f17def9c774) - Use covariant container types across the codebase and add typing to our demos. Thanks @abidlabs! + +### Dependency updates + +- @gradio/code@0.8.0 +- @gradio/paramviewer@0.4.18 + +## 0.34.0 + +### Highlights + +#### Support message format in chatbot 💬 ([#8422](https://github.com/gradio-app/gradio/pull/8422) [`4221290`](https://github.com/gradio-app/gradio/commit/4221290d847041024b1faa3df5585bba0775b8b3)) + +`gr.Chatbot` and `gr.ChatInterface` now support the [Messages API](https://huggingface.co/docs/text-generation-inference/en/messages_api#messages-api), which is fully compatible with LLM API providers such as Hugging Face Text Generation Inference, OpenAI's chat completions API, and Llama.cpp server. + +Building Gradio applications around these LLM solutions is now even easier! + +`gr.Chatbot` and `gr.ChatInterface` now have a `type` parameter that can accept two values - `'tuples'` and `'messages'`. If set to `'tuples'`, the default chatbot data format is expected. If set to `'messages'`, a list of dictionaries with `content` and `role` keys is expected. See below - + +```python +def chat_greeter(msg, history): + history.append({"role": "assistant", "content": "Hello!"}) + return history +``` + +Additionally, gradio now exposes a `gr.ChatMessage` dataclass you can use for IDE type hints and auto completion. + +image + + +#### Tool use in Chatbot 🛠️ + +The Gradio Chatbot can now natively display tool usage and intermediate thoughts common in Agent and chain-of-thought workflows! + +If you are using the new "messages" format, simply add a `metadata` key with a dictionary containing a `title` key and `value`. This will display the assistant message in an expandable message box to show the result of a tool or intermediate step. + +```python +import gradio as gr +from gradio import ChatMessage +import time + +def generate_response(history): + history.append(ChatMessage(role="user", content="What is the weather in San Francisco right now?")) + yield history + time.sleep(0.25) + history.append(ChatMessage(role="assistant", + content="In order to find the current weather in San Francisco, I will need to use my weather tool.") + ) + yield history + time.sleep(0.25) + + history.append(ChatMessage(role="assistant", + content="API Error when connecting to weather service.", + metadata={"title": "💥 Error using tool 'Weather'"}) + ) + yield history + time.sleep(0.25) + + history.append(ChatMessage(role="assistant", + content="I will try again", + )) + yield history + time.sleep(0.25) + + history.append(ChatMessage(role="assistant", + content="Weather 72 degrees Fahrenheit with 20% chance of rain.", + metadata={"title": "🛠️ Used tool 'Weather'"} + )) + yield history + time.sleep(0.25) + + history.append(ChatMessage(role="assistant", + content="Now that the API succeeded I can complete my task.", + )) + yield history + time.sleep(0.25) + + history.append(ChatMessage(role="assistant", + content="It's a sunny day in San Francisco with a current temperature of 72 degrees Fahrenheit and a 20% chance of rain. Enjoy the weather!", + )) + yield history + + +with gr.Blocks() as demo: + chatbot = gr.Chatbot(type="messages") + button = gr.Button("Get San Francisco Weather") + button.click(generate_response, chatbot, chatbot) + +if __name__ == "__main__": + demo.launch() +``` + + + +![tool-box-demo](https://github.com/freddyaboulton/freddyboulton/assets/41651716/cf73ecc9-90ac-42ce-bca5-768e0cc00a48) + + Thanks @freddyaboulton! + +### Features + +- [#8733](https://github.com/gradio-app/gradio/pull/8733) [`fb0daf3`](https://github.com/gradio-app/gradio/commit/fb0daf3730ffbe6aab5ebe4210eae150729a40b1) - Improvements to `gr.Examples`: adds events as attributes and documents, them, adds `sample_labels`, and `visible` properties. Thanks @abidlabs! +- [#8686](https://github.com/gradio-app/gradio/pull/8686) [`64ac05b`](https://github.com/gradio-app/gradio/commit/64ac05b1114e08c21909d21653c02d1c45f05aee) - Better spacing for codeblocks on docs. Thanks @aliabd! +- [#8656](https://github.com/gradio-app/gradio/pull/8656) [`740364e`](https://github.com/gradio-app/gradio/commit/740364e5cee5f96625fe0da3ac8257d97e5f0815) - Add guide on best practices for ZeroGPU limits with the python client. Thanks @freddyaboulton! +- [#8689](https://github.com/gradio-app/gradio/pull/8689) [`edcd574`](https://github.com/gradio-app/gradio/commit/edcd5748f6c0faf2028a8e6a330aad5eccf103d5) - Fix playground to display errors. Thanks @whitphx! +- [#8624](https://github.com/gradio-app/gradio/pull/8624) [`ba59bb8`](https://github.com/gradio-app/gradio/commit/ba59bb824f77dd3cb57019c59d3c3b0755c68b85) - Add search to website. Thanks @aliabd! + +### Fixes + +- [#8505](https://github.com/gradio-app/gradio/pull/8505) [`2943d6d`](https://github.com/gradio-app/gradio/commit/2943d6d68847314885dc6c5c0247083116017ca0) - Add Timer component. Thanks @aliabid94! +- [#8677](https://github.com/gradio-app/gradio/pull/8677) [`c946c6f`](https://github.com/gradio-app/gradio/commit/c946c6f31a34bfd888a6a16c3fb479fe34710206) - Allow supplying custom `gr.Chatbot` with events to `gr.ChatInterface`. Thanks @abidlabs! + +### Dependency updates + +- @gradio/code@0.7.0 + +## 0.33.0 + +### Features + +- [#8604](https://github.com/gradio-app/gradio/pull/8604) [`b6fa6b5`](https://github.com/gradio-app/gradio/commit/b6fa6b543f226540247cd50748019cde59b93005) - Add docs for `.on()`, `.then()`, and `.success()`, as well as the subclasses of `gr.EventData`. Thanks @abidlabs! +- [#8623](https://github.com/gradio-app/gradio/pull/8623) [`4c6e4e0`](https://github.com/gradio-app/gradio/commit/4c6e4e0ba9a6dc29f256d00d97f3062a516f5aac) - Fix CORS issues with Lite Component Demos. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.6.13 + +## 0.32.0 + +### Features + +- [#8489](https://github.com/gradio-app/gradio/pull/8489) [`c2a0d05`](https://github.com/gradio-app/gradio/commit/c2a0d056d679d90631d9ccd944dadd67e7e03b7f) - Control Display of Error, Info, Warning. Thanks @freddyaboulton! +- [#8593](https://github.com/gradio-app/gradio/pull/8593) [`d35c290`](https://github.com/gradio-app/gradio/commit/d35c290aadcb85113ee7ceea96a7ed7dc894b1d2) - Adding more docs for using components in chatbot. Thanks @abidlabs! +- [#8516](https://github.com/gradio-app/gradio/pull/8516) [`de6aa2b`](https://github.com/gradio-app/gradio/commit/de6aa2b67668605b65ad92842b2c798afa2c6d8a) - Add helper classes to docs. Thanks @aliabd! +- [#8605](https://github.com/gradio-app/gradio/pull/8605) [`fe83e64`](https://github.com/gradio-app/gradio/commit/fe83e6445a53c9376d92a7af9fd9a5ccf9376d7d) - Small fix to guide styling. Thanks @aliabd! +- [#8557](https://github.com/gradio-app/gradio/pull/8557) [`ed82a62`](https://github.com/gradio-app/gradio/commit/ed82a6237ec7873e2554c2ad0be438650cfebe8c) - Bring back embedded demos on component docs. Thanks @aliabd! + +### Fixes + +- [#8589](https://github.com/gradio-app/gradio/pull/8589) [`34430b9`](https://github.com/gradio-app/gradio/commit/34430b934dbab3bc525f56b390dbc054f76cf56c) - Handle GIFs correct in `gr.Image` preprocessing. Thanks @abidlabs! +- [#8581](https://github.com/gradio-app/gradio/pull/8581) [`a1c21cb`](https://github.com/gradio-app/gradio/commit/a1c21cb69a688bd38139153fe9c85a50c6ae86f2) - fix dataset update. Thanks @abidlabs! +- [#8537](https://github.com/gradio-app/gradio/pull/8537) [`81ae766`](https://github.com/gradio-app/gradio/commit/81ae7663b303ac7738bc216d9bf916f0515dd22e) - Many small fixes to website and docs. Thanks @aliabd! +- [#8559](https://github.com/gradio-app/gradio/pull/8559) [`483ecaa`](https://github.com/gradio-app/gradio/commit/483ecaae627145470ed68ed6872d42f2ac3a1980) - fix website build. Thanks @pngwn! + +### Dependency updates + +- @gradio/code@0.6.12 + +## 0.31.5 + +### Features + +- [#8491](https://github.com/gradio-app/gradio/pull/8491) [`ffd53fa`](https://github.com/gradio-app/gradio/commit/ffd53fa2dcb13d564fd07aa441d4016df8d2f155) - Remove broken guide redirect. Thanks @aliabd! +- [#8487](https://github.com/gradio-app/gradio/pull/8487) [`3a5d56e`](https://github.com/gradio-app/gradio/commit/3a5d56ea7bdbfc24357eaf8174f9275cb15fcf97) - Add Client Release Notes to Docs. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/code@0.6.11 + +## 0.31.4 + +### Dependency updates + +- @gradio/code@0.6.10 + +## 0.31.3 + +### Features + +- [#8471](https://github.com/gradio-app/gradio/pull/8471) [`a9e6595`](https://github.com/gradio-app/gradio/commit/a9e6595817b741c3dcf1eaedf58ee4f901784e57) - Tweak meta titles and descriptions for clients. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.6.9 + +## 0.31.2 + +### Features + +- [#8456](https://github.com/gradio-app/gradio/pull/8456) [`881f11c`](https://github.com/gradio-app/gradio/commit/881f11c862c769c21710735604c0733e0cfefe66) - Add website banner for clients launch. Thanks @aliabd! + +## 0.31.1 + +### Dependency updates + +- @gradio/code@0.6.8 + +## 0.31.0 + +### Features + +- [#8403](https://github.com/gradio-app/gradio/pull/8403) [`5efd35c`](https://github.com/gradio-app/gradio/commit/5efd35c7a06d894fdcb68898bdaaf9b457e608f1) - Editable Docs. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.6.7 + +## 0.30.4 + +### Dependency updates + +- @gradio/code@0.6.6 + +## 0.30.3 + +### Features + +- [#8319](https://github.com/gradio-app/gradio/pull/8319) [`1f9a5f0`](https://github.com/gradio-app/gradio/commit/1f9a5f0aa395ab51731f7d2a6ef0268a319cdc1b) - Fix bad redirect breaking website build. Thanks @aliabd! + +## 0.30.2 + +### Dependency updates + +- @gradio/code@0.6.5 + +## 0.30.1 + +### Dependency updates + +- @gradio/code@0.6.4 + +## 0.30.0 + +### Features + +- [#8278](https://github.com/gradio-app/gradio/pull/8278) [`4ae17a4`](https://github.com/gradio-app/gradio/commit/4ae17a4653fcf60de7b646e6243f1b77d7f8cd27) - Embedded Lite example apps in the docs. Thanks @whitphx! +- [#8262](https://github.com/gradio-app/gradio/pull/8262) [`d708ca8`](https://github.com/gradio-app/gradio/commit/d708ca8fca8c39bf878c70117c2910730a1bb76c) - Reorganize Guides in a more logical order. Thanks @abidlabs! + +### Dependency updates + +- @gradio/code@0.6.3 + +## 0.29.0 + +### Features + +- [#8224](https://github.com/gradio-app/gradio/pull/8224) [`6ee1f1f`](https://github.com/gradio-app/gradio/commit/6ee1f1f7215bc557c138e1f43d5a835775deacfc) - Display all custom components in the gallery. Thanks @freddyaboulton! + +### Fixes + +- [#8220](https://github.com/gradio-app/gradio/pull/8220) [`f176e1b`](https://github.com/gradio-app/gradio/commit/f176e1b509b7687b02c9173db1cd1ce25c3cd8f6) - Convert all demos on docs to lite. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.6.2 + +## 0.28.0 + +### Features + +- [#8121](https://github.com/gradio-app/gradio/pull/8121) [`f5b710c`](https://github.com/gradio-app/gradio/commit/f5b710c919b0ce604ea955f0d5f4faa91095ca4a) - chore(deps): update dependency eslint to v9. Thanks @renovate! +- [#8189](https://github.com/gradio-app/gradio/pull/8189) [`68dcae5`](https://github.com/gradio-app/gradio/commit/68dcae512c0fb699304446c3b1ae2afaba1a63d2) - Use workspace version for code in _website. Thanks @aliabd! +- [#8152](https://github.com/gradio-app/gradio/pull/8152) [`989fe25`](https://github.com/gradio-app/gradio/commit/989fe2566fc93e4f67dc86a869dc30e83404c7ab) - Make guide for tailwind more verbose. Thanks @duerrsimon! + +### Dependency updates + +- @gradio/code@0.6.1 + +## 0.27.0 + +### Features + +- [#8061](https://github.com/gradio-app/gradio/pull/8061) [`17e83c9`](https://github.com/gradio-app/gradio/commit/17e83c958ebb35b3e122ca486067d1bd5ce33a22) - Docs Reorg and Intro Page. Thanks @aliabd! +- [#8122](https://github.com/gradio-app/gradio/pull/8122) [`e089e4c`](https://github.com/gradio-app/gradio/commit/e089e4cb4a285e0d15593fc5b13b8f254b86c090) - update dependencies. Thanks @pngwn! +- [#8119](https://github.com/gradio-app/gradio/pull/8119) [`38a5482`](https://github.com/gradio-app/gradio/commit/38a5482df4d175d81e2aea319c2ffc525a76c538) - Be able to link to a custom component in the gallery directly. Thanks @freddyaboulton! + +## 0.26.1 + +### Dependency updates + +- @gradio/code@0.5.12 + +## 0.26.0 + +### Features + +- [#7945](https://github.com/gradio-app/gradio/pull/7945) [`328325a`](https://github.com/gradio-app/gradio/commit/328325a7ad812e7e152fe57a5a91a54b67adf728) - style changes for gradio website docs navbar. Thanks @shafiqihtsham! + +### Fixes + +- [#7935](https://github.com/gradio-app/gradio/pull/7935) [`919afff`](https://github.com/gradio-app/gradio/commit/919afffcee87bee25a6905c488484936df92189d) - Adds a Guide on deploying Gradio apps with Docker. Thanks @abidlabs! + +### Dependency updates + +- @gradio/code@0.5.11 + +## 0.25.2 + +### Dependency updates + +- @gradio/code@0.5.10 + +## 0.25.1 + +### Dependency updates + +- @gradio/code@0.5.9 + +## 0.25.0 + +### Features + +- [#7684](https://github.com/gradio-app/gradio/pull/7684) [`755157f`](https://github.com/gradio-app/gradio/commit/755157f99c2961f2e5caeaa9b76d248b4225ea8f) - Do not reload code inside gr.NO_RELOAD context. Thanks @freddyaboulton! +- [#7661](https://github.com/gradio-app/gradio/pull/7661) [`c62a57e`](https://github.com/gradio-app/gradio/commit/c62a57e7f8f2f6dad0110d06e915c48e7f628073) - Convert Docs Demos to Lite. Thanks @aliabd! + +### Dependency updates + +- @gradio/code@0.5.8 + +## 0.24.3 + +### Dependency updates + +- @gradio/code@0.5.7 + +## 0.24.2 + +### Dependency updates + +- @gradio/code@0.5.6 + +## 0.24.1 + +### Patch Changes + +- Updated dependencies []: + - @gradio/code@0.5.5 + +## 0.24.0 + +### Features + +- [#7451](https://github.com/gradio-app/gradio/pull/7451) [`65f114a`](https://github.com/gradio-app/gradio/commit/65f114a117b351f5935424fa78c830a58bafc44f) - Add error handling for missing `js/_website/version.json`. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 0.23.4 + +### Patch Changes + +- Updated dependencies []: + - @gradio/code@0.5.3 + +## 0.23.3 + +### Patch Changes + +- Updated dependencies []: + - @gradio/code@0.5.2 + +## 0.23.2 + +### Patch Changes + +- Updated dependencies []: + - @gradio/code@0.5.1 + +## 0.23.1 + +### Patch Changes + +- Updated dependencies [[`c1a7ea7`](https://github.com/gradio-app/gradio/commit/c1a7ea7c0c294aa970624f02225717c12bcf9b58)]: + - @gradio/code@0.5.0 + +## 0.23.0 + +### Features + +- [#7116](https://github.com/gradio-app/gradio/pull/7116) [`3c8c4ac`](https://github.com/gradio-app/gradio/commit/3c8c4ac2db284e1cb503c397205a79a6dcc27e23) - Document the `gr.ParamViewer` component, and fix component preprocessing/postprocessing docstrings. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.22.0 + +### Features + +- [#6970](https://github.com/gradio-app/gradio/pull/6970) [`dfe1f08`](https://github.com/gradio-app/gradio/commit/dfe1f08ae216dca8bac8e2d992ebde1f8746c795) - Style changes to custom components gallery. Thanks [@aliabd](https://github.com/aliabd)! +- [#7080](https://github.com/gradio-app/gradio/pull/7080) [`6654a32`](https://github.com/gradio-app/gradio/commit/6654a32ebad3c5b9f762d8e9e531f29152625819) - start cc docs guide. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.21.3 + +### Patch Changes + +- Updated dependencies []: + - @gradio/code@0.3.7 + +## 0.21.2 + +### Features + +- [#6997](https://github.com/gradio-app/gradio/pull/6997) [`523c08f`](https://github.com/gradio-app/gradio/commit/523c08fe3036f9d72416f7599fe0c64c1a4af823) - Design changes to Playground. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.21.1 + +### Patch Changes + +- Updated dependencies []: + - @gradio/code@0.3.6 + +## 0.21.0 + +### Features + +- [#6913](https://github.com/gradio-app/gradio/pull/6913) [`a5f3d2b`](https://github.com/gradio-app/gradio/commit/a5f3d2bef2d53b367ebf78d86e61f227cda5effa) - Fix broken redirects and guides in website. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.20.4 + +### Fixes + +- [#6767](https://github.com/gradio-app/gradio/pull/6767) [`7bb561a`](https://github.com/gradio-app/gradio/commit/7bb561a294ca41d1044927cb34d8645c4175cae0) - Rewriting parts of the README and getting started guides for 4.0. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.20.3 + +### Patch Changes + +- Updated dependencies []: + - @gradio/code@0.3.3 + +## 0.20.2 + +### Patch Changes + +- Updated dependencies []: + - @gradio/code@0.3.2 + +## 0.20.1 + +### Patch Changes + +- Updated dependencies []: + - @gradio/code@0.3.1 + +## 0.20.0 + +### Features + +- [#6679](https://github.com/gradio-app/gradio/pull/6679) [`abe9785`](https://github.com/gradio-app/gradio/commit/abe9785c50cf3d1098605326c92a1825ae89df14) - Remove Discourse Forum Link from Website. Thanks [@aliabd](https://github.com/aliabd)! +- [#6477](https://github.com/gradio-app/gradio/pull/6477) [`21ce721`](https://github.com/gradio-app/gradio/commit/21ce721bbddaf4f5768f59eef5fefc74c8f0cf27) - Custom component gallery. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.19.0 + +### Features + +- [#5885](https://github.com/gradio-app/gradio/pull/5885) [`9919b8a`](https://github.com/gradio-app/gradio/commit/9919b8ab43bee3d1d7cc65fd641fc8bc9725e102) - Fix the docstring decoration. Thanks [@whitphx](https://github.com/whitphx)! +- [#6650](https://github.com/gradio-app/gradio/pull/6650) [`d59ceec`](https://github.com/gradio-app/gradio/commit/d59ceec99d16f52e71d165448d959ba6b5624425) - Removes smooth scrolling from website. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.18.0 + +### Features + +- [#6549](https://github.com/gradio-app/gradio/pull/6549) [`3e60c13b9`](https://github.com/gradio-app/gradio/commit/3e60c13b9192fac04c5386135ede906d0e6a2025) - Add 3.x docs to the website!. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.17.0 + +### Features + +- [#6533](https://github.com/gradio-app/gradio/pull/6533) [`e2810fcfc`](https://github.com/gradio-app/gradio/commit/e2810fcfc84e2d66797736d8002c6a16f5b330d6) - Fix redirected paths on website. Thanks [@aliabd](https://github.com/aliabd)! +- [#6532](https://github.com/gradio-app/gradio/pull/6532) [`96290d304`](https://github.com/gradio-app/gradio/commit/96290d304a61064b52c10a54b2feeb09ca007542) - tweak deps. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.16.1 + +### Patch Changes + +- Updated dependencies []: + - @gradio/code@0.2.7 + +## 0.16.0 + +### Features + +- [#6460](https://github.com/gradio-app/gradio/pull/6460) [`e01b67f96`](https://github.com/gradio-app/gradio/commit/e01b67f96f054b4d96cd72ce9b713bab9992c6cc) - Custom Component Guide Redirects. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.15.0 + +### Features + +- [#6436](https://github.com/gradio-app/gradio/pull/6436) [`58e3ca826`](https://github.com/gradio-app/gradio/commit/58e3ca8260a6635e10e7a7f141221c4f746e9386) - Custom Component CLI Improvements. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6427](https://github.com/gradio-app/gradio/pull/6427) [`e0fc14659`](https://github.com/gradio-app/gradio/commit/e0fc146598ba9b081bc5fa9616d0a41c2aba2427) - Allow google analytics to work on Spaces (and other iframe situations). Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.14.0 + +### Features + +- [#6387](https://github.com/gradio-app/gradio/pull/6387) [`9d6d72f44`](https://github.com/gradio-app/gradio/commit/9d6d72f44370c45d9e213421a5586c25c5789278) - Tiny fix to indent on landing page demo. Thanks [@aliabd](https://github.com/aliabd)! +- [#6344](https://github.com/gradio-app/gradio/pull/6344) [`747197089`](https://github.com/gradio-app/gradio/commit/7471970894e999f335126766549552184231e8ea) - PDF component custom component guide. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.13.0 + +### Features + +- [#6351](https://github.com/gradio-app/gradio/pull/6351) [`294414d9f`](https://github.com/gradio-app/gradio/commit/294414d9f7b53da1a870d2d96e62a75242b40849) - Add sharing to playground. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.12.1 + +### Patch Changes + +- Updated dependencies []: + - @gradio/code@0.2.3 + +## 0.12.0 + +### Features + +- [#6268](https://github.com/gradio-app/gradio/pull/6268) [`de36820ef`](https://github.com/gradio-app/gradio/commit/de36820ef51097b47937b41fb76e4038aaa369cb) - Fix various issues with demos on website. Thanks [@aliabd](https://github.com/aliabd)! +- [#6193](https://github.com/gradio-app/gradio/pull/6193) [`fdedc5949`](https://github.com/gradio-app/gradio/commit/fdedc59491bf55e3a24936e97da48bf0144de816) - 4.0 Website Changes. Thanks [@aliabd](https://github.com/aliabd)! +- [#6243](https://github.com/gradio-app/gradio/pull/6243) [`2c9fd437f`](https://github.com/gradio-app/gradio/commit/2c9fd437f8249b238f2b1904fb5acfe3413ff950) - Some tweaks to the Custom Components Guide. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.11.1 + +### Features + +- [#6189](https://github.com/gradio-app/gradio/pull/6189) [`345ddd888`](https://github.com/gradio-app/gradio/commit/345ddd888e9f55cb04e5c6601d95d2a25e4ef39f) - Custom Component Guides. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.11.0 + +### Patch Changes + +- Updated dependencies [[`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7), [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7)]: + - @gradio/code@0.2.0 + +## 0.11.0-beta.1 + +### Features + +- [#6136](https://github.com/gradio-app/gradio/pull/6136) [`667802a6c`](https://github.com/gradio-app/gradio/commit/667802a6cdbfb2ce454a3be5a78e0990b194548a) - JS Component Documentation. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6142](https://github.com/gradio-app/gradio/pull/6142) [`103416d17`](https://github.com/gradio-app/gradio/commit/103416d17f021c82f5ff0583dcc2d80906ad279e) - JS READMEs and Storybook on Docs. Thanks [@aliabd](https://github.com/aliabd)! +- [#6121](https://github.com/gradio-app/gradio/pull/6121) [`93d28bc08`](https://github.com/gradio-app/gradio/commit/93d28bc088f7154ecc00f79eb98119f6d4858fe3) - Small fix to website header. Thanks [@aliabd](https://github.com/aliabd)! +- [#6151](https://github.com/gradio-app/gradio/pull/6151) [`e67e3f813`](https://github.com/gradio-app/gradio/commit/e67e3f813ea461d3245b4b575f3b2c44fca6a39c) - Fix issues with website deploy. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.11.0-beta.0 + +### Features + +- [#6082](https://github.com/gradio-app/gradio/pull/6082) [`037e5af33`](https://github.com/gradio-app/gradio/commit/037e5af3363c5b321b95efc955ee8d6ec0f4504e) - WIP: Fix docs. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6016](https://github.com/gradio-app/gradio/pull/6016) [`83e947676`](https://github.com/gradio-app/gradio/commit/83e947676d327ca2ab6ae2a2d710c78961c771a0) - Format js in v4 branch. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6089](https://github.com/gradio-app/gradio/pull/6089) [`cd8146ba0`](https://github.com/gradio-app/gradio/commit/cd8146ba053fbcb56cf5052e658e4570d457fb8a) - Update logos for v4. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#6097](https://github.com/gradio-app/gradio/pull/6097) [`439efa39d`](https://github.com/gradio-app/gradio/commit/439efa39dd47bd0c5235f74244aae539d0629492) - Add banner for 4.0 livestream to website. Thanks [@aliabd](https://github.com/aliabd)! +- [#6040](https://github.com/gradio-app/gradio/pull/6040) [`5524e5905`](https://github.com/gradio-app/gradio/commit/5524e590577769b0444a5332b8d444aafb0c5c12) - playground proposal. Thanks [@pngwn](https://github.com/pngwn)! + +### Fixes + +- [#6046](https://github.com/gradio-app/gradio/pull/6046) [`dbb7de5e0`](https://github.com/gradio-app/gradio/commit/dbb7de5e02c53fee05889d696d764d212cb96c74) - fix tests. Thanks [@pngwn](https://github.com/pngwn)! +- [#6052](https://github.com/gradio-app/gradio/pull/6052) [`8241f9a7b`](https://github.com/gradio-app/gradio/commit/8241f9a7bd034256aabb9efa9acb9e36216557ac) - Updated the twitter logo to its latest logo (X). Thanks [@niranjan-kurhade](https://github.com/niranjan-kurhade)! + +## 0.10.0 + +### Features + +- [#6021](https://github.com/gradio-app/gradio/pull/6021) [`86cff0c29`](https://github.com/gradio-app/gradio/commit/86cff0c293db776c08c1ab372d69aac7c594cfb3) - Playground: Better spacing on navbar. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.9.0 + +### Features + +- [#5386](https://github.com/gradio-app/gradio/pull/5386) [`0312c990f`](https://github.com/gradio-app/gradio/commit/0312c990fbe63fdf3bfa9a8f13bbc042295d49bf) - Playground v1. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.8.0 + +### Features + +- [#5936](https://github.com/gradio-app/gradio/pull/5936) [`b8b9f6d27`](https://github.com/gradio-app/gradio/commit/b8b9f6d27e258256584b7662d03110cc2eeb883b) - Adds a Guide on how to stylize the DataFrame component. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.7.1 + +### Features + +- [#5721](https://github.com/gradio-app/gradio/pull/5721) [`84e03fe50`](https://github.com/gradio-app/gradio/commit/84e03fe506e08f1f81bac6d504c9fba7924f2d93) - Adds copy buttons to website, and better descriptions to API Docs. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.7.0 + +### Features + +- [#5643](https://github.com/gradio-app/gradio/pull/5643) [`f661c0733`](https://github.com/gradio-app/gradio/commit/f661c0733b501f1a54a0c62af2567909c7202944) - Add the brand assets page to the website. Thanks [@whitphx](https://github.com/whitphx)! +- [#5675](https://github.com/gradio-app/gradio/pull/5675) [`b619e6f6e`](https://github.com/gradio-app/gradio/commit/b619e6f6e4ca55334fb86da53790e45a8f978566) - Reorganize Docs Navbar and Fill in Gaps. Thanks [@aliabd](https://github.com/aliabd)! +- [#5669](https://github.com/gradio-app/gradio/pull/5669) [`c5e969559`](https://github.com/gradio-app/gradio/commit/c5e969559612f956afcdb0c6f7b22ab8275bc49a) - Fix small issues in docs and guides. Thanks [@aliabd](https://github.com/aliabd)! + +### Fixes + +- [#5608](https://github.com/gradio-app/gradio/pull/5608) [`eebf9d71f`](https://github.com/gradio-app/gradio/commit/eebf9d71f90a83bd84b62c855fdcd13b086f7ad5) - Styling fixes to guides. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.6.0 + +### Features + +- [#5565](https://github.com/gradio-app/gradio/pull/5565) [`f0514fc49`](https://github.com/gradio-app/gradio/commit/f0514fc49ea04ba01dce748238e1fd16f9cb5d8b) - Route docs and guide urls correctly. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.5.0 + +### Features + +- [#5481](https://github.com/gradio-app/gradio/pull/5481) [`df623e74`](https://github.com/gradio-app/gradio/commit/df623e743aad4b21a7eda9bae4c03eb17f01c90d) - Toggle main vs versioned demos on website and show install snippet. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.4.0 + +### Features + +- [#5423](https://github.com/gradio-app/gradio/pull/5423) [`bb31cd7d`](https://github.com/gradio-app/gradio/commit/bb31cd7dd0dc60c18b2b21269512775f3784ef01) - Remove stable diffusion demo from landing page. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.3.0 + +### Features + +- [#5271](https://github.com/gradio-app/gradio/pull/5271) [`97c3c7b1`](https://github.com/gradio-app/gradio/commit/97c3c7b1730407f9e80566af9ecb4ca7cccf62ff) - Move scripts from old website to CI. Thanks [@aliabd](https://github.com/aliabd)! +- [#5381](https://github.com/gradio-app/gradio/pull/5381) [`3d66e61d`](https://github.com/gradio-app/gradio/commit/3d66e61d641da8ca2a7d10c545c7dc0139697f00) - chore(deps): update dependency hast-util-to-string to v3. Thanks [@renovate](https://github.com/apps/renovate)! + +### Fixes + +- [#5304](https://github.com/gradio-app/gradio/pull/5304) [`05892302`](https://github.com/gradio-app/gradio/commit/05892302fb8fe2557d57834970a2b65aea97355b) - Adds kwarg to disable html sanitization in `gr.Chatbot()`. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! + +## 0.2.2 + +### Features + +- [#5284](https://github.com/gradio-app/gradio/pull/5284) [`5f25eb68`](https://github.com/gradio-app/gradio/commit/5f25eb6836f6a78ce6208b53495a01e1fc1a1d2f) - Minor bug fix sweep. Thanks [@aliabid94](https://github.com/aliabid94)!/n - Our use of **exit** was catching errors and corrupting the traceback of any component that failed to instantiate (try running blocks_kitchen_sink off main for an example). Now the **exit** exits immediately if there's been an exception, so the original exception can be printed cleanly/n - HighlightedText was rendering weird, cleaned it up + +## 0.2.1 + +### Fixes + +- [#5324](https://github.com/gradio-app/gradio/pull/5324) [`31996c99`](https://github.com/gradio-app/gradio/commit/31996c991d6bfca8cef975eb8e3c9f61a7aced19) - ensure login form has correct styles. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.2.0 + +### Highlights + +#### Improve startup performance and markdown support ([#5279](https://github.com/gradio-app/gradio/pull/5279) [`fe057300`](https://github.com/gradio-app/gradio/commit/fe057300f0672c62dab9d9b4501054ac5d45a4ec)) + +##### Improved markdown support + +We now have better support for markdown in `gr.Markdown` and `gr.Dataframe`. Including syntax highlighting and Github Flavoured Markdown. We also have more consistent markdown behaviour and styling. + +##### Various performance improvements + +These improvements will be particularly beneficial to large applications. + +- Rather than attaching events manually, they are now delegated, leading to a significant performance improvement and addressing a performance regression introduced in a recent version of Gradio. App startup for large applications is now around twice as fast. +- Optimised the mounting of individual components, leading to a modest performance improvement during startup (~30%). +- Corrected an issue that was causing markdown to re-render infinitely. +- Ensured that the `gr.3DModel` does re-render prematurely. + +Thanks [@pngwn](https://github.com/pngwn)! + +### Features + +- [#5298](https://github.com/gradio-app/gradio/pull/5298) [`cf167cd1`](https://github.com/gradio-app/gradio/commit/cf167cd1dd4acd9aee225ff1cb6fac0e849806ba) - Create event listener table for components on docs. Thanks [@aliabd](https://github.com/aliabd)! +- [#5092](https://github.com/gradio-app/gradio/pull/5092) [`643442e1`](https://github.com/gradio-app/gradio/commit/643442e1a5e25fc0c89a15a38b6279b8955643ac) - generate docs json in ci, reimplement main vs release. Thanks [@pngwn](https://github.com/pngwn)! +- [#5186](https://github.com/gradio-app/gradio/pull/5186) [`24b66e1c`](https://github.com/gradio-app/gradio/commit/24b66e1cff0452bce71c71cea1b818913aeb8d51) - homepage demo update. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.1.0 + +### Features + +- [#5076](https://github.com/gradio-app/gradio/pull/5076) [`2745075a`](https://github.com/gradio-app/gradio/commit/2745075a26f80e0e16863d483401ff1b6c5ada7a) - Add deploy_discord to docs. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Fixes + +- [#5111](https://github.com/gradio-app/gradio/pull/5111) [`b84a35b7`](https://github.com/gradio-app/gradio/commit/b84a35b7b91eca947f787648ceb361b1d023427b) - Add icon and link to DuplicateButton. Thanks [@aliabd](https://github.com/aliabd)! +- [#5037](https://github.com/gradio-app/gradio/pull/5037) [`42488c07`](https://github.com/gradio-app/gradio/commit/42488c076aaf3ac2302b27760773a87f5b6ecc41) - Correct gradio version on website. Thanks [@aliabd](https://github.com/aliabd)! + +## 0.0.2 + +### Features + +- [#5009](https://github.com/gradio-app/gradio/pull/5009) [`3e70fc81`](https://github.com/gradio-app/gradio/commit/3e70fc81fc12dcb07f40a280b972a61348c9d263) - Correctly render changelog on website after new formatting. Thanks [@aliabd](https://github.com/aliabd)! + +### Fixes + +- [#5007](https://github.com/gradio-app/gradio/pull/5007) [`71c90394`](https://github.com/gradio-app/gradio/commit/71c90394012a9cfe10eae312b437a6deff52da3a) - Make sure tags aren't rendered inside a guide. Thanks [@aliabd](https://github.com/aliabd)! \ No newline at end of file diff --git a/js/_website/generate_jsons/check_lite_demos.py b/js/_website/generate_jsons/check_lite_demos.py new file mode 100644 index 0000000..1a34547 --- /dev/null +++ b/js/_website/generate_jsons/check_lite_demos.py @@ -0,0 +1,22 @@ +import os + +def main(): + demo_to_reqs = {} + for demo in os.listdir("demo"): + requirements = "" + # only check demos with requirements, because we don't need to check the others + if os.path.exists(os.path.join("demo", demo, "requirements.txt")): + with open(os.path.join("demo", demo, "requirements.txt"), "r") as f: + requirements = f.read() + demo_to_reqs[demo] = requirements.split("\n") + non_lite_reqs = ['spacy', 'cmake', 'opencv-python-headless', 'torch==2.5.1', 'open3d', 'tensorflow', 'torch>=2.3.1', 'psycopg2', 'torchvision==0.13.0', 'onnxruntime-gpu', 'torchaudio==0.12.0', 'xgboost==1.7.6', 'torch', 'safetensors==0.4.3', 'torchaudio', 'torch==1.12.0', 'shap', 'prophet==1.1.2', 'gradio-pdf==0.0.7', 'datasets', 'gradio-datetimerange', 'safetensors>=0.4.1', 'safetensors>=0.4.1', 'numba>=0.45.1', 'safetensors>=0.4.1', 'safetensors>=0.4.3', 'torch>=2.0.0', 'safetensors>=0.3.1', 'safetensors>=0.3.1', 'safetensors>=0.4.1', 'torch', 'torch>=1.9', 'jiter<1,>=0.4.0', 'jiter<1,>=0.4.0', 'tokenizers<0.22,>=0.21', 'tokenizers<0.22,>=0.21', 'tokenizers<0.22,>=0.21', 'tokenizers<0.22,>=0.21', 'aiortc', 'psutil', 'psutil<6,>=2', 'psutil', 'numba>=0.53', 'zstandard<0.24.0,>=0.23.0', 'combo-lock~=0.2', 'ovos-bus-client<0.2.0,>=0.0.8', 'watchdog', 'combo-lock~=0.2', 'combo-lock~=0.2', 'pyee<12.0.0,>=8.1.0', 'combo-lock<1.0.0,>=0.2.1', 'git+https://github.com/huggingface/parler-tts.git', 'git+https://github.com/huggingface/transformers', 'git+https://github.com/nielsrogge/transformers.git@add_dpt_redesign#egg=transformers'] + non_lite_demos = [] + for demo, requirements in demo_to_reqs.items(): + for req in requirements: + if req in non_lite_reqs: + non_lite_demos.append(demo) + break + print(non_lite_demos) + print(len(non_lite_demos)) +if __name__ == "__main__": + main() diff --git a/js/_website/generate_jsons/chunking.py b/js/_website/generate_jsons/chunking.py new file mode 100644 index 0000000..b0ac2ea --- /dev/null +++ b/js/_website/generate_jsons/chunking.py @@ -0,0 +1,210 @@ +import re +from dataclasses import dataclass +from typing import List, Tuple +from transformers import AutoTokenizer + +@dataclass +class BlogChunks: + title: str + content: List[str] + type: str + url: str + +class TextChunker: + def __init__(self, model_name: str = "voyageai/voyage-3-large"): + """Initialize the chunker with a tokenizer.""" + self.tokenizer = AutoTokenizer.from_pretrained(model_name) + + def count_tokens(self, text: str) -> int: + """Count the number of tokens in a text string.""" + return len(self.tokenizer.encode(text)) + + def find_chunk_boundary(self, text: str, target_tokens: int, overlap_tokens: int) -> Tuple[str, str]: + """ + Find a boundary near the target token count, breaking at sentence boundaries when possible + but enforcing a hard limit of 100 tokens. Includes overlap in the next chunk. + Returns a tuple of (chunk, remainder). + """ + if not text: + return "", "" + + # First try to split by sentence + sentence_pattern = r'(?<=[.!?])\s+(?=[A-Z])' + sentences = re.split(sentence_pattern, text) + + current_chunk = [] + current_tokens = 0 + overlap_start_idx = 0 # Track where to start overlap + + for i, sentence in enumerate(sentences): + sentence_tokens = self.count_tokens(sentence) + + # If this would exceed our hard limit of 100 tokens + if current_tokens + sentence_tokens > target_tokens: + if not current_chunk: + # Need to split the sentence by words + words = sentence.split() + word_chunk = [] + + for word in words: + word_tokens = self.count_tokens(word + ' ') + if current_tokens + word_tokens > target_tokens: + break + word_chunk.append(word) + current_tokens += word_tokens + + if not word_chunk: # If even a single word is too long + return sentence[:target_tokens], sentence[target_tokens:] + + chunk_text = ' '.join(word_chunk) + # Include some of the end of this chunk in the next chunk for overlap + overlap_point = max(0, len(word_chunk) - int(len(word_chunk) * 0.5)) + remainder = ' '.join(words[overlap_point:]) + if i < len(sentences) - 1: + remainder += ' ' + ' '.join(sentences[i+1:]) + return chunk_text, remainder.strip() + + chunk_text = ' '.join(current_chunk) + # Start the next chunk from roughly halfway through this one for overlap + overlap_start_idx = max(0, i - len(current_chunk) // 2) + remainder = ' '.join(sentences[overlap_start_idx:]) + return chunk_text, remainder.strip() + + # If we would exceed the target token count (but not hard limit) + if current_tokens + sentence_tokens > target_tokens and current_chunk: + chunk_text = ' '.join(current_chunk) + # Start the next chunk from roughly halfway through this one for overlap + overlap_start_idx = max(0, i - len(current_chunk) // 2) + remainder = ' '.join(sentences[overlap_start_idx:]) + return chunk_text, remainder.strip() + + current_chunk.append(sentence) + current_tokens += sentence_tokens + + # If we get here, return the entire text as one chunk + return ' '.join(current_chunk), '' + + def chunk_page( + self, + title: str, + url: str, + content: str, + type: str, + target_length: int = 100, # Token counts for different chunk sizes + overlap_percentage: float = 0.5 # 20% overlap by default + ) -> BlogChunks: + """ + Chunks document content with specified overlap percentage and multiple target lengths. + Returns a dictionary mapping target length to list of chunks. + """ + # Clean the content first + content = self._clean_content(content) + + # Dictionary to store chunks for each target length + chunks = BlogChunks( + title=title, + content=[], + type=type, + url=url + ) + + overlap_tokens = int(target_length * overlap_percentage) + remaining_text = content + current_section = None + + while remaining_text: + # Handle section headers + if remaining_text.lstrip().startswith('#'): + section_end = remaining_text.find('\n') + if section_end == -1: + break + current_section = remaining_text[:section_end].lstrip('#').strip() + remaining_text = remaining_text[section_end:].strip() + continue + + # Find natural break point + chunk_text, remaining_text = self.find_chunk_boundary( + remaining_text, + target_length, + overlap_tokens + ) + + if chunk_text: + chunks.content.append(chunk_text) + + if not remaining_text: + break + for i, chunk in enumerate(chunks.content): + if "Demos" in chunk and "demo.launch()" in chunk: + chunks.content[i] = chunk.split("Demos")[0] + chunk.split("demo.launch()")[1] + if "Open in" in chunk and "demo.launch()" in chunk: + chunks.content.pop(i) + + print(f"\nChunked: {title}") + # print(f"\n\n\n{'*'*50}") + # print(f"Target Length: {target_length}") + # print(f"\n{'*'*50}") + # print(f"{'='*50}") + # for chunk in all_chunks: + # print(f"Length: {self.count_tokens(chunk.content)} tokens") + # print(f"Content: {chunk.content}") + # print(f"{'='*50}") + # print(f"\n\n\n{'*'*50}") + # print([chunk.content for chunk in all_chunks]) + + return chunks + + def _clean_content(self, content: str) -> str: + """Clean the content by removing code blocks and markdown links.""" + # Remove triple backtick code blocks with optional language + content = re.sub(r'```(?:[a-zA-Z]*\s*)?[\s\S]*?```', '', content) + + content = re.sub(r'`{1,2}\w*\n[\s\S]*?(?:`{1,2})', '', content) + + # Remove any remaining single or double backtick blocks + content = re.sub(r'``[^`]*(?:`[^`]+`)*[^`]*``', '', content) # Double backticks + content = re.sub(r'`[^`]*`', '', content) # Single backticks + + # Remove markdown links but keep text + content = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', content) + + # Replace common HTML entities with their readable equivalents + html_entities = { + '"': '"', + ''': "'", + ''': "'", + '<': '<', + '>': '>', + '&': '&', + '–': '-', + '—': '--', + ' ': ' ', + '’': "'", + '‘': "'", + '”': '"', + '“': '"', + '’': "'", + '‘': "'", + '”': '"', + '“': '"', + '…': '...', + ''': "'", + '•': '•', + '·': '·', + '•': '•' + } + + for entity, replacement in html_entities.items(): + content = content.replace(entity, replacement) + + # Also handle numeric entities like " (double quote) + content = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), content) + + # Handle hex entities like " + content = re.sub(r'&#x([0-9a-fA-F]+);', lambda m: chr(int(m.group(1), 16)), content) + + # Normalize whitespace (including handling of newlines) + content = re.sub(r'\s+', ' ', content) + + return content.strip() + \ No newline at end of file diff --git a/js/_website/generate_jsons/embed.py b/js/_website/generate_jsons/embed.py new file mode 100644 index 0000000..e54bc30 --- /dev/null +++ b/js/_website/generate_jsons/embed.py @@ -0,0 +1,161 @@ +import libsql_client as libsql +import os +from openai import OpenAI +import numpy as np +from chunking import TextChunker +from tqdm import tqdm +import requests +import voyageai + +# vo = voyageai.Client(api_key=os.getenv("VOYAGE_API_KEY"),) +url = os.getenv("TURSO_DATABASE_URL") +auth_token = os.getenv("TURSO_AUTH_TOKEN") +db_client = libsql.create_client_sync(url, auth_token=auth_token) + +# openai_deepinfra = OpenAI( +# api_key=os.getenv("DEEPINFRA_API_TOKEN"), +# base_url="https://api.deepinfra.com/v1/openai" +# ) + + +# def embed_and_upload(title, _type, url, contents): +# documents_embeddings = vo.embed(contents, model="voyage-3", input_type="document").embeddings + +# values = [ +# (title, _type, url, content, np.array(embedding, dtype=np.float32)) +# for content, embedding in zip(contents, documents_embeddings) +# ] + +# placeholders = ','.join(['(?, ?, ?, ?, ?)'] * len(contents)) + +# flattened_values = [item for tup in values for item in tup] + +# db_client.execute( +# f"INSERT INTO EMBEDDINGS (title, type, url, content, embedding) VALUES {placeholders}", +# flattened_values +# ) +# return + +# url = "http://localhost:5174/search-api" +# response = requests.get(url) +# data = response.json() + +# guides = [d for d in data if d["type"] == "GUIDE"] +# docs = [d for d in data if d["type"] == "DOCS"] + +# chunker = TextChunker() + +# DOCS_AND_GUIDES_DESCRIPTION_SYSTEM_PROMPT = """ +# You are a helpful assistant that summarizes pages in the Gradio website in only one sentence. +# You are given a page that is either a guide or docs. Both will consist of natural language mixed with python code. +# Your summaries will be used for embedding search that points to the page, so please be concise, accurate and include the most important parts. But it can only be one sentence. +# Your sentence should clarify what type of questions the page answers. +# Do not include 'gr.' before the function or class name. Do not ever use backticks or special code formatting in your response. For example write Interface instead of `Interface`. +# """ + +# def describe_page(content: str): +# description = openai_deepinfra.chat.completions.create( +# model="Qwen/Qwen2.5-72B-Instruct", +# messages=[ +# {"role": "system", "content": DOCS_AND_GUIDES_DESCRIPTION_SYSTEM_PROMPT}, +# {"role": "user", "content": content} +# ], +# ) + +# description = description.choices[0].message.content + +# return description + +# for guide in tqdm(guides[1:]): # ignore weird +# description = describe_page(guide["content"]) +# chunks = chunker.chunk_page(guide["title"], guide["slug"], description, guide["type"]) +# try: +# embed_and_upload(chunks.title, chunks.type, chunks.url, chunks.content) +# except Exception as e: +# print(e) +# db_client.close() +# 1/0 + +# for page in tqdm(docs): +# description = describe_page(guide["content"]) +# chunks = chunker.chunk_page(guide["title"], guide["slug"], description, guide["type"]) +# try: +# embed_and_upload(chunks.title, chunks.type, chunks.url, chunks.content) +# except Exception as e: +# print(e) +# db_client.close() +# 1/0 + + +# demo_descriptions = [] +# def get_demo_descriptions(): +# results = db_client.execute( +# """ +# SELECT +# MIN(id) as id, +# title, +# type, +# url, +# STRING_AGG(content, ' ') as combined_content +# FROM EMBEDDINGS_LLM_250 +# WHERE type = 'DEMO' +# GROUP BY title, type, url; +# """ +# ) +# for result in results: +# demo_descriptions.append( +# { +# "title": result["title"], +# "url": result["url"], +# "content": result["combined_content"] +# } +# ) +# return + +# get_demo_descriptions() + +# for demo in tqdm(demo_descriptions): +# chunks = chunker.chunk_page(demo["title"], demo["url"], demo["content"], "DEMO") +# try: +# embed_and_upload(chunks.title, chunks.type, chunks.url, chunks.content) +# except Exception as e: +# print(e) +# db_client.close() +# 1/0 + +demo_to_reqs = {} +for demo in os.listdir("demo"): + if os.path.exists(os.path.join("demo", demo, "requirements.txt")): + with open(os.path.join("demo", demo, "requirements.txt"), "r") as f: + reqs = f.read() + reqs = reqs.split("\n") + demo_to_reqs[demo] = reqs + +for title, requirements in tqdm(demo_to_reqs.items()): + db_client.execute( + """UPDATE EMBEDDINGS + SET requirements = ? + WHERE type = 'DEMO' AND title = ? + """, + (requirements, title.replace("_", " ").capitalize()) + ) + print(title.replace("_", " ").capitalize()) + +# demo_to_reqs = {} +# for demo in os.listdir("demo"): +# if os.path.exists(os.path.join("demo", demo, "requirements.txt")): +# with open(os.path.join("demo", demo, "requirements.txt"), "r") as f: +# demo_to_reqs[demo] = f.read() + +# for title, requirements in tqdm(demo_to_reqs.items()): +# db_client.execute( +# """UPDATE EMBEDDINGS +# SET requirements = ? +# WHERE type = 'DEMO' AND title = ? +# """, +# (requirements, title.replace("_", " ").capitalize()) +# ) +# print(title.replace("_", " ").capitalize()) + + +db_client.close() diff --git a/js/_website/generate_jsons/generate.py b/js/_website/generate_jsons/generate.py new file mode 100644 index 0000000..234c191 --- /dev/null +++ b/js/_website/generate_jsons/generate.py @@ -0,0 +1,137 @@ +import json +import os +import re +from subprocess import run +import boto3 +from botocore import UNSIGNED +from botocore.client import Config + +from src import changelog, demos, docs, guides + +WEBSITE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +GRADIO_DIR = os.path.abspath(os.path.join(WEBSITE_DIR, "..", "..", "gradio")) +ROOT_DIR = os.path.abspath(os.path.join(WEBSITE_DIR, "..", "..")) + + +def make_dir(root, path): + return os.path.abspath(os.path.join(root, path)) + + +def download_from_s3(bucket_name, s3_folder, local_dir): + print(f"Downloading templates from S3: {bucket_name}/{s3_folder} to {local_dir}") + s3 = boto3.client("s3", config=Config(signature_version=UNSIGNED)) + + try: + objects = s3.list_objects_v2(Bucket=bucket_name, Prefix=s3_folder) + except Exception as e: + print(f"Error listing objects in bucket {bucket_name}: {e}") + return + + for obj in objects.get("Contents", []): + s3_key = obj["Key"] + local_file_path = os.path.join(local_dir, os.path.relpath(s3_key, s3_folder)) + + if s3_key.endswith("/"): + continue + + try: + os.makedirs(os.path.dirname(local_file_path), exist_ok=True) + s3.download_file(bucket_name, s3_key, local_file_path) + except Exception as e: + print(f"Error downloading {s3_key}: {e}") + + print(f"Download process completed for {bucket_name}/{s3_folder}") + + +def convert_to_pypi_prerelease(version: str) -> str: + def replacement(match): + v, tag, tag_version = match.groups() + if tag == "beta": + return f"{v}b{tag_version}" + elif tag == "alpha": + return f"{v}a{tag_version}" + else: + return version + + return re.sub(r"(\d+\.\d+\.\d+)-([-a-z]+)\.(\d+)", replacement, version) + + +def get_latest_release(): + with open(make_dir(ROOT_DIR, "client/js/package.json")) as f: + js_client_version = json.load(f)["version"] + with open(make_dir(GRADIO_DIR, "package.json")) as f: + version = convert_to_pypi_prerelease(json.load(f)["version"]) + with open(make_dir(WEBSITE_DIR, "src/lib/json/version.json"), "w+") as j: + json.dump({"version": version}, j) + with open(make_dir(WEBSITE_DIR, "src/lib/json/wheel.json"), "w+") as j: + sha = ( + run(["git", "log", "-1", "--format='%H'"], capture_output=True) + .stdout.decode("utf-8") + .strip("'\n") + ) + json.dump( + { + "gradio_install": f"pip install https://gradio-builds.s3.amazonaws.com/{sha}/gradio-{version}-py3-none-any.whl", + "gradio_py_client_install": f"pip install 'gradio-client @ git+https://github.com/gradio-app/gradio@{sha}#subdirectory=client/python'", + "gradio_js_client_install": f"npm install https://gradio-builds.s3.amazonaws.com/{sha}/gradio-client-{js_client_version}.tgz", + "gradio_lite_url": f"https://gradio-lite-previews.s3.amazonaws.com/PINNED_HF_HUB", + }, + j, + ) + if not os.path.exists( + make_dir(WEBSITE_DIR, f"src/lib/templates_{version.replace('.', '-')}") + ): + print(f"Downloading templates from S3: {version}") + download_from_s3( + "gradio-docs-json", + f"{version}/templates/", + make_dir(WEBSITE_DIR, f"src/lib/templates_{version.replace('.', '-')}"), + ) + + print("Downloading templates from S3: 4.44.1") + download_from_s3( + "gradio-docs-json", + "4.44.1/templates/", + make_dir(WEBSITE_DIR, "src/lib/templates_4-44-1"), + ) + + print("Downloading templates from S3: 5.49.1") + download_from_s3( + "gradio-docs-json", + "5.49.1/templates/", + make_dir(WEBSITE_DIR, "src/lib/templates_5-49-1"), + ) + + +def create_dir_if_not_exists(path): + if not os.path.exists(path): + os.makedirs(path) + + +create_dir_if_not_exists(make_dir(WEBSITE_DIR, "src/lib/json")) +create_dir_if_not_exists(make_dir(WEBSITE_DIR, "src/lib/json/guides")) + +demos.generate(make_dir(WEBSITE_DIR, "src/lib/json/demos.json")) +guides.generate(make_dir(WEBSITE_DIR, "src/lib/json/guides/") + "/") +SYSTEM_PROMPT, FALLBACK_PROMPT = docs.generate( + make_dir(WEBSITE_DIR, "src/lib/json/docs.json") +) +_, _ = docs.generate(make_dir(WEBSITE_DIR, "src/lib/templates/docs.json")) +changelog.generate(make_dir(WEBSITE_DIR, "src/lib/json/changelog.json")) +get_latest_release() + +# print("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n") +# print(SYSTEM_PROMPT) +# print("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n") + + +with open(make_dir(WEBSITE_DIR, "src/lib/json/system_prompt.json"), "w+") as f: + json.dump( + { + "SYSTEM": SYSTEM_PROMPT, + "FALLBACK": FALLBACK_PROMPT, + }, + f, + ) + +print("JSON generated! " + make_dir(WEBSITE_DIR, "src/lib/json/")) diff --git a/js/_website/generate_jsons/src/changelog/__init__.py b/js/_website/generate_jsons/src/changelog/__init__.py new file mode 100644 index 0000000..bd16053 --- /dev/null +++ b/js/_website/generate_jsons/src/changelog/__init__.py @@ -0,0 +1,25 @@ +import json +import os +import re + +DIR = os.path.dirname(__file__) +CHANGELOG_FILE = os.path.abspath(os.path.join(DIR, "../../../../../CHANGELOG.md")) + + +def clean(): + with open(CHANGELOG_FILE) as change_file: + content = change_file.read() + + # remove empty/unused sections + content = re.sub(r"## [\w^:\n ]*No changes to highlight.", "", content) + content = content.replace("# gradio", "# Changelog") + + return content + + +def generate(json_path): + content = clean() + with open(json_path, 'w+') as f: + json.dump({ + "content": content, + }, f) \ No newline at end of file diff --git a/js/_website/generate_jsons/src/demos/__init__.py b/js/_website/generate_jsons/src/demos/__init__.py new file mode 100644 index 0000000..c8280b2 --- /dev/null +++ b/js/_website/generate_jsons/src/demos/__init__.py @@ -0,0 +1,157 @@ +import json +from pathlib import Path + + +HERE = Path(__file__).parent +WEBSITE_DIR = HERE.joinpath("../../..").resolve() +PROJECT_ROOT = WEBSITE_DIR.joinpath("../..").resolve() +GRADIO_DEMO_DIR = PROJECT_ROOT / "demo" + + +def get_code_description_and_reqs(demo_dir): + with demo_dir.joinpath("run.py").open() as f: + code = f.read() + + description = "" + description_path = demo_dir.joinpath("DESCRIPTION.md") + if description_path.exists(): + with description_path.open() as f: + description = f.read() + + requirements = [] + requirements_path = demo_dir.joinpath("requirements.txt") + if requirements_path.exists(): + with requirements_path.open() as f: + requirements = [line.strip() for line in f.read().strip().split("\n")] + + return code, description, requirements + + +demos_by_category = [ + { + "category": "Text", + "demos": [ + { + "name": "Hello World", + "dir": "hello_world", + }, + { + "name": "Hello Blocks", + "dir": "hello_blocks", + }, + { + "name": "Sentence Builder", + "dir": "sentence_builder", + }, + { + "name": "Diff Texts", + "dir": "diff_texts", + }, + ], + }, + { + "category": "Media", + "demos": [ + { + "name": "Sepia Filter", + "dir": "sepia_filter", + }, + { + "name": "Video Identity", + "dir": "video_identity_2", + }, + { + "name": "Iterative Output", + "dir": "fake_diffusion", + }, + { + "name": "Generate Tone", + "dir": "generate_tone", + }, + ], + }, + { + "category": "Tabular", + "demos": [ + {"name": "Filter Records", "dir": "filter_records"}, + {"name": "Transpose Matrix", "dir": "matrix_transpose"}, + {"name": "Tax Calculator", "dir": "tax_calculator"}, + { + "name": "Kinematics", + "dir": "blocks_kinematics", + }, + { + "name": "Stock Forecast", + "dir": "stock_forecast", + }, + ], + }, + { + "category": "Chatbots", + "demos": [ + { + "name": "Chatbot", + "dir": "chatinterface_random_response", + }, + { + "name": "Streaming Chatbot", + "dir": "chatinterface_streaming_echo", + }, + { + "name": "Chatbot with Tools", + "dir": "chatbot_with_tools", + }, + { + "name": "Chatinterface with Code", + "dir": "chatinterface_artifacts", + }, + ], + }, + { + "category": "Other", + "demos": [ + { + "name": "Tabbed Interface", + "dir": "tabbed_interface_lite", + }, + { + "name": "Layouts", + "dir": "blocks_flipper", + }, + { + "name": "Error", + "dir": "calculator", + }, + { + "name": "Chained Events", + "dir": "blocks_chained_events", + }, + { + "name": "Change Listener", + "dir": "blocks_hello", + }, + ], + }, +] + + +for category in demos_by_category: + for demo in category["demos"]: + base_dir = GRADIO_DEMO_DIR + demo_dir = base_dir / demo["dir"] + + code, description, requirements = get_code_description_and_reqs(demo_dir) + demo["code"] = code.replace("# type: ignore", "") + demo["text"] = description + demo["requirements"] = requirements + + +def generate(json_path): + with open(json_path, "w+") as f: + json.dump(demos_by_category, f) + + +if __name__ == "__main__": + dest_path = WEBSITE_DIR / "src/lib/json/demos.json" + print(f"Generating {dest_path}") + generate(dest_path) diff --git a/js/_website/generate_jsons/src/docs/__init__.py b/js/_website/generate_jsons/src/docs/__init__.py new file mode 100644 index 0000000..02238eb --- /dev/null +++ b/js/_website/generate_jsons/src/docs/__init__.py @@ -0,0 +1,423 @@ +import html +import json +import os +import re +import requests +import base64 +import urllib.parse + + + +from gradio_client.documentation import document_cls, generate_documentation +import gradio +from ..guides import guides, guide_names + +DIR = os.path.dirname(__file__) +DEMOS_DIR = os.path.abspath(os.path.join(DIR, "../../../../../demo")) +JS_CLIENT_README = os.path.abspath(os.path.join(DIR, "../../../../../client/js/README.md")) +JS_DIR = os.path.abspath(os.path.join(DIR, "../../../../../js/")) +TEMPLATES_DIR = os.path.abspath(os.path.join(DIR, "../../../src/lib/templates")) + +docs = generate_documentation() + +def add_component_shortcuts(): + for component in docs["component"]: + if not getattr(component["class"], "allow_string_shortcut", True): + continue + component["string_shortcuts"] = [ + ( + component["class"].__name__, + component["name"].lower(), + "Uses default values", + ) + ] + if not hasattr(component["class"], "__subclasses__"): + continue + for subcls in component["class"].__subclasses__(): + if getattr(subcls, "is_template", False): + _, tags, _ = document_cls(subcls) + component["string_shortcuts"].append( + ( + subcls.__name__, + subcls.__name__.lower(), + "Uses " + tags.get("sets", "default values"), + ) + ) + + +add_component_shortcuts() + + +def add_demos(): + for mode in docs: + for cls in docs[mode]: + if "demos" not in cls["tags"]: + continue + cls["demos"] = [] + demos = [demo.strip() for demo in cls["tags"]["demos"].split(",")] + for demo in demos: + demo_file = os.path.join(DEMOS_DIR, demo, "run.py") + with open(demo_file) as run_py: + demo_code = run_py.read() + demo_code = demo_code.replace("# type: ignore", "") + cls["demos"].append((demo, demo_code)) + + +add_demos() + +def create_events_matrix(): + events = set({}) + component_events = {} + for component in docs["component"]: + component_event_list = [] + if hasattr(component["class"], 'EVENTS'): + for event in component["class"].EVENTS: + events.add(event) + for fn in component["fns"]: + if event == fn["name"]: + component_event_list.append(event) + component_events[component["name"]] = component_event_list + + + return list(events), component_events + +events, component_events = create_events_matrix() + + +def add_guides(): + for mode in docs: + for cls in docs[mode]: + if "guides" not in cls["tags"]: + continue + cls["guides"] = [] + docstring_guides = [ + guide.strip() for guide in cls["tags"]["guides"].split(",") + ] + for docstring_guide in docstring_guides: + for guide in guides: + if docstring_guide == guide["name"]: + cls["guides"].append(guide) + + +add_guides() + + +def escape_parameters(parameters): + new_parameters = [] + for param in parameters: + param = param.copy() # Manipulating the list item directly causes issues, so copy it first + param["doc"] = html.escape(param["doc"]) if param["doc"] else param["doc"] + new_parameters.append(param) + assert len(new_parameters) == len(parameters) + return new_parameters + + +def escape_html_string_fields(): + for mode in docs: + for cls in docs[mode]: + for tag in [ + "preprocessing", + "postprocessing", + "examples-format", + "events", + ]: + if tag not in cls["tags"]: + continue + cls["tags"][tag] = html.escape(cls["tags"][tag]) + + cls["parameters"] = escape_parameters(cls["parameters"]) + for fn in cls["fns"]: + fn["description"] = html.escape(fn["description"]) + fn["parameters"] = escape_parameters(fn["parameters"]) + +escape_html_string_fields() + +def find_cls(target_cls): + for mode in docs: + for cls in docs[mode]: + if cls["name"] == target_cls: + return cls + raise ValueError("Class not found") + + +def organize_docs(d): + organized = { + "gradio": { + "building": {}, + "components": {}, + "helpers": {}, + "modals": {}, + "routes": {}, + "events": {}, + "chatinterface": {} + }, + "python-client": {} + } + for mode in d: + for c in d[mode]: + c["parent"] = "gradio" + c["class"] = None + if "returns" in c: + c["returns"]["annotation"] = None + for p in c.get("parameters", []): + p["annotation"] = str(p["annotation"]) + if "default" in p: + p["default"] = str(p["default"]) + for f in c["fns"]: + f["fn"] = None + f["parent"] = "gradio." + c["name"] + for p in f.get("parameters", []): + p["annotation"] = str(p["annotation"]) + if "default" in p: + p["default"] = str(p["default"]) + if mode == "component": + target = organized["gradio"]["components"] + elif mode == "py-client": + target = organized["python-client"] + elif mode in ["helpers", "routes", "chatinterface", "modals"]: + target = organized["gradio"][mode] + else: + target = organized["gradio"]["building"] + key = c["name"].lower() + if key in target: + if c["name"] != c["name"].lower(): + key = key + "-class" + else: + target[key + "-class"] = target.pop(key) + target[key] = c + + + def format_name(page_name): + index = None + page_path = page_name + if re.match("^[0-9]+_", page_name): + index = int(page_name[: page_name.index("_")]) + page_name = page_name[page_name.index("_") + 1 :] + if page_name.lower().endswith(".svx"): + page_name = page_name[:-4] + pretty_page_name = " ".join([word[0].upper() + word[1:] for word in page_name.split("-")]) + for library in organized: + for category in organized[library]: + if page_name in organized[library][category]: + return index, page_name, organized[library][category][page_name]["name"], page_path + if page_name == "chatinterface": + pretty_page_name = "ChatInterface" + return index, page_name, pretty_page_name, page_path + + + def organize_pages(): + pages = {"gradio": [], "python-client": [], "third-party-clients": []} + absolute_index = -1; + for library in pages: + library_templates_dir = os.path.join(TEMPLATES_DIR, library) + page_folders = sorted(os.listdir(library_templates_dir)) + for page_folder in page_folders: + page_list = sorted(os.listdir(os.path.join(library_templates_dir, page_folder))) + _, page_category, pretty_page_category, category_path = format_name(page_folder) + category_path = os.path.join(library, category_path) + pages[library].append({"category": pretty_page_category, "pages": []}) + for page_file in page_list: + page_index, page_name, pretty_page_name, page_path = format_name(page_file) + pages[library][-1]["pages"].append({"name": page_name, "pretty_name": pretty_page_name, "path": os.path.join(category_path, page_path), "page_index": page_index, "abolute_index": absolute_index + 1}) + return pages + + pages = organize_pages() + + organized["gradio"]["events_matrix"] = component_events + organized["gradio"]["events"] = events + + js = {} + js_pages = [] + + for js_component in os.listdir(JS_DIR): + if not js_component.startswith("_") and js_component not in ["app", "highlighted-text", "playground", "preview", "upload-button", "theme", "tootils"]: + if os.path.exists(os.path.join(JS_DIR, js_component, "package.json")): + with open(os.path.join(JS_DIR, js_component, "package.json")) as f: + package_json = json.load(f) + if package_json.get("private", False): + continue + if os.path.exists(os.path.join(JS_DIR, js_component, "README.md")): + with open(os.path.join(JS_DIR, js_component, "README.md")) as f: + readme_content = f.read() + + try: + latest_npm = requests.get(f"https://registry.npmjs.org/@gradio/{js_component}/latest").json()["version"] + latest_npm = f" [v{latest_npm}](https://www.npmjs.com/package/@gradio/{js_component})" + readme_content = readme_content.split("\n") + readme_content = "\n".join([readme_content[0], latest_npm, *readme_content[1:]]) + except TypeError or KeyError: + # KeyError because gradio 5 docs still looking for deprecated components + pass + + js[js_component] = readme_content + js_pages.append(js_component) + + + with open(JS_CLIENT_README) as f: + readme_content = f.read() + js_pages.append("js-client") + + js["js-client"] = readme_content + + js_pages.sort() + + return {"docs": organized, "pages": pages, "js": js, "js_pages": js_pages, "js_client": readme_content} + + +docs = organize_docs(docs) + +gradio_docs = docs["docs"]["gradio"] + +SYSTEM_PROMPT = "" + +FALLBACK_PROMPT = SYSTEM_PROMPT + +FALLBACK_PROMPT += "# API Reference: \n\nBelow are all the class and function signatures in the Gradio library. \n\n" + +for key in gradio_docs: + if key in ["events", "events_matrix"]: + continue + if "name" in key: + o = gradio_docs[key] + signature = f"""{o['name']}({', '.join([ + p['name'] + + ': ' + p['annotation'] + + (' = ' + p['default'] if 'default' in p else '') + for p in o['parameters']])})""" + FALLBACK_PROMPT += f"{signature}\n" + FALLBACK_PROMPT += f"{o['description']}\n\n" + + else: + for c in gradio_docs[key]: + o = gradio_docs[key][c] + signature = f"""{o['name']}({', '.join([ + p['name'] + + ': ' + p['annotation'] + + (' = ' + p['default'] if 'default' in p else '') + for p in o['parameters']])})""" + FALLBACK_PROMPT += f"{signature}\n" + FALLBACK_PROMPT += f"{o['description']}\n\n" + if "fns" in o and key != "components": + for f in o["fns"]: + signature = f"""{o['name']}.{f['name']}({', '.join([ + p['name'] + + ': ' + p['annotation'] + + (' = ' + p['default'] if 'default' in p else '') + for p in f['parameters']])})""" + FALLBACK_PROMPT += f"{signature}\n" + FALLBACK_PROMPT += f"{f['description']}\n\n" + +FALLBACK_PROMPT += "\nEvent listeners allow Gradio to respond to user interactions with the UI components defined in a Blocks app. When a user interacts with an element, such as changing a slider value or uploading an image, a function is called.\n" + +FALLBACK_PROMPT += "All event listeners have the same signature:\n" + +f = gradio_docs["components"]["audio"]["fns"][0] +signature = f""".({', '.join([ + p['name'] + + ': ' + p['annotation'] + + (' = ' + p['default'] if 'default' in p else '') + for p in f['parameters']])})""" +FALLBACK_PROMPT += signature +FALLBACK_PROMPT += "\nEach component only supports some specific events. Below is a list of all gradio components and every event that each component supports. If an event is supported by a component, it is a valid method of the component." +for component in gradio_docs["events_matrix"]: + FALLBACK_PROMPT += f"{component}: {', '.join(gradio_docs['events_matrix'][component])}\n\n" + +SYSTEM_PROMPT += "Below are examples of full end-to-end Gradio apps:\n\n" +FALLBACK_PROMPT += "End to End Demos: \nBelow are examples of full end-to-end Gradio apps:\n\n" + + +# 'audio_component_events', 'audio_mixer', 'blocks_essay', 'blocks_chained_events', 'blocks_xray', 'chatbot_multimodal', 'sentence_builder', 'custom_css', 'blocks_update', 'fake_gan' +# important_demos = ["annotatedimage_component", "blocks_essay_simple", "blocks_flipper", "blocks_form", "blocks_hello", "blocks_js_load", "blocks_js_methods", "blocks_kinematics", "blocks_layout", "blocks_plug", "blocks_simple_squares", "calculator", "chatbot_consecutive", "chatbot_simple", "chatbot_streaming", "chatinterface_multimodal", "datetimes", "diff_texts", "dropdown_key_up", "fake_diffusion", "fake_gan", "filter_records", "function_values", "gallery_component_events", "generate_tone", "hangman", "hello_blocks", "hello_blocks_decorator", "hello_world", "image_editor", "matrix_transpose", "model3D", "on_listener_decorator", "plot_component", "render_merge", "render_split", "reverse_audio_2", "sales_projections", "sepia_filter", "sort_records", "streaming_simple", "tabbed_interface_lite", "tax_calculator", "theme_soft", "timer", "timer_simple", "variable_outputs", "video_identity"] +important_demos = ['custom_css', "annotatedimage_component", "blocks_essay_simple", "blocks_flipper", "blocks_form", "blocks_hello", "blocks_js_load", "blocks_js_methods", "blocks_kinematics", "blocks_layout", "blocks_plug", "blocks_simple_squares", "calculator", "chatbot_consecutive", "chatbot_simple", "chatbot_streaming", "datetimes", "diff_texts", "dropdown_key_up", "fake_diffusion", "filter_records", "function_values", "gallery_component_events", "generate_tone", "hangman", "hello_blocks", "hello_blocks_decorator", "hello_world", "image_editor", "matrix_transpose", "model3D", "on_listener_decorator", "plot_component", "render_merge", "render_split", "reverse_audio_2", "sepia_filter", "sort_records", "streaming_simple", "tabbed_interface_lite", "tax_calculator", "theme_soft", "timer", "timer_simple", "variable_outputs", "video_identity"] +very_important_demos = ["blocks_essay_simple", "blocks_flipper", "blocks_form", "blocks_hello","reverse_audio_2", "sepia_filter", "sort_records", "streaming_simple", "tabbed_interface_lite", "tax_calculator", "timer_simple", "video_identity"] + +def length(demo): + if os.path.exists(os.path.join(DEMOS_DIR, demo, "run.py")): + demo_file = os.path.join(DEMOS_DIR, demo, "run.py") + else: + return 0 + with open(demo_file) as run_py: + demo_code = run_py.read() + demo_code = demo_code.replace("# type: ignore", "").replace('if __name__ == "__main__":\n ', "") + return len(demo_code) + +# important_demos = sorted(important_demos, key=length, reverse=True) +# print(important_demos) + +for demo in important_demos: + if os.path.exists(os.path.join(DEMOS_DIR, demo, "run.py")): + demo_file = os.path.join(DEMOS_DIR, demo, "run.py") + else: + continue + with open(demo_file) as run_py: + demo_code = run_py.read() + demo_code = demo_code.replace("# type: ignore", "").replace('if __name__ == "__main__":\n ', "") + FALLBACK_PROMPT += f"Name: {demo.replace('_', ' ')}\n" + FALLBACK_PROMPT += "Code: \n\n" + FALLBACK_PROMPT += f"{demo_code}\n\n" + +for demo in very_important_demos: + if os.path.exists(os.path.join(DEMOS_DIR, demo, "run.py")): + demo_file = os.path.join(DEMOS_DIR, demo, "run.py") + else: + continue + with open(demo_file) as run_py: + demo_code = run_py.read() + demo_code = demo_code.replace("# type: ignore", "").replace('if __name__ == "__main__":\n ', "") + SYSTEM_PROMPT += f"Name: {demo.replace('_', ' ')}\n" + SYSTEM_PROMPT += "Code: \n\n" + SYSTEM_PROMPT += f"{demo_code}\n\n" + +guides_llm_text = [ + "gradio-6-migration-guide", + "quickstart", + "the-interface-class", + "blocks-and-event-listeners", + "controlling-layout", + "more-blocks-features", + "custom-CSS-and-JS", + "streaming-outputs", + "streaming-inputs", + "sharing-your-app" + ] + +GUIDES_LLM_TEXT_CONTENT = "" + +for guide_name in guides_llm_text: + for guide in guides: + if guide["name"] == guide_name: + GUIDES_LLM_TEXT_CONTENT += guide["content"] + "\n\n" + break + +INTRO = f"""This page contains the documentation for the Gradio library. It is organized into the following sections: + +- {guides_llm_text[0].replace('-', ' ').title()} +- {guides_llm_text[1].replace('-', ' ').title()} +- {guides_llm_text[2].replace('-', ' ').title()} +- {guides_llm_text[3].replace('-', ' ').title()} +- {guides_llm_text[4].replace('-', ' ').title()} +- {guides_llm_text[5].replace('-', ' ').title()} +- {guides_llm_text[6].replace('-', ' ').title()} +- {guides_llm_text[7].replace('-', ' ').title()} +- {guides_llm_text[8].replace('-', ' ').title()} +- API Reference: This section contains all the class and function signatures in the Gradio library. +- End to End Demos: This section contains examples of full end-to-end Gradio apps. + + +""" + +FALLBACK_PROMPT = INTRO + GUIDES_LLM_TEXT_CONTENT + "\n\n" + FALLBACK_PROMPT + +SYSTEM_PROMPT += "\n\n$INSERT_GUIDES_DOCS_DEMOS" + + +# print("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n") +# print(SYSTEM_PROMPT) +# print("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n") + + + +def generate(json_path): + with open(json_path, "w+") as f: + json.dump(docs, f) + return SYSTEM_PROMPT, FALLBACK_PROMPT \ No newline at end of file diff --git a/js/_website/generate_jsons/src/guides/__init__.py b/js/_website/generate_jsons/src/guides/__init__.py new file mode 100644 index 0000000..5b96497 --- /dev/null +++ b/js/_website/generate_jsons/src/guides/__init__.py @@ -0,0 +1,186 @@ +import json +import os +import re +import markdown + +DIR = os.path.dirname(__file__) +GUIDES_DIR = os.path.abspath(os.path.join(DIR, "../../../../../guides")) +GUIDE_ASSETS_DIR = os.path.join(GUIDES_DIR, "assets") +DEMOS_DIR = os.path.abspath(os.path.join(DIR, "../../../../../demo")) +CN_GUIDES_DIR = os.path.abspath(os.path.join(DIR, "../../../../../guides/cn")) + +UNDERSCORE_TOKEN = "!UNDERSCORE!" + +demos = {} +for demo_folder in os.listdir(DEMOS_DIR): + runfile = os.path.join(DEMOS_DIR, demo_folder, "run.py") + if not os.path.exists(runfile): + continue + with open(runfile) as run_py: + demos[demo_folder] = ( + run_py.read() + .replace('if __name__ == "__main__":\n demo.launch()', "demo.launch()") + .replace("# type: ignore", "") + ) + + +def format_name(guide_name): + index = None + if re.match("^[0-9]+_", guide_name): + index = int(guide_name[: guide_name.index("_")]) + guide_name = guide_name[guide_name.index("_") + 1 :] + if guide_name.lower().endswith(".md"): + guide_name = guide_name[:-3] + pretty_guide_name = " ".join( + [word[0].upper() + word[1:] for word in guide_name.split("-")] + ) + return index, guide_name, pretty_guide_name + + +guide_folders = sorted(os.listdir(GUIDES_DIR)) +guide_folders.remove("CONTRIBUTING.md") +guide_folders.remove("assets") +guide_folders.remove("cn") + +guides = [] +guides_by_category = [] +guide_names = [] +guide_urls = [] +absolute_index = 0 +for guide_folder in guide_folders: + guide_list = sorted(os.listdir(os.path.join(GUIDES_DIR, guide_folder))) + _, guide_category, pretty_guide_category = format_name(guide_folder) + guides_by_category.append({"category": pretty_guide_category, "guides": []}) + guide_names.append({"category": pretty_guide_category, "guides": []}) + for guide_file in guide_list: + guide_index, guide_name, pretty_guide_name = format_name(guide_file) + with open(os.path.join(GUIDES_DIR, guide_folder, guide_file)) as f: + guide_content = f.read() + + title = guide_content.split("\n")[0] + + metadata_labels = [] + + def get_labeled_metadata(label, is_list=True): + global guide_content + metadata_labels.append(label) + full_label = label + " " + metadata = [] if is_list else None + if full_label in guide_content: + metadata = guide_content.split(full_label)[1].split("\n")[0] + guide_content = guide_content.replace(full_label + metadata, "") + if is_list: + metadata = metadata.split(", ") + return metadata + + tags = get_labeled_metadata("Tags:") + spaces = get_labeled_metadata("Related spaces:") + contributor = get_labeled_metadata("Contributed by", is_list=False) + + url = f"/guides/{guide_name}/" + guide_content = re.sub( + r"\$code_([a-z _\-0-9]+)", + lambda x: f"```python\n{demos[x.group(1)]}\n```", + guide_content, + ) + guide_content = re.sub( + r"\$demo_([a-z _\-0-9]+)", + lambda x: f"", + guide_content, + ) + + guide_content = re.sub( + r"\n\nTip: (.*?)(?=\n\n|$)", + lambda x: f""" +
+ + + + + +
{markdown.markdown(x.group(1))}
+
+ """, + guide_content, + ) + + guide_content = re.sub( + r"(\n\nWarning: )(.*?)(?=\n\n|$)", + lambda x: f""" +
+ + + + + Warning: + + {markdown.markdown(x.group(2))} +
+ """, + guide_content, + ) + + content_no_html = guide_content + + guide_content = "\n".join( + [ + line + for i, line in enumerate(guide_content.split("\n")) + if not any(line.startswith(label) for label in metadata_labels) + ] + ) + + guide_content = re.sub( + r"```([a-z]+)\n", + lambda x: f"
",
+            guide_content,
+        )
+        guide_content = re.sub(r"```", "
", guide_content) + guide_content = re.sub( + r"\$code_([a-z _\-0-9]+)", + lambda x: f"
{demos[x.group(1)]}
", + guide_content, + ) + guide_content = re.sub( + r"\$demo_([a-z _\-0-9]+)", + lambda x: f"", + guide_content, + ) + + guide_data = { + "name": guide_name, + "category": guide_category, + "pretty_category": pretty_guide_category, + "guide_index": guide_index, + "absolute_index": absolute_index, + "pretty_name": pretty_guide_name, + "content": content_no_html, + "tags": tags, + "spaces": spaces, + "url": url, + "contributor": contributor, + } + guides.append(guide_data) + guides_by_category[-1]["guides"].append(guide_data) + guide_names[-1]["guides"].append( + {"name": guide_name, "pretty_name": pretty_guide_name, "url": url} + ) + guide_urls.append(guide_name) + absolute_index += 1 + + +def generate(json_path): + if not os.path.isdir(json_path): + os.mkdir(json_path) + with open(json_path + "guides_by_category.json", "w+") as f: + json.dump( + { + "guides_by_category": guides_by_category, + }, + f, + ) + for guide in guides: + with open(json_path + guide["name"] + ".json", "w+") as f: + json.dump({"guide": guide}, f) + with open(json_path + "guide_names.json", "w+") as f: + json.dump({"guide_names": guide_names, "guide_urls": guide_urls}, f) diff --git a/js/_website/package.json b/js/_website/package.json new file mode 100644 index 0000000..efcb84b --- /dev/null +++ b/js/_website/package.json @@ -0,0 +1,46 @@ +{ + "name": "website", + "version": "0.79.0", + "private": true, + "scripts": { + "dev": "pip install boto3 markdown && python generate_jsons/generate.py && python ../../scripts/generate_theme.py --website --outfile ./src/lib/assets/theme.css && vite dev", + "build": "vite build", + "preview": "vite preview", + "deploy": "wrangler deploy", + "dev:worker": "wrangler dev", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "prepare": "svelte-kit sync" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^6.1.1", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.46.2", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.19", + "@types/prismjs": "^1.26.5", + "flexsearch": "^0.8.212", + "prismjs": "^1.30.0", + "tailwindcss": "^3.1.6" + }, + "type": "module", + "engines": { + "node": ">=22.0.0" + }, + "dependencies": { + "@gradio/code": "workspace:^", + "@gradio/html": "workspace:^", + "@gradio/paramviewer": "workspace:^", + "@gradio/statustracker": "workspace:^", + "@gradio/tabitem": "workspace:^", + "@gradio/tabs": "workspace:^", + "@gradio/button": "workspace:^", + "@sindresorhus/slugify": "^3.0.0", + "@sveltejs/adapter-vercel": "^5.10.3", + "hast-util-to-string": "^3.0.1", + "mdsvex": "^0.12.6", + "postcss": "^8.5.6", + "prism-svelte": "^0.5.0", + "wrangler": "^4.42.1" + } +} diff --git a/js/_website/postcss.config.cjs b/js/_website/postcss.config.cjs new file mode 100644 index 0000000..0794fa7 --- /dev/null +++ b/js/_website/postcss.config.cjs @@ -0,0 +1,4 @@ +module.exports = { + extract: "themes.css", + plugins: [require("tailwindcss/nesting"), require("tailwindcss")] +}; diff --git a/js/_website/src/app.d.ts b/js/_website/src/app.d.ts new file mode 100644 index 0000000..ca07268 --- /dev/null +++ b/js/_website/src/app.d.ts @@ -0,0 +1,14 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +declare module "*.json"; + +export {}; diff --git a/js/_website/src/app.html b/js/_website/src/app.html new file mode 100644 index 0000000..21e6447 --- /dev/null +++ b/js/_website/src/app.html @@ -0,0 +1,33 @@ + + + + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + diff --git a/js/_website/src/hooks.server.ts b/js/_website/src/hooks.server.ts new file mode 100644 index 0000000..e26cabb --- /dev/null +++ b/js/_website/src/hooks.server.ts @@ -0,0 +1,147 @@ +import type { Handle } from "@sveltejs/kit"; +import { readFileSync } from "fs"; +import { resolve as pathResolve } from "path"; + +const AI_UA_PATTERNS: RegExp[] = [ + /\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 +]; + +export function isLLMRequest(req: Request): boolean { + const ua = req.headers.get("user-agent") || ""; + const accept = req.headers.get("accept") || ""; + + if (accept.includes("text/markdown")) { + return true; + } + + return AI_UA_PATTERNS.some((re) => re.test(ua)); +} + +function parseDocsPath( + pathname: string +): { version: string | null; doc: string } | null { + const match = pathname.match( + /^\/(?:([^/]+)\/)?docs\/gradio\/([a-z_-]+)\/?$/i + ); + + if (match) { + const version = match[1] || null; + const doc = match[2]; + + if (version && !version.match(/^(\d+\.\d+\.\d+|main)$/)) { + return null; + } + + return { version, doc }; + } + + return null; +} + +function parseGuidePath( + pathname: string +): { version: string | null; guide: string } | null { + const match = pathname.match(/^\/(?:([^/]+)\/)?guides\/([a-z0-9_-]+)\/?$/i); + + if (match) { + const version = match[1] || null; + const guide = match[2]; + + if (version && !version.match(/^(\d+\.\d+\.\d+|main)$/)) { + return null; + } + + return { version, guide }; + } + + return null; +} + +export const handle: Handle = async ({ event, resolve }) => { + const pathname = event.url.pathname; + + if (isLLMRequest(event.request)) { + const docsParams = parseDocsPath(pathname); + + if (docsParams) { + try { + const { svxToMarkdown } = await import("$lib/utils/svx-to-markdown"); + + const docsJson = await import("$lib/templates/docs.json"); + const pages = docsJson.pages; + + let svxPath: string | null = null; + for (const category of pages.gradio) { + for (const page of category.pages) { + if (page.name === docsParams.doc) { + svxPath = page.path; + break; + } + } + if (svxPath) break; + } + + if (svxPath) { + const fullPath = pathResolve( + process.cwd(), + "src/lib/templates", + svxPath + ); + const svxContent = readFileSync(fullPath, "utf-8"); + + const markdown = await svxToMarkdown(svxContent, docsParams.doc); + + return new Response(markdown, { + status: 200, + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "X-Robots-Tag": "noindex" + } + }); + } + } catch (error) { + console.error("Error generating markdown for LLM:", error); + } + } + + const guideParams = parseGuidePath(pathname); + + if (guideParams) { + try { + const guideModule = await import( + `$lib/json/guides/${guideParams.guide}.json` + ); + const guideData = guideModule.default || guideModule; + + const markdown = guideData.guide?.content; + + if (markdown) { + return new Response(markdown, { + status: 200, + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "X-Robots-Tag": "noindex" + } + }); + } + } catch (error) { + console.error("Error serving guide markdown for LLM:", error); + } + } + } + return resolve(event); +}; diff --git a/js/_website/src/lib/assets/brand-assets/gradio-logo-with-title.png b/js/_website/src/lib/assets/brand-assets/gradio-logo-with-title.png new file mode 100644 index 0000000..964332e Binary files /dev/null and b/js/_website/src/lib/assets/brand-assets/gradio-logo-with-title.png differ diff --git a/js/_website/src/lib/assets/brand-assets/gradio-logo-with-title.svg b/js/_website/src/lib/assets/brand-assets/gradio-logo-with-title.svg new file mode 100644 index 0000000..ff8df41 --- /dev/null +++ b/js/_website/src/lib/assets/brand-assets/gradio-logo-with-title.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/js/_website/src/lib/assets/brand-assets/gradio-logo.png b/js/_website/src/lib/assets/brand-assets/gradio-logo.png new file mode 100644 index 0000000..e465f2b Binary files /dev/null and b/js/_website/src/lib/assets/brand-assets/gradio-logo.png differ diff --git a/js/_website/src/lib/assets/brand-assets/gradio-logo.svg b/js/_website/src/lib/assets/brand-assets/gradio-logo.svg new file mode 100644 index 0000000..4666c03 --- /dev/null +++ b/js/_website/src/lib/assets/brand-assets/gradio-logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/js/_website/src/lib/assets/copy.js b/js/_website/src/lib/assets/copy.js new file mode 100644 index 0000000..43fa0b3 --- /dev/null +++ b/js/_website/src/lib/assets/copy.js @@ -0,0 +1,4 @@ +export const svgCopy = + ''; +export const svgCheck = + ''; diff --git a/js/_website/src/lib/assets/demo_code.js b/js/_website/src/lib/assets/demo_code.js new file mode 100644 index 0000000..6d26dbb --- /dev/null +++ b/js/_website/src/lib/assets/demo_code.js @@ -0,0 +1,21 @@ +export const hello = `
import gradio as gr
+
+def greet(name):
+    return "Hello " + name + "!"
+
+demo = gr.Interface(fn=greet, inputs="text", outputs="text")
+demo.launch()   
`; + +export const chat = `
import gradio as gr
+def chat(message, history):
+    pass  # Implement your chatbot here...
+
+gr.ChatInterface(fn=chat).launch()
+
`; + +export const stable_diffusion = `
import gradio as gr
+def generate(prompt):
+    pass  # Implement your image generation model here...
+
+gr.Interface(fn=generate, inputs="prompt", outputs="image").launch()
+
`; diff --git a/js/_website/src/lib/assets/gradio.svg b/js/_website/src/lib/assets/gradio.svg new file mode 100644 index 0000000..48bfdc8 --- /dev/null +++ b/js/_website/src/lib/assets/gradio.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/js/_website/src/lib/assets/gradiodark.svg b/js/_website/src/lib/assets/gradiodark.svg new file mode 100644 index 0000000..b2fca32 --- /dev/null +++ b/js/_website/src/lib/assets/gradiodark.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/_website/src/lib/assets/img/LLMGameHub.png b/js/_website/src/lib/assets/img/LLMGameHub.png new file mode 100644 index 0000000..a2e8b7a Binary files /dev/null and b/js/_website/src/lib/assets/img/LLMGameHub.png differ diff --git a/js/_website/src/lib/assets/img/anchor.svg b/js/_website/src/lib/assets/img/anchor.svg new file mode 100644 index 0000000..94393f8 --- /dev/null +++ b/js/_website/src/lib/assets/img/anchor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/img/anchor_gray.svg b/js/_website/src/lib/assets/img/anchor_gray.svg new file mode 100644 index 0000000..882dac9 --- /dev/null +++ b/js/_website/src/lib/assets/img/anchor_gray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/img/anchor_orange.svg b/js/_website/src/lib/assets/img/anchor_orange.svg new file mode 100644 index 0000000..b2d2c16 --- /dev/null +++ b/js/_website/src/lib/assets/img/anchor_orange.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/img/blaxel-logo.png b/js/_website/src/lib/assets/img/blaxel-logo.png new file mode 100644 index 0000000..3a8cf27 Binary files /dev/null and b/js/_website/src/lib/assets/img/blaxel-logo.png differ diff --git a/js/_website/src/lib/assets/img/chess.png b/js/_website/src/lib/assets/img/chess.png new file mode 100644 index 0000000..f930a0f Binary files /dev/null and b/js/_website/src/lib/assets/img/chess.png differ diff --git a/js/_website/src/lib/assets/img/consilium.png b/js/_website/src/lib/assets/img/consilium.png new file mode 100644 index 0000000..30e9b2e Binary files /dev/null and b/js/_website/src/lib/assets/img/consilium.png differ diff --git a/js/_website/src/lib/assets/img/dataflow.svg b/js/_website/src/lib/assets/img/dataflow.svg new file mode 100644 index 0000000..43e7e86 --- /dev/null +++ b/js/_website/src/lib/assets/img/dataflow.svg @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/_website/src/lib/assets/img/doc-mcp.png b/js/_website/src/lib/assets/img/doc-mcp.png new file mode 100644 index 0000000..715dfd8 Binary files /dev/null and b/js/_website/src/lib/assets/img/doc-mcp.png differ diff --git a/js/_website/src/lib/assets/img/eleven-labs-logo.png b/js/_website/src/lib/assets/img/eleven-labs-logo.png new file mode 100644 index 0000000..0786c51 Binary files /dev/null and b/js/_website/src/lib/assets/img/eleven-labs-logo.png differ diff --git a/js/_website/src/lib/assets/img/esc.svg b/js/_website/src/lib/assets/img/esc.svg new file mode 100644 index 0000000..d038326 --- /dev/null +++ b/js/_website/src/lib/assets/img/esc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/img/geocalc.png b/js/_website/src/lib/assets/img/geocalc.png new file mode 100644 index 0000000..670ea59 Binary files /dev/null and b/js/_website/src/lib/assets/img/geocalc.png differ diff --git a/js/_website/src/lib/assets/img/github-black.svg b/js/_website/src/lib/assets/img/github-black.svg new file mode 100644 index 0000000..977cf9d --- /dev/null +++ b/js/_website/src/lib/assets/img/github-black.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/_website/src/lib/assets/img/github.svg b/js/_website/src/lib/assets/img/github.svg new file mode 100644 index 0000000..787f059 --- /dev/null +++ b/js/_website/src/lib/assets/img/github.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/_website/src/lib/assets/img/hackathon-header.png b/js/_website/src/lib/assets/img/hackathon-header.png new file mode 100644 index 0000000..e6365ab Binary files /dev/null and b/js/_website/src/lib/assets/img/hackathon-header.png differ diff --git a/js/_website/src/lib/assets/img/header-faded.jpg b/js/_website/src/lib/assets/img/header-faded.jpg new file mode 100644 index 0000000..5bb33f0 Binary files /dev/null and b/js/_website/src/lib/assets/img/header-faded.jpg differ diff --git a/js/_website/src/lib/assets/img/header-image.jpg b/js/_website/src/lib/assets/img/header-image.jpg new file mode 100644 index 0000000..2afa3ce Binary files /dev/null and b/js/_website/src/lib/assets/img/header-image.jpg differ diff --git a/js/_website/src/lib/assets/img/header-image.png b/js/_website/src/lib/assets/img/header-image.png new file mode 100644 index 0000000..fd3e559 Binary files /dev/null and b/js/_website/src/lib/assets/img/header-image.png differ diff --git a/js/_website/src/lib/assets/img/instagramcaption.png b/js/_website/src/lib/assets/img/instagramcaption.png new file mode 100644 index 0000000..be57122 Binary files /dev/null and b/js/_website/src/lib/assets/img/instagramcaption.png differ diff --git a/js/_website/src/lib/assets/img/llamaindex-logo.png b/js/_website/src/lib/assets/img/llamaindex-logo.png new file mode 100644 index 0000000..af5afbd Binary files /dev/null and b/js/_website/src/lib/assets/img/llamaindex-logo.png differ diff --git a/js/_website/src/lib/assets/img/logo-melted.png b/js/_website/src/lib/assets/img/logo-melted.png new file mode 100644 index 0000000..64b422c Binary files /dev/null and b/js/_website/src/lib/assets/img/logo-melted.png differ diff --git a/js/_website/src/lib/assets/img/mcp-birthday-header-with-sponsors.png b/js/_website/src/lib/assets/img/mcp-birthday-header-with-sponsors.png new file mode 100644 index 0000000..d03f62c Binary files /dev/null and b/js/_website/src/lib/assets/img/mcp-birthday-header-with-sponsors.png differ diff --git a/js/_website/src/lib/assets/img/mcp-birthday-header.png b/js/_website/src/lib/assets/img/mcp-birthday-header.png new file mode 100644 index 0000000..037a752 Binary files /dev/null and b/js/_website/src/lib/assets/img/mcp-birthday-header.png differ diff --git a/js/_website/src/lib/assets/img/meta-image.jpg b/js/_website/src/lib/assets/img/meta-image.jpg new file mode 100644 index 0000000..8707c3e Binary files /dev/null and b/js/_website/src/lib/assets/img/meta-image.jpg differ diff --git a/js/_website/src/lib/assets/img/mistral-logo.png b/js/_website/src/lib/assets/img/mistral-logo.png new file mode 100644 index 0000000..a5fa924 Binary files /dev/null and b/js/_website/src/lib/assets/img/mistral-logo.png differ diff --git a/js/_website/src/lib/assets/img/mmorpg.png b/js/_website/src/lib/assets/img/mmorpg.png new file mode 100644 index 0000000..0d5efb7 Binary files /dev/null and b/js/_website/src/lib/assets/img/mmorpg.png differ diff --git a/js/_website/src/lib/assets/img/modal-logo.png b/js/_website/src/lib/assets/img/modal-logo.png new file mode 100644 index 0000000..71221d2 Binary files /dev/null and b/js/_website/src/lib/assets/img/modal-logo.png differ diff --git a/js/_website/src/lib/assets/img/nasaspaceexplorer.png b/js/_website/src/lib/assets/img/nasaspaceexplorer.png new file mode 100644 index 0000000..8bac0ec Binary files /dev/null and b/js/_website/src/lib/assets/img/nasaspaceexplorer.png differ diff --git a/js/_website/src/lib/assets/img/openai-logo.png b/js/_website/src/lib/assets/img/openai-logo.png new file mode 100644 index 0000000..79af865 Binary files /dev/null and b/js/_website/src/lib/assets/img/openai-logo.png differ diff --git a/js/_website/src/lib/assets/img/opensorus.png b/js/_website/src/lib/assets/img/opensorus.png new file mode 100644 index 0000000..1dedc40 Binary files /dev/null and b/js/_website/src/lib/assets/img/opensorus.png differ diff --git a/js/_website/src/lib/assets/img/sambanova-logo.png b/js/_website/src/lib/assets/img/sambanova-logo.png new file mode 100644 index 0000000..a64e031 Binary files /dev/null and b/js/_website/src/lib/assets/img/sambanova-logo.png differ diff --git a/js/_website/src/lib/assets/img/sentinel.png b/js/_website/src/lib/assets/img/sentinel.png new file mode 100644 index 0000000..596a56c Binary files /dev/null and b/js/_website/src/lib/assets/img/sentinel.png differ diff --git a/js/_website/src/lib/assets/img/shallowcoderesearch.png b/js/_website/src/lib/assets/img/shallowcoderesearch.png new file mode 100644 index 0000000..5ef82f3 Binary files /dev/null and b/js/_website/src/lib/assets/img/shallowcoderesearch.png differ diff --git a/js/_website/src/lib/assets/img/share_gray.svg b/js/_website/src/lib/assets/img/share_gray.svg new file mode 100644 index 0000000..e2aa365 --- /dev/null +++ b/js/_website/src/lib/assets/img/share_gray.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/js/_website/src/lib/assets/img/spaces-logo.svg b/js/_website/src/lib/assets/img/spaces-logo.svg new file mode 100644 index 0000000..6ee3003 --- /dev/null +++ b/js/_website/src/lib/assets/img/spaces-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/img/twitter.svg b/js/_website/src/lib/assets/img/twitter.svg new file mode 100644 index 0000000..fce3ddf --- /dev/null +++ b/js/_website/src/lib/assets/img/twitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/img/workflow.png b/js/_website/src/lib/assets/img/workflow.png new file mode 100644 index 0000000..d8085eb Binary files /dev/null and b/js/_website/src/lib/assets/img/workflow.png differ diff --git a/js/_website/src/lib/assets/index.ts b/js/_website/src/lib/assets/index.ts new file mode 100644 index 0000000..9e37a6d --- /dev/null +++ b/js/_website/src/lib/assets/index.ts @@ -0,0 +1,59 @@ +export { default as gradio_logo } from "./gradio.svg"; +export { default as gradio_logo_dark } from "./gradiodark.svg"; +export { default as twitter } from "./img/twitter.svg"; +export { default as github } from "./img/github.svg"; +export { default as github_black } from "./img/github-black.svg"; + +import google from "./logos/google.svg"; +import amazon from "./logos/amazon.svg"; +import fb from "./logos/fb.svg"; +import cisco from "./logos/cisco.svg"; +import twitter from "./logos/twitter.svg"; +import vm from "./logos/vmware.svg"; +import hf from "./logos/huggingface.svg"; +import siemens from "./logos/siemens.svg"; +import mit from "./logos/mit-svg-50.png"; +import stanford from "./logos/stanford.svg"; +import uipath from "./logos/uipath.svg"; +import unify from "./logos/unifyid.svg"; +import humans from "./logos/humaniseai.svg"; +import factmata from "./logos/factmata.svg"; +import wns from "./logos/wns.png"; + +import _tweets from "./tweets.json"; + +export const logos = [ + { img: google, contrast: false, description: "Google Logo" }, + { img: amazon, contrast: true, description: "Amazon logo" }, + { img: fb, contrast: false, description: "Facebook logo" }, + { img: cisco, contrast: false, description: "CISCO logo" }, + { img: twitter, contrast: false, description: "Twitter logo" }, + { img: vm, contrast: false, description: "VMwarelogo" }, + { img: hf, contrast: false, description: "Hugging Face logo" }, + { img: siemens, contrast: false, description: "Siemens logo" }, + { img: mit, contrast: true, description: "MIT logo" }, + { img: stanford, contrast: false, description: "Stanford logo" }, + { img: uipath, contrast: false, description: "UI Path logo" }, + { img: unify, contrast: false, description: "UnifyID logo" }, + { img: humans, contrast: true, description: "Humanise AI logo" }, + { img: factmata, contrast: true, description: "Factmata logo" }, + { img: wns, contrast: true, description: "WNS logo" } +]; + +export const twitter_pics = ( + Object.entries(import.meta.glob("./twitter/**", { eager: true })) as [ + string, + { default: string } + ][] +).reduce( + (a, [k, mod]) => { + a[k.replace("./twitter/", "")] = mod.default; + return a; + }, + {} as Record +); + +export const tweets = _tweets.map((x) => ({ + ...x, + profile_pic: twitter_pics[x.profile_pic] +})); diff --git a/js/_website/src/lib/assets/logos/amazon.svg b/js/_website/src/lib/assets/logos/amazon.svg new file mode 100644 index 0000000..92c6c3c --- /dev/null +++ b/js/_website/src/lib/assets/logos/amazon.svg @@ -0,0 +1,20 @@ + + + + + +image/svg+xml + + + + + + + + + + + + + + diff --git a/js/_website/src/lib/assets/logos/cisco.svg b/js/_website/src/lib/assets/logos/cisco.svg new file mode 100644 index 0000000..31c8e28 --- /dev/null +++ b/js/_website/src/lib/assets/logos/cisco.svg @@ -0,0 +1 @@ + 480x220-Cisco-Partner-Cameo-Global \ No newline at end of file diff --git a/js/_website/src/lib/assets/logos/factmata.svg b/js/_website/src/lib/assets/logos/factmata.svg new file mode 100644 index 0000000..95edeb4 --- /dev/null +++ b/js/_website/src/lib/assets/logos/factmata.svg @@ -0,0 +1,18 @@ + + + diff --git a/js/_website/src/lib/assets/logos/fb.svg b/js/_website/src/lib/assets/logos/fb.svg new file mode 100644 index 0000000..0715538 --- /dev/null +++ b/js/_website/src/lib/assets/logos/fb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/logos/google.svg b/js/_website/src/lib/assets/logos/google.svg new file mode 100644 index 0000000..8eb8c82 --- /dev/null +++ b/js/_website/src/lib/assets/logos/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/logos/gradio.svg b/js/_website/src/lib/assets/logos/gradio.svg new file mode 100644 index 0000000..ff8df41 --- /dev/null +++ b/js/_website/src/lib/assets/logos/gradio.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/js/_website/src/lib/assets/logos/hf-logo.svg b/js/_website/src/lib/assets/logos/hf-logo.svg new file mode 100644 index 0000000..ab959d1 --- /dev/null +++ b/js/_website/src/lib/assets/logos/hf-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/js/_website/src/lib/assets/logos/huggingface.svg b/js/_website/src/lib/assets/logos/huggingface.svg new file mode 100644 index 0000000..5843e85 --- /dev/null +++ b/js/_website/src/lib/assets/logos/huggingface.svg @@ -0,0 +1,182 @@ + + + + + icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hugging Face + diff --git a/js/_website/src/lib/assets/logos/humaniseai.svg b/js/_website/src/lib/assets/logos/humaniseai.svg new file mode 100644 index 0000000..67a262d --- /dev/null +++ b/js/_website/src/lib/assets/logos/humaniseai.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/logos/mit-svg-50.png b/js/_website/src/lib/assets/logos/mit-svg-50.png new file mode 100644 index 0000000..218bcb3 Binary files /dev/null and b/js/_website/src/lib/assets/logos/mit-svg-50.png differ diff --git a/js/_website/src/lib/assets/logos/siemens.svg b/js/_website/src/lib/assets/logos/siemens.svg new file mode 100644 index 0000000..6b02d60 --- /dev/null +++ b/js/_website/src/lib/assets/logos/siemens.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/js/_website/src/lib/assets/logos/stanford.svg b/js/_website/src/lib/assets/logos/stanford.svg new file mode 100644 index 0000000..59c4677 --- /dev/null +++ b/js/_website/src/lib/assets/logos/stanford.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/js/_website/src/lib/assets/logos/twitter.svg b/js/_website/src/lib/assets/logos/twitter.svg new file mode 100644 index 0000000..5ea4b52 --- /dev/null +++ b/js/_website/src/lib/assets/logos/twitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/logos/uipath.svg b/js/_website/src/lib/assets/logos/uipath.svg new file mode 100644 index 0000000..0e0c124 --- /dev/null +++ b/js/_website/src/lib/assets/logos/uipath.svg @@ -0,0 +1,9 @@ + + + + UiPath_Logo_full + Created with Sketch. + + + + \ No newline at end of file diff --git a/js/_website/src/lib/assets/logos/unifyid.svg b/js/_website/src/lib/assets/logos/unifyid.svg new file mode 100644 index 0000000..b7dbd44 --- /dev/null +++ b/js/_website/src/lib/assets/logos/unifyid.svg @@ -0,0 +1,30 @@ + + + + +UnifyID + + + + + + + + + + diff --git a/js/_website/src/lib/assets/logos/vmware.svg b/js/_website/src/lib/assets/logos/vmware.svg new file mode 100644 index 0000000..3107c1d --- /dev/null +++ b/js/_website/src/lib/assets/logos/vmware.svg @@ -0,0 +1,42 @@ + +VMware logoAn information technology company based in Palo Alto, California, United Statesimage/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/_website/src/lib/assets/logos/wns.png b/js/_website/src/lib/assets/logos/wns.png new file mode 100644 index 0000000..1d3dbbc Binary files /dev/null and b/js/_website/src/lib/assets/logos/wns.png differ diff --git a/js/_website/src/lib/assets/prism.css b/js/_website/src/lib/assets/prism.css new file mode 100644 index 0000000..50a0af9 --- /dev/null +++ b/js/_website/src/lib/assets/prism.css @@ -0,0 +1,259 @@ +/* PrismJS 1.20.0 +https://prismjs.com/download.html#themes=prism&languages=python */ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + +code[class*="language-"], +pre[class*="language-"], +pre.gradio-code, +pre.gradio-code code { + word-wrap: normal; + color: #171717; + font-size: 1em; + line-height: 1.5; + font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; + text-align: left; + text-shadow: 0 1px white; + white-space: pre; + word-break: normal; + word-spacing: normal; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + font-size: 0.9em; +} + +pre[class*="language-"]::-moz-selection, +pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, +code[class*="language-"] ::-moz-selection { + background: #b3d4fc; + text-shadow: none; +} + +pre[class*="language-"]::selection, +pre[class*="language-"] ::selection, +code[class*="language-"]::selection, +code[class*="language-"] ::selection { + background: #b3d4fc; + text-shadow: none; +} + +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"], +pre.gradio-code { + margin: 0; + padding: 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"], +pre.gradio-code { + background: rgb(249, 250, 251); +} + +.dark :not(pre) > code[class*="language-"], +.dark pre[class*="language-"], +.dark pre.gradio-code { + background: rgb(38, 38, 38); +} + +.dark code[class*="language-"], +.dark pre[class*="language-"], +.dark pre.gradio-code, +.dark pre.gradio-code code { + text-shadow: none; + color: #e6edf3; +} + +.prose code[class*="language-"], +.prose pre[class*="language-"], +.prose pre.gradio-code, +.prose pre.gradio-code code { + font-size: 0.9em; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + border-radius: 0.3em; + padding: 0.1em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.token.namespace { + color: #6f42c1; +} + +.token.function-call { + color: #dd4a68; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + background: hsla(0, 0%, 100%, 0.5); + color: #9a6e3a; +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function, +.token.class-name { + color: #dd4a68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.annotation, +.token.decorator { + color: #9a6e3a; +} + +.token.keyword-argument { + color: #e36209; + font-style: italic; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +/* Dark mode syntax highlighting */ +.dark .token.comment, +.dark .token.prolog, +.dark .token.doctype, +.dark .token.cdata { + color: #8b949e; +} + +.dark .token.punctuation { + color: #c9d1d9; +} + +.dark .token.property, +.dark .token.tag, +.dark .token.boolean, +.dark .token.number, +.dark .token.constant, +.dark .token.symbol, +.dark .token.deleted { + color: #ff7b72; +} + +.dark .token.selector, +.dark .token.attr-name, +.dark .token.string, +.dark .token.char, +.dark .token.builtin, +.dark .token.inserted { + color: #a5d6ff; +} + +.dark .token.operator, +.dark .token.entity, +.dark .token.url, +.dark .language-css .token.string, +.dark .style .token.string { + background: transparent; + color: #ffa657; +} + +.dark .token.atrule, +.dark .token.attr-value, +.dark .token.keyword { + color: #ff7b72; +} + +.dark .token.function, +.dark .token.class-name { + color: #d2a8ff; +} + +.dark .token.regex, +.dark .token.important, +.dark .token.variable { + color: #ffa657; +} + +.dark .token.annotation, +.dark .token.decorator { + color: #ffa657; +} + +.dark .token.namespace { + color: #79c0ff; +} + +.dark .token.function-call { + color: #d2a8ff; +} + +.dark .token.keyword-argument { + color: #ffa657; + font-style: italic; +} diff --git a/js/_website/src/lib/assets/style.css b/js/_website/src/lib/assets/style.css new file mode 100644 index 0000000..faad0d0 --- /dev/null +++ b/js/_website/src/lib/assets/style.css @@ -0,0 +1,522 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.dark input[type="text"], +.dark input[type="email"], +.dark input[type="search"], +.dark input[type="password"], +.dark input[type="number"], +.dark input[type="url"], +.dark input[type="tel"], +.dark textarea { + background-color: rgb(38, 38, 38); + color: rgb(245, 245, 245); + border-color: rgb(64, 64, 64); +} + +.link { + @apply font-semibold hover:text-orange-500 transition-colors; +} +.thin-link { + @apply text-gray-900 dark:text-gray-300 hover:text-gray-600 dark:hover:text-gray-100 transition-colors; +} +.thinner-link { + @apply hover:text-orange-500 transition-colors; +} + +.prose :where(img):not(:where([class~="not-prose"] *)) { + margin-top: 0; + margin-bottom: 0; +} + +.prose :where(code):not(:where([class~="not-prose"] *)) { + word-break: break-all; +} + +.prose :where(a):not(:where([class~="not-prose"] *)) { + word-break: break-all; +} + +.group:hover .group-hover\:flex { + /* for some reason, group-hover:flex not working on mobile */ + display: flex; +} +.group:active .group-active\:flex { + /* for some reason, group-active:flex not working on mobile */ + display: flex; +} +@media (min-width: 640px) { + /* for some reason, sm:block not working */ + .sm\:block { + @apply block; + } +} +@media (min-width: 768px) { + /* for some reason, md:block not working */ + .md\:block { + @apply block; + } +} + +@layer base { + a.text-link { + @apply font-semibold text-gray-800 dark:text-white underline decoration-orange-500 underline-offset-2 hover:text-orange-500; + } +} + +@layer utilities { + .filter-none { + filter: none; + } + .filter-grayscale { + filter: grayscale(100%); + } + .shadow-alternate-sm { + box-shadow: + 0px 5px 5px rgba(0, 0, 0, 0.03), + 0px 2px 2px rgba(0, 0, 0, 0.03), + 0px 0px 1px rgba(0, 0, 0, 0.03); + transition: transform 0.3s ease-in-out; + } + .shadow-alternate { + box-shadow: + 0px 10px 20px rgba(0, 0, 0, 0.04), + 0px 2px 6px rgba(0, 0, 0, 0.04), + 0px 0px 1px rgba(0, 0, 0, 0.04); + transition: transform 0.3s ease-in-out; + } + .shadow-alternate-xl { + box-shadow: + 0px 24px 32px rgba(0, 0, 0, 0.04), + 0px 16px 24px rgba(0, 0, 0, 0.04), + 0px 4px 8px rgba(0, 0, 0, 0.04), + 0px 0px 1px rgba(0, 0, 0, 0.04); + transition: transform 0.3s ease-in-out; + } +} + +.shadow-alternate:hover { + transform: scale(1.1); +} +/* index */ +.active-example-tab { + @apply border-orange-500 rounded-full text-orange-500 ring-1 bg-orange-50 dark:bg-orange-900 ring-orange-200 dark:ring-orange-700 hover:!text-orange-500 cursor-pointer shadow shadow-orange-200 dark:shadow-orange-900; +} + +/* guides */ +.prose > p > img { + @apply max-w-full mx-auto my-0 w-4/5; +} +.prose > p > video { + @apply max-w-full mx-auto my-0 w-4/5; +} + +.prose code::before { + display: none; +} + +.prose code::after { + display: none; +} + +/* docs & guides */ +.thin-link.current-nav-link { + @apply text-orange-500; +} +.thin-link.current-nav-link:not(.subheading) { + @apply border-orange-500 md:border-l-2 pl-4; +} +.link.current-nav-link { + @apply border-orange-500 text-orange-500; +} +.thinner-link.current-nav-link { + @apply border-orange-500 text-orange-500; +} +.second-nav-link { + @apply border-l-2 border-gray-100 px-3; +} +.current-nav-link { + @apply border-orange-500 text-orange-500; +} + +/* editable docs */ + +.obj h1 { + @apply text-3xl font-light py-4 text-gray-900 dark:text-gray-100; +} +.obj h2 { + @apply mb-2 text-lg text-gray-900 dark:text-gray-100; +} +.obj h3 { + @apply mt-8 text-xl text-orange-500 dark:text-orange-400 font-light; +} +.obj h4 { + @apply mt-8 text-xl text-orange-500 dark:text-orange-400 font-light; +} + +.obj p { + @apply mb-2 text-lg text-gray-900 dark:text-gray-100; +} + +/* docs */ +.selected-demo { + @apply font-semibold bg-gray-50 rounded text-orange-500; +} +.codeblock code.language-python { + @apply !leading-7 !whitespace-pre-wrap !break-all; + line-height: 1 !important; +} +code.language-bash { + @apply !leading-7 !whitespace-pre-wrap !break-all; +} + +.group-hover-visible { + @apply group-hover:visible; +} +.anchor-img { + @apply w-7 max-w-full inline-block m-0 ml-2; +} +.anchor-img-small { + @apply w-5 max-w-full inline-block m-0 ml-0.5; +} +.selected-version { + @apply font-semibold text-orange-500; +} +.selected-version:before { + content: "• "; +} + +.obj p { + word-break: break-word; +} + +/* copy button */ +.clipboard-button { + @apply absolute right-0 px-1.5 pb-1 text-gray-500 text-sm z-[100] opacity-0 duration-100; +} +.clipboard-button:hover { + @apply cursor-pointer; +} +.clipboard-button:hover > svg { + @apply fill-gray-700; +} +.clipboard-button:focus { + @apply outline-0; +} + +/* interactive banner */ +.interactive-banner { + @apply absolute right-0 px-1.5 pt-0.5 pb-1 m-4 -mt-1 text-sm z-[100] bg-orange-500 rounded-lg; +} + +.codeblock { + @apply relative bg-gray-50 dark:bg-neutral-800 mx-auto p-6 mt-2 rounded-lg; +} +.codeblock:hover > .clipboard-button { + @apply opacity-100 duration-200; +} + +[type="search"]::-webkit-search-cancel-button { + display: none; +} + +.view-code { + @apply w-16 p-2 mx-auto hover:bg-gray-100; + background: rgb(249, 250, 251); +} + +/* demos */ +.selected-demo-tab { + @apply font-semibold text-orange-500 rounded-t-md border-2 border-gray-100 dark:border-neutral-700 border-b-0 bg-white dark:bg-neutral-800; +} +.selected-demo-window { + @apply rounded-b-md border-2 border-gray-100 -mt-0.5; +} + +.tip { + @apply bg-orange-50 dark:bg-orange-950/40 border-2 border-orange-200 dark:border-orange-500/30 rounded-2xl text-gray-900 dark:text-gray-100 p-6 my-6 flex items-center gap-4 text-sm; +} + +.tip-icon { + @apply flex items-center justify-center flex-shrink-0 -mt-1; +} + +.tip-content { + @apply flex-1; +} + +.tip strong { + @apply text-orange-500 dark:text-orange-400 font-semibold; +} + +.tip code { + @apply text-orange-400 dark:text-orange-400 bg-orange-900/30 px-1.5 py-0.5 rounded text-sm; +} + +.tip a { + @apply text-orange-400 dark:text-orange-400 underline hover:text-orange-300 dark:hover:text-orange-300; +} + +.tip p { + @apply m-0 text-sm; +} + +.tip svg { + @apply text-orange-500 dark:text-orange-500 flex-shrink-0 w-6 h-6; +} + +.warning { + @apply bg-red-50 dark:bg-red-900/30 border-red-50 dark:border-red-800 border-l-2 border-l-red-300 dark:border-l-red-600 text-red-700 dark:text-red-300 p-4 px-6; +} + +.warning strong { + @apply text-red-700; +} + +.warning code { + @apply text-red-700; +} + +.warning a { + @apply text-red-700; +} + +.warning p { + @apply inline; +} + +.shared-link:before { + content: url("img/anchor_orange.svg"); + width: 15px; + display: inline-block; + margin-right: 5px; +} + +li > p { + margin: 0.25rem 0 !important; + padding: 0 !important; +} + +ul > li, +li > ul { + margin: 0 !important; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +code { + font-size: 0.9em; + @apply text-gray-900 dark:text-gray-100; +} + +.prose :where(code):not(:where([class~="not-prose"] *)):not(pre code) { + @apply bg-orange-100 dark:bg-orange-900/40 text-orange-800 dark:text-orange-200 px-1.5 py-0.5 rounded font-medium; +} + +pre > code { + font-weight: 500 !important; + border-radius: 0.25rem; + font-size: 0.9em !important; + line-height: 1 !important; +} + +h3.header-tag { + margin-bottom: 10px; +} + +h1 > code, +h2 > code, +h3 > code, +h4 > code, +h5 > code, +h6 > code { + border: none; + border-radius: 0; + background-color: transparent; +} + +h5 { + @apply text-gray-500; +} + +.token.table { + display: unset; +} + +strong { + font-weight: 600; + @apply text-gray-900 dark:text-gray-100; +} + +.obj a { + color: var(--tw-prose-links); + text-decoration: underline; + font-weight: 500; +} +.dark .obj a { + @apply text-orange-400; +} + +.codeblock > pre { + font-size: 1em; +} + +.obj ul > li { + list-style: circle; +} +.obj ol, +.obj ul { + padding-inline-start: 40px; +} +.obj li { + @apply text-gray-900 dark:text-gray-100; +} + +ol { + list-style: decimal; +} + +ul { + list-style: circle; +} + +li { + display: list-item; +} + +.results ul { + list-style: none; +} + +.results ol { + list-style: none; +} + +:where(ol > li)::marker { + font-weight: 400; + color: #6b7280; +} + +.embedded-component .gradio-container { + padding: var(--size-4) 0 0 0 !important; +} +.embedded-component .gradio-container footer { + display: none !important; +} +.embedded-component:has(.loading) { + visibility: hidden; + height: 0; +} + +.obj .codeblock { + @apply my-4; + display: grid; +} + +.prose .codeblock { + @apply my-4; + display: grid; +} + +summary { + display: grid !important; +} + +summary code { + white-space: nowrap !important; + overflow-x: scroll; +} + +summary::after { + @apply pl-1 !opacity-100; + background: var(--table-odd-background-fill); + height: 90%; + padding-top: 14px; +} + +.dark .obj .max-h-96.overflow-y-scroll table thead th { + background: #0a0a0a !important; + color: #e5e5e5 !important; + border-top: 1px solid #404040 !important; + border-left: 1px solid #404040 !important; +} + +.dark .obj .max-h-96.overflow-y-scroll table thead th:first-child { + border-top: none !important; + border-left: none !important; +} + +.dark .obj .max-h-96.overflow-y-scroll table tbody { + border: 1px solid #404040 !important; +} + +.dark .obj .max-h-96.overflow-y-scroll table tbody tr { + border-bottom: 1px solid #404040 !important; +} + +.dark .obj .max-h-96.overflow-y-scroll table tbody tr:last-child { + border-bottom: none !important; +} + +.dark .obj .max-h-96.overflow-y-scroll table tbody tr:nth-child(odd) { + background: #171717 !important; +} + +.dark .obj .max-h-96.overflow-y-scroll table tbody tr:nth-child(even) { + background: #0a0a0a !important; +} + +.dark .obj .max-h-96.overflow-y-scroll table tbody tr:hover { + background: #262626 !important; +} + +.dark .obj .max-h-96.overflow-y-scroll table tbody th { + background: inherit !important; + color: #e5e5e5 !important; +} + +.dark .obj .max-h-96.overflow-y-scroll table tbody td { + color: #e5e5e5 !important; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1.5rem; + margin-bottom: 1.5rem; + font-size: 1rem; +} + +table thead th { + background-color: #f3f4f6; + color: #111827; + font-weight: 600; + text-align: left; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + border: 1px solid #e5e7eb; +} + +.dark table thead th { + background-color: #374151; + color: #f3f4f6; + border: 1px solid #52525b; +} + +table tbody td { + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + border: 1px solid #e5e7eb; + color: #374151; + vertical-align: top; +} + +.dark table tbody td { + border: 1px solid #52525b; + color: #d1d5db; +} diff --git a/js/_website/src/lib/assets/theme.css b/js/_website/src/lib/assets/theme.css new file mode 100644 index 0000000..a24fdc6 --- /dev/null +++ b/js/_website/src/lib/assets/theme.css @@ -0,0 +1,496 @@ +@font-face { + font-family: "IBM Plex Mono"; + src: url("/fonts/IBMPlexMono/IBMPlexMono-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: "IBM Plex Mono"; + src: url("/fonts/IBMPlexMono/IBMPlexMono-Bold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; +} + +:root { + --name: default; + --primary-50: #fff7ed; + --primary-100: #ffedd5; + --primary-200: #ffddb3; + --primary-300: #fdba74; + --primary-400: #fb923c; + --primary-500: #f97316; + --primary-600: #ea580c; + --primary-700: #c2410c; + --primary-800: #9a3412; + --primary-900: #7c2d12; + --primary-950: #6c2e12; + --secondary-50: #eff6ff; + --secondary-100: #dbeafe; + --secondary-200: #bfdbfe; + --secondary-300: #93c5fd; + --secondary-400: #60a5fa; + --secondary-500: #3b82f6; + --secondary-600: #2563eb; + --secondary-700: #1d4ed8; + --secondary-800: #1e40af; + --secondary-900: #1e3a8a; + --secondary-950: #1d3660; + --neutral-50: #fafafa; + --neutral-100: #f4f4f5; + --neutral-200: #e4e4e7; + --neutral-300: #d4d4d8; + --neutral-400: #bbbbc2; + --neutral-500: #71717a; + --neutral-600: #52525b; + --neutral-700: #3f3f46; + --neutral-800: #27272a; + --neutral-900: #18181b; + --neutral-950: #0f0f11; + --spacing-xxs: 1px; + --spacing-xs: 2px; + --spacing-sm: 4px; + --spacing-md: 6px; + --spacing-lg: 8px; + --spacing-xl: 10px; + --spacing-xxl: 16px; + --radius-xxs: 1px; + --radius-xs: 2px; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --radius-xxl: 22px; + --text-xxs: 9px; + --text-xs: 10px; + --text-sm: 12px; + --text-md: 14px; + --text-lg: 16px; + --text-xl: 22px; + --text-xxl: 26px; + --font: "Source Sans Pro", ui-sans-serif, system-ui, sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, Consolas, monospace; + --body-background-fill: var(--background-fill-primary); + --body-text-color: var(--neutral-800); + --body-text-size: var(--text-md); + --body-text-weight: 400; + --embed-radius: var(--radius-sm); + --color-accent: var(--primary-500); + --color-accent-soft: var(--primary-50); + --background-fill-primary: white; + --background-fill-secondary: var(--neutral-50); + --border-color-accent: var(--primary-300); + --border-color-primary: var(--neutral-200); + --link-text-color: var(--secondary-600); + --link-text-color-active: var(--secondary-600); + --link-text-color-hover: var(--secondary-700); + --link-text-color-visited: var(--secondary-500); + --body-text-color-subdued: var(--neutral-400); + --accordion-text-color: var(--body-text-color); + --table-text-color: var(--body-text-color); + --shadow-drop: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px; + --shadow-drop-lg: + 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-inset: rgba(0, 0, 0, 0.05) 0px 2px 4px 0px inset; + --shadow-spread: 3px; + --block-background-fill: var(--background-fill-primary); + --block-border-color: var(--border-color-primary); + --block-border-width: 1px; + --block-info-text-color: var(--body-text-color-subdued); + --block-info-text-size: var(--text-sm); + --block-info-text-weight: 400; + --block-label-background-fill: var(--background-fill-primary); + --block-label-border-color: var(--border-color-primary); + --block-label-border-width: 1px; + --block-label-shadow: var(--block-shadow); + --block-label-text-color: var(--neutral-500); + --block-label-margin: 0; + --block-label-padding: var(--spacing-sm) var(--spacing-lg); + --block-label-radius: calc(var(--radius-sm) - 1px) 0 + calc(var(--radius-sm) - 1px) 0; + --block-label-right-radius: 0 calc(var(--radius-sm) - 1px) 0 + calc(var(--radius-sm) - 1px); + --block-label-text-size: var(--text-sm); + --block-label-text-weight: 400; + --block-padding: var(--spacing-xl) calc(var(--spacing-xl) + 2px); + --block-radius: var(--radius-sm); + --block-shadow: none; + --block-title-background-fill: none; + --block-title-border-color: none; + --block-title-border-width: 0px; + --block-title-text-color: var(--neutral-500); + --block-title-padding: 0; + --block-title-radius: none; + --block-title-text-size: var(--text-md); + --block-title-text-weight: 400; + --container-radius: var(--radius-sm); + --form-gap-width: 0px; + --layout-gap: var(--spacing-xxl); + --panel-background-fill: var(--background-fill-secondary); + --panel-border-color: var(--border-color-primary); + --panel-border-width: 0; + --section-header-text-size: var(--text-md); + --section-header-text-weight: 400; + --border-color-accent-subdued: var(--primary-200); + --code-background-fill: var(--neutral-100); + --chatbot-text-size: var(--text-lg); + --checkbox-background-color: var(--background-fill-primary); + --checkbox-background-color-focus: var(--checkbox-background-color); + --checkbox-background-color-hover: var(--checkbox-background-color); + --checkbox-background-color-selected: var(--color-accent); + --checkbox-border-color: var(--neutral-300); + --checkbox-border-color-focus: var(--color-accent); + --checkbox-border-color-hover: var(--neutral-300); + --checkbox-border-color-selected: var(--color-accent); + --checkbox-border-radius: var(--radius-sm); + --checkbox-border-width: var(--input-border-width); + --checkbox-label-background-fill: var(--background-fill-primary); + --checkbox-label-background-fill-hover: var(--background-fill-secondary); + --checkbox-label-background-fill-selected: var( + --checkbox-label-background-fill + ); + --checkbox-label-border-color: var(--border-color-primary); + --checkbox-label-border-color-hover: var(--checkbox-label-border-color); + --checkbox-label-border-color-selected: var(--checkbox-label-border-color); + --checkbox-label-border-width: var(--input-border-width); + --checkbox-label-gap: var(--spacing-lg); + --checkbox-label-padding: var(--spacing-md) calc(2 * var(--spacing-md)); + --checkbox-label-shadow: none; + --checkbox-label-text-size: var(--text-md); + --checkbox-label-text-weight: 400; + --checkbox-check: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); + --radio-circle: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); + --checkbox-shadow: var(--input-shadow); + --checkbox-label-text-color: var(--body-text-color); + --checkbox-label-text-color-selected: var(--checkbox-label-text-color); + --error-background-fill: #fef2f2; + --error-border-color: #b91c1c; + --error-border-width: 1px; + --error-text-color: #b91c1c; + --error-icon-color: #b91c1c; + --input-background-fill: white; + --input-background-fill-focus: var(--input-background-fill); + --input-background-fill-hover: var(--input-background-fill); + --input-border-color: var(--border-color-primary); + --input-border-color-focus: var(--secondary-300); + --input-border-color-hover: var(--input-border-color); + --input-border-width: 1px; + --input-padding: var(--spacing-xl); + --input-placeholder-color: var(--neutral-400); + --input-radius: var(--radius-sm); + --input-shadow: none; + --input-shadow-focus: + 0 0 0 var(--shadow-spread) var(--secondary-50), var(--shadow-inset); + --input-text-size: var(--text-md); + --input-text-weight: 400; + --loader-color: var(--color-accent); + --prose-text-size: var(--text-md); + --prose-text-weight: 400; + --prose-header-text-weight: 600; + --slider-color: var(--color-accent); + --stat-background-fill: linear-gradient( + to right, + var(--primary-400), + var(--primary-200) + ); + --table-border-color: var(--neutral-300); + --table-even-background-fill: white; + --table-odd-background-fill: var(--neutral-50); + --table-radius: var(--radius-sm); + --table-row-focus: var(--color-accent-soft); + --button-border-width: 0px; + --button-cancel-background-fill: #ef4444; + --button-cancel-background-fill-hover: #dc2626; + --button-cancel-border-color: var(--button-secondary-border-color); + --button-cancel-border-color-hover: var( + --button-secondary-border-color-hover + ); + --button-cancel-text-color: white; + --button-cancel-text-color-hover: white; + --button-cancel-shadow: var(--button-secondary-shadow); + --button-cancel-shadow-hover: var(--button-secondary-shadow-hover); + --button-cancel-shadow-active: var(--button-secondary-shadow-active); + --button-transform-hover: none; + --button-transform-active: none; + --button-transition: all 0.2s ease; + --button-large-padding: var(--spacing-lg) calc(2 * var(--spacing-lg)); + --button-large-radius: var(--radius-md); + --button-large-text-size: var(--text-lg); + --button-large-text-weight: 600; + --button-primary-background-fill: var(--primary-500); + --button-primary-background-fill-hover: var(--primary-600); + --button-primary-border-color: var(--primary-500); + --button-primary-border-color-hover: var(--primary-500); + --button-primary-text-color: white; + --button-primary-text-color-hover: var(--button-primary-text-color); + --button-primary-shadow: none; + --button-primary-shadow-hover: var(--button-primary-shadow); + --button-primary-shadow-active: var(--button-primary-shadow); + --button-secondary-background-fill: var(--neutral-200); + --button-secondary-background-fill-hover: var(--neutral-300); + --button-secondary-border-color: var(--neutral-200); + --button-secondary-border-color-hover: var(--neutral-200); + --button-secondary-text-color: black; + --button-secondary-text-color-hover: var(--button-secondary-text-color); + --button-secondary-shadow: var(--button-primary-shadow); + --button-secondary-shadow-hover: var(--button-secondary-shadow); + --button-secondary-shadow-active: var(--button-secondary-shadow); + --button-small-padding: var(--spacing-sm) calc(1.5 * var(--spacing-sm)); + --button-small-radius: var(--radius-md); + --button-small-text-size: var(--text-sm); + --button-small-text-weight: 400; + --button-medium-padding: var(--spacing-md) calc(2 * var(--spacing-md)); + --button-medium-radius: var(--radius-md); + --button-medium-text-size: var(--text-md); + --button-medium-text-weight: 600; +} + +:root.dark, +:root .dark { + --body-background-fill: var(--background-fill-primary); + --body-text-color: var(--neutral-100); + --color-accent-soft: var(--neutral-700); + --background-fill-primary: var(--neutral-950); + --background-fill-secondary: var(--neutral-900); + --border-color-accent: var(--neutral-600); + --border-color-primary: var(--neutral-700); + --link-text-color-active: var(--secondary-500); + --link-text-color: var(--secondary-500); + --link-text-color-hover: var(--secondary-400); + --link-text-color-visited: var(--secondary-600); + --body-text-color-subdued: var(--neutral-400); + --accordion-text-color: var(--body-text-color); + --table-text-color: var(--body-text-color); + --shadow-spread: 1px; + --block-background-fill: var(--neutral-800); + --block-border-color: var(--border-color-primary); + --block_border_width: None; + --block-info-text-color: var(--body-text-color-subdued); + --block-label-background-fill: var(--background-fill-secondary); + --block-label-border-color: var(--border-color-primary); + --block_label_border_width: None; + --block-label-text-color: var(--neutral-200); + --block_shadow: None; + --block_title_background_fill: None; + --block_title_border_color: None; + --block_title_border_width: None; + --block-title-text-color: var(--neutral-200); + --panel-background-fill: var(--background-fill-secondary); + --panel-border-color: var(--border-color-primary); + --panel_border_width: None; + --border-color-accent-subdued: var(--border-color-accent); + --code-background-fill: var(--neutral-800); + --checkbox-background-color: var(--neutral-800); + --checkbox-background-color-focus: var(--checkbox-background-color); + --checkbox-background-color-hover: var(--checkbox-background-color); + --checkbox-background-color-selected: var(--color-accent); + --checkbox-border-color: var(--neutral-700); + --checkbox-border-color-focus: var(--color-accent); + --checkbox-border-color-hover: var(--neutral-600); + --checkbox-border-color-selected: var(--color-accent); + --checkbox-border-width: var(--input-border-width); + --checkbox-label-background-fill: var(--neutral-800); + --checkbox-label-background-fill-hover: var(--checkbox-label-background-fill); + --checkbox-label-background-fill-selected: var( + --checkbox-label-background-fill + ); + --checkbox-label-border-color: var(--border-color-primary); + --checkbox-label-border-color-hover: var(--checkbox-label-border-color); + --checkbox-label-border-color-selected: var(--checkbox-label-border-color); + --checkbox-label-border-width: var(--input-border-width); + --checkbox-label-text-color: var(--body-text-color); + --checkbox-label-text-color-selected: var(--checkbox-label-text-color); + --error-background-fill: var(--neutral-900); + --error-border-color: #ef4444; + --error_border_width: None; + --error-text-color: #fef2f2; + --error-icon-color: #ef4444; + --input-background-fill: var(--neutral-800); + --input_background_fill_focus: None; + --input-background-fill-hover: var(--input-background-fill); + --input-border-color: var(--border-color-primary); + --input-border-color-focus: var(--neutral-700); + --input-border-color-hover: var(--input-border-color); + --input_border_width: None; + --input-placeholder-color: var(--neutral-500); + --input_shadow: None; + --input-shadow-focus: + 0 0 0 var(--shadow-spread) var(--neutral-700), var(--shadow-inset); + --loader_color: None; + --slider_color: None; + --stat-background-fill: linear-gradient( + to right, + var(--primary-400), + var(--primary-600) + ); + --table-border-color: var(--neutral-700); + --table-even-background-fill: var(--neutral-950); + --table-odd-background-fill: var(--neutral-900); + --table-row-focus: var(--color-accent-soft); + --button_border_width: None; + --button-cancel-background-fill: #b91c1c; + --button-cancel-background-fill-hover: #991b1b; + --button-cancel-border-color: var(--button-secondary-border-color); + --button-cancel-border-color-hover: var( + --button-secondary-border-color-hover + ); + --button-cancel-text-color: white; + --button-cancel-text-color-hover: white; + --button-cancel-shadow: var(--button-secondary-shadow); + --button-cancel-shadow-hover: var(--button-secondary-shadow-hover); + --button-cancel-shadow-active: var(--button-secondary-shadow-active); + --button-primary-background-fill: var(--primary-600); + --button-primary-background-fill-hover: var(--primary-700); + --button-primary-border-color: var(--primary-600); + --button-primary-border-color-hover: var(--primary-500); + --button-primary-text-color: white; + --button-primary-text-color-hover: var(--button-primary-text-color); + --button_primary_shadow: None; + --button-primary-shadow-hover: var(--button-primary-shadow); + --button-primary-shadow-active: var(--button-primary-shadow); + --button-secondary-background-fill: var(--neutral-600); + --button-secondary-background-fill-hover: var(--neutral-700); + --button-secondary-border-color: var(--neutral-600); + --button-secondary-border-color-hover: var(--neutral-500); + --button-secondary-text-color: white; + --button-secondary-text-color-hover: var(--button-secondary-text-color); + --button_secondary_shadow: None; + --button-secondary-shadow-hover: var(--button-secondary-shadow); + --button-secondary-shadow-active: var(--button-secondary-shadow); + --name: default; + --primary-50: #fff7ed; + --primary-100: #ffedd5; + --primary-200: #ffddb3; + --primary-300: #fdba74; + --primary-400: #fb923c; + --primary-500: #f97316; + --primary-600: #ea580c; + --primary-700: #c2410c; + --primary-800: #9a3412; + --primary-900: #7c2d12; + --primary-950: #6c2e12; + --secondary-50: #eff6ff; + --secondary-100: #dbeafe; + --secondary-200: #bfdbfe; + --secondary-300: #93c5fd; + --secondary-400: #60a5fa; + --secondary-500: #3b82f6; + --secondary-600: #2563eb; + --secondary-700: #1d4ed8; + --secondary-800: #1e40af; + --secondary-900: #1e3a8a; + --secondary-950: #1d3660; + --neutral-50: #fafafa; + --neutral-100: #f4f4f5; + --neutral-200: #e4e4e7; + --neutral-300: #d4d4d8; + --neutral-400: #bbbbc2; + --neutral-500: #71717a; + --neutral-600: #52525b; + --neutral-700: #3f3f46; + --neutral-800: #27272a; + --neutral-900: #18181b; + --neutral-950: #0f0f11; + --spacing-xxs: 1px; + --spacing-xs: 2px; + --spacing-sm: 4px; + --spacing-md: 6px; + --spacing-lg: 8px; + --spacing-xl: 10px; + --spacing-xxl: 16px; + --radius-xxs: 1px; + --radius-xs: 2px; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + --radius-xxl: 22px; + --text-xxs: 9px; + --text-xs: 10px; + --text-sm: 12px; + --text-md: 14px; + --text-lg: 16px; + --text-xl: 22px; + --text-xxl: 26px; + --font: "Source Sans Pro", ui-sans-serif, system-ui, sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, Consolas, monospace; + --body-text-size: var(--text-md); + --body-text-weight: 400; + --embed-radius: var(--radius-sm); + --color-accent: var(--primary-500); + --shadow-drop: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px; + --shadow-drop-lg: + 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-inset: rgba(0, 0, 0, 0.05) 0px 2px 4px 0px inset; + --block-border-width: 1px; + --block-info-text-size: var(--text-sm); + --block-info-text-weight: 400; + --block-label-border-width: 1px; + --block-label-shadow: var(--block-shadow); + --block-label-margin: 0; + --block-label-padding: var(--spacing-sm) var(--spacing-lg); + --block-label-radius: calc(var(--radius-sm) - 1px) 0 + calc(var(--radius-sm) - 1px) 0; + --block-label-right-radius: 0 calc(var(--radius-sm) - 1px) 0 + calc(var(--radius-sm) - 1px); + --block-label-text-size: var(--text-sm); + --block-label-text-weight: 400; + --block-padding: var(--spacing-xl) calc(var(--spacing-xl) + 2px); + --block-radius: var(--radius-sm); + --block-shadow: none; + --block-title-background-fill: none; + --block-title-border-color: none; + --block-title-border-width: 0px; + --block-title-padding: 0; + --block-title-radius: none; + --block-title-text-size: var(--text-md); + --block-title-text-weight: 400; + --container-radius: var(--radius-sm); + --form-gap-width: 0px; + --layout-gap: var(--spacing-xxl); + --panel-border-width: 0; + --section-header-text-size: var(--text-md); + --section-header-text-weight: 400; + --chatbot-text-size: var(--text-lg); + --checkbox-border-radius: var(--radius-sm); + --checkbox-label-gap: var(--spacing-lg); + --checkbox-label-padding: var(--spacing-md) calc(2 * var(--spacing-md)); + --checkbox-label-shadow: none; + --checkbox-label-text-size: var(--text-md); + --checkbox-label-text-weight: 400; + --checkbox-check: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); + --radio-circle: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); + --checkbox-shadow: var(--input-shadow); + --error-border-width: 1px; + --input-background-fill-focus: var(--input-background-fill); + --input-border-width: 1px; + --input-padding: var(--spacing-xl); + --input-radius: var(--radius-sm); + --input-shadow: none; + --input-text-size: var(--text-md); + --input-text-weight: 400; + --loader-color: var(--color-accent); + --prose-text-size: var(--text-md); + --prose-text-weight: 400; + --prose-header-text-weight: 600; + --slider-color: var(--color-accent); + --table-radius: var(--radius-sm); + --button-border-width: 0px; + --button-transform-hover: none; + --button-transform-active: none; + --button-transition: all 0.2s ease; + --button-large-padding: var(--spacing-lg) calc(2 * var(--spacing-lg)); + --button-large-radius: var(--radius-md); + --button-large-text-size: var(--text-lg); + --button-large-text-weight: 600; + --button-primary-shadow: none; + --button-secondary-shadow: var(--button-primary-shadow); + --button-small-padding: var(--spacing-sm) calc(1.5 * var(--spacing-sm)); + --button-small-radius: var(--radius-md); + --button-small-text-size: var(--text-sm); + --button-small-text-weight: 400; + --button-medium-padding: var(--spacing-md) calc(2 * var(--spacing-md)); + --button-medium-radius: var(--radius-md); + --button-medium-text-size: var(--text-md); + --button-medium-text-weight: 600; +} diff --git a/js/_website/src/lib/assets/tweets.json b/js/_website/src/lib/assets/tweets.json new file mode 100644 index 0000000..1485da6 --- /dev/null +++ b/js/_website/src/lib/assets/tweets.json @@ -0,0 +1,65 @@ +[ + { + "name": "Anastasios Nikolas Angelopoulos", + "handle": "ml_angelopoulos", + "link": "https://twitter.com/ml_angelopoulos/status/1925288220360909286", + "content": "We love @Gradio.

It helped us scale to our first million users. Amazing and undervalued that Gradio can do this.

cc @_akhaliq", + "profile_pic": "4sbu_-Af_400x400.jpeg" + }, + { + "name": "Jaydeep", + "handle": "_jaydeepkarale", + "link": "https://x.com/_jaydeepkarale/status/1639978163643252736", + "content": "Gradio was love at first sight..so easy to use", + "profile_pic": "NLkT5L7d_400x400.jpeg" + }, + { + "name": "Will Rice", + "handle": "_Will_Rice", + "link": "https://twitter.com/_Will_Rice/status/1430258610131582979", + "content": "Just tried out @Gradio and I am very impressed. Only took like 10mins to put together a #tts demo.", + "profile_pic": "LsCnjnsl_400x400.jpeg" + }, + { + "name": "Art Litvinau", + "handle": "ArtLitvinau", + "link": "https://x.com/ArtLitvinau/status/1678937099343364097", + "content": "I love how easy is to build quick prototypes with @Gradio ⚡️ this one took me 45 minutes with no previous experience with the library", + "profile_pic": "lpJQ2BrK_400x400.jpeg" + }, + { + "name": "Shirochenko Dmitriy", + "handle": "dmshirochenko", + "link": "https://x.com/dmshirochenko/status/1924417517503627346", + "content": "Spent the weekend prototyping LLM interfaces directly in Python. @gradio-app is a game changer for rapid UI development. Forget wrestling with JS/CSS; share your model in seconds. Thinking this unlocks serious iteration speed.", + "profile_pic": "ADswb5-b_400x400.jpeg" + }, + { + "name": "iCode2", + "handle": "Ifeanyidiaye", + "link": "https://x.com/Ifeanyidiaye/status/1785948320596099111", + "content": "Apart from being very easy to use, one thing that I simply love about @Gradio is its Javascript client, which makes it easy to build a nice HTML frontend for Python applications. This integration makes me rank gradio high in my list of favorite Python libraries.", + "profile_pic": "d6sTU6ul_400x400.jpeg" + }, + { + "name": "Amar Saini", + "handle": "_Epoching_", + "link": "https://twitter.com/_Epoching_/status/1471091318482825219", + "content": "Just built a ️@Gradio app for a video related deep learning project.
I’m astonished by how simple it is to use & how elegant it looks! Lots and lots of great features & flexibility. Thanks for making this ❤ ", + "profile_pic": "pwMrDOBv_400x400.jpeg" + }, + { + "name": "Roxana Daneshjou MD/PhD", + "handle": "RoxanaDaneshjou", + "link": "https://twitter.com/RoxanaDaneshjou/status/1418399829944721415", + "content": "Honestly, without @Gradio, we would not be doing a real time AI trial. We have many other ideas for algorithms we want to test through clinical trials, and we know it's possible thanks to @Gradio.", + "profile_pic": "ITFspAMm_x96.jpg" + }, + { + "name": "Vinay Prabhu", + "handle": "vinayprabhu", + "link": "https://twitter.com/vinayprabhu/status/1324409497641652225", + "content": "Dear #MachineLearning twitter,
If you haven't typed:
$ pip install gradio
yet, now would be a damn good time.
Especially if you are working in computer vision & deploying models in the real world. ", + "profile_pic": "1013607349943058433.jpg" + } +] diff --git a/js/_website/src/lib/assets/twitter/0Hxb1ESL_x96.png b/js/_website/src/lib/assets/twitter/0Hxb1ESL_x96.png new file mode 100644 index 0000000..f483158 Binary files /dev/null and b/js/_website/src/lib/assets/twitter/0Hxb1ESL_x96.png differ diff --git a/js/_website/src/lib/assets/twitter/1013607349943058433.jpg b/js/_website/src/lib/assets/twitter/1013607349943058433.jpg new file mode 100644 index 0000000..97d3e95 Binary files /dev/null and b/js/_website/src/lib/assets/twitter/1013607349943058433.jpg differ diff --git a/js/_website/src/lib/assets/twitter/1116115321218146304.jpg b/js/_website/src/lib/assets/twitter/1116115321218146304.jpg new file mode 100644 index 0000000..4e099ca Binary files /dev/null and b/js/_website/src/lib/assets/twitter/1116115321218146304.jpg differ diff --git a/js/_website/src/lib/assets/twitter/1362781887098454025.jpg b/js/_website/src/lib/assets/twitter/1362781887098454025.jpg new file mode 100644 index 0000000..40368a3 Binary files /dev/null and b/js/_website/src/lib/assets/twitter/1362781887098454025.jpg differ diff --git a/js/_website/src/lib/assets/twitter/4sbu_-Af_400x400.jpeg b/js/_website/src/lib/assets/twitter/4sbu_-Af_400x400.jpeg new file mode 100644 index 0000000..ecd26e4 Binary files /dev/null and b/js/_website/src/lib/assets/twitter/4sbu_-Af_400x400.jpeg differ diff --git a/js/_website/src/lib/assets/twitter/810912649827364864.jpg b/js/_website/src/lib/assets/twitter/810912649827364864.jpg new file mode 100644 index 0000000..bb95a6a Binary files /dev/null and b/js/_website/src/lib/assets/twitter/810912649827364864.jpg differ diff --git a/js/_website/src/lib/assets/twitter/889312280092999680.jpg b/js/_website/src/lib/assets/twitter/889312280092999680.jpg new file mode 100644 index 0000000..6ae2807 Binary files /dev/null and b/js/_website/src/lib/assets/twitter/889312280092999680.jpg differ diff --git a/js/_website/src/lib/assets/twitter/8vyTl51q_400x400.jpeg b/js/_website/src/lib/assets/twitter/8vyTl51q_400x400.jpeg new file mode 100644 index 0000000..f87d805 Binary files /dev/null and b/js/_website/src/lib/assets/twitter/8vyTl51q_400x400.jpeg differ diff --git a/js/_website/src/lib/assets/twitter/ADswb5-b_400x400.jpeg b/js/_website/src/lib/assets/twitter/ADswb5-b_400x400.jpeg new file mode 100644 index 0000000..321408f Binary files /dev/null and b/js/_website/src/lib/assets/twitter/ADswb5-b_400x400.jpeg differ diff --git a/js/_website/src/lib/assets/twitter/GDLc7Oe4_400x400.jpeg b/js/_website/src/lib/assets/twitter/GDLc7Oe4_400x400.jpeg new file mode 100644 index 0000000..928e10f Binary files /dev/null and b/js/_website/src/lib/assets/twitter/GDLc7Oe4_400x400.jpeg differ diff --git a/js/_website/src/lib/assets/twitter/ITFspAMm_x96.jpg b/js/_website/src/lib/assets/twitter/ITFspAMm_x96.jpg new file mode 100644 index 0000000..fb4fedb Binary files /dev/null and b/js/_website/src/lib/assets/twitter/ITFspAMm_x96.jpg differ diff --git a/js/_website/src/lib/assets/twitter/LsCnjnsl_400x400.jpeg b/js/_website/src/lib/assets/twitter/LsCnjnsl_400x400.jpeg new file mode 100644 index 0000000..0c03a76 Binary files /dev/null and b/js/_website/src/lib/assets/twitter/LsCnjnsl_400x400.jpeg differ diff --git a/js/_website/src/lib/assets/twitter/NLkT5L7d_400x400.jpeg b/js/_website/src/lib/assets/twitter/NLkT5L7d_400x400.jpeg new file mode 100644 index 0000000..0652349 Binary files /dev/null and b/js/_website/src/lib/assets/twitter/NLkT5L7d_400x400.jpeg differ diff --git a/js/_website/src/lib/assets/twitter/R1gj6nb3_x96.jpg b/js/_website/src/lib/assets/twitter/R1gj6nb3_x96.jpg new file mode 100644 index 0000000..e12deae Binary files /dev/null and b/js/_website/src/lib/assets/twitter/R1gj6nb3_x96.jpg differ diff --git a/js/_website/src/lib/assets/twitter/d6sTU6ul_400x400.jpeg b/js/_website/src/lib/assets/twitter/d6sTU6ul_400x400.jpeg new file mode 100644 index 0000000..f21caf6 Binary files /dev/null and b/js/_website/src/lib/assets/twitter/d6sTU6ul_400x400.jpeg differ diff --git a/js/_website/src/lib/assets/twitter/d8qFeBSq_x96.png b/js/_website/src/lib/assets/twitter/d8qFeBSq_x96.png new file mode 100644 index 0000000..8326576 Binary files /dev/null and b/js/_website/src/lib/assets/twitter/d8qFeBSq_x96.png differ diff --git a/js/_website/src/lib/assets/twitter/heart.svg b/js/_website/src/lib/assets/twitter/heart.svg new file mode 100644 index 0000000..a6cc7d8 --- /dev/null +++ b/js/_website/src/lib/assets/twitter/heart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/twitter/ksO1TT2P_400x400.jpeg b/js/_website/src/lib/assets/twitter/ksO1TT2P_400x400.jpeg new file mode 100644 index 0000000..0b996bc Binary files /dev/null and b/js/_website/src/lib/assets/twitter/ksO1TT2P_400x400.jpeg differ diff --git a/js/_website/src/lib/assets/twitter/logo.svg b/js/_website/src/lib/assets/twitter/logo.svg new file mode 100644 index 0000000..49f4544 --- /dev/null +++ b/js/_website/src/lib/assets/twitter/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/assets/twitter/lpJQ2BrK_400x400.jpeg b/js/_website/src/lib/assets/twitter/lpJQ2BrK_400x400.jpeg new file mode 100644 index 0000000..982125b Binary files /dev/null and b/js/_website/src/lib/assets/twitter/lpJQ2BrK_400x400.jpeg differ diff --git a/js/_website/src/lib/assets/twitter/pwMrDOBv_400x400.jpeg b/js/_website/src/lib/assets/twitter/pwMrDOBv_400x400.jpeg new file mode 100644 index 0000000..4b674f4 Binary files /dev/null and b/js/_website/src/lib/assets/twitter/pwMrDOBv_400x400.jpeg differ diff --git a/js/_website/src/lib/assets/twitter/redheart.svg b/js/_website/src/lib/assets/twitter/redheart.svg new file mode 100644 index 0000000..b1562fe --- /dev/null +++ b/js/_website/src/lib/assets/twitter/redheart.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/_website/src/lib/components/Banner.svelte b/js/_website/src/lib/components/Banner.svelte new file mode 100644 index 0000000..446d309 --- /dev/null +++ b/js/_website/src/lib/components/Banner.svelte @@ -0,0 +1,162 @@ + + +{#if visible} + + {badge_label} + {message} + {#if link_text} + + {link_text} + + + + + + {/if} + +{/if} + + diff --git a/js/_website/src/lib/components/BehaviorSection.svelte b/js/_website/src/lib/components/BehaviorSection.svelte new file mode 100644 index 0000000..c84cbd7 --- /dev/null +++ b/js/_website/src/lib/components/BehaviorSection.svelte @@ -0,0 +1,79 @@ + + +

Using {name} as an input component.

+ +

How {name} will pass its value to your function:

+ +

+ Type: + + {preprocess.return_doc.annotation} + +

+ +

+ {@html style_formatted_text(preprocess.return_doc.doc)} +

+ +
Example Code
+ +
+    
+    import gradio as gr
+
+    def predict(
+        value: {preprocess.return_doc.annotation}
+    ):
+        # process value from the {name} component
+        return "prediction"
+
+    interface = gr.Interface(predict, gr.{name}(), gr.Textbox())
+    interface.launch()
+
+
+ +
+ +

Using {name} as an output component

+ +

How {name} expects you to return a value:

+ +

+ Type: + + + {postprocess.parameter_doc[0].annotation} + +

+ +

+ {@html style_formatted_text(postprocess.parameter_doc[0].doc)} +

+ +
Example Code
+
+    
+    import gradio as gr
+
+    def predict(text) -> {postprocess.parameter_doc[0].annotation}
+        # process value to return to the {name} component
+        return value
+
+    interface = gr.Interface(predict, gr.Textbox(), gr.{name}())
+    interface.launch()
+
+
diff --git a/js/_website/src/lib/components/CopyButton.svelte b/js/_website/src/lib/components/CopyButton.svelte new file mode 100644 index 0000000..17edb3a --- /dev/null +++ b/js/_website/src/lib/components/CopyButton.svelte @@ -0,0 +1,20 @@ + + + diff --git a/js/_website/src/lib/components/CopyMarkdown.svelte b/js/_website/src/lib/components/CopyMarkdown.svelte new file mode 100644 index 0000000..b0bff87 --- /dev/null +++ b/js/_website/src/lib/components/CopyMarkdown.svelte @@ -0,0 +1,455 @@ + + + + +
+
+ + +
+ + {#if open} + + + {/if} +
+ + diff --git a/js/_website/src/lib/components/Demos.svelte b/js/_website/src/lib/components/Demos.svelte new file mode 100644 index 0000000..2ed04db --- /dev/null +++ b/js/_website/src/lib/components/Demos.svelte @@ -0,0 +1,44 @@ + + +
+ {#key name} +
+ {#if url_version === "main"} + + {:else} + + {/if} +
+ {/key} +
+ + diff --git a/js/_website/src/lib/components/DemosLanding.svelte b/js/_website/src/lib/components/DemosLanding.svelte new file mode 100644 index 0000000..1d72da6 --- /dev/null +++ b/js/_website/src/lib/components/DemosLanding.svelte @@ -0,0 +1,66 @@ + + +
+ +
+
+ {#each tabs as { demo, code }, i (demo)} +
+
+ {@html code} +
+
+ {#key demo} + + {/key} +
+
+ {/each} +
diff --git a/js/_website/src/lib/components/DemosSection.svelte b/js/_website/src/lib/components/DemosSection.svelte new file mode 100644 index 0000000..cdee42d --- /dev/null +++ b/js/_website/src/lib/components/DemosSection.svelte @@ -0,0 +1,40 @@ + + +
+
+
+
+ {#each demos as demo, i} + + {/each} +
+ {#each demos as demo, i} +
+ +
+ {/each} +
+
+
diff --git a/js/_website/src/lib/components/Details.svelte b/js/_website/src/lib/components/Details.svelte new file mode 100644 index 0000000..3d10ed9 --- /dev/null +++ b/js/_website/src/lib/components/Details.svelte @@ -0,0 +1,41 @@ + + +
+ + {#if open} +
{@html content}
+ {/if} +
+ + diff --git a/js/_website/src/lib/components/DocsCopyMarkdown.svelte b/js/_website/src/lib/components/DocsCopyMarkdown.svelte new file mode 100644 index 0000000..7ef5221 --- /dev/null +++ b/js/_website/src/lib/components/DocsCopyMarkdown.svelte @@ -0,0 +1,509 @@ + + + + +
+
+ + +
+ + {#if open} + + + {/if} +
+ + diff --git a/js/_website/src/lib/components/DocsNav.svelte b/js/_website/src/lib/components/DocsNav.svelte new file mode 100644 index 0000000..b757ac5 --- /dev/null +++ b/js/_website/src/lib/components/DocsNav.svelte @@ -0,0 +1,75 @@ + + +
+
(show_nav = false)} + class:hidden={!show_nav} + class="w-64 flex-shrink-0 max-h-[calc(100vh-4rem)] overflow-y-auto fixed inset-0 z-50 bg-white lg:bg-transparent dark:bg-neutral-900 lg:dark:bg-transparent p-6 lg:sticky lg:top-8 lg:self-start lg:block" + id="mobile-nav" +> + + +

+ Use our Docs MCP +

+ + {#if show_dropdown} +
+ +
+ {/if} + +
+ {#each library_pages as category_pages} +
+

+ {category_pages.category} +

+ +
+ {/each} +
+
diff --git a/js/_website/src/lib/components/DocsNavCustom.svelte b/js/_website/src/lib/components/DocsNavCustom.svelte new file mode 100644 index 0000000..d341321 --- /dev/null +++ b/js/_website/src/lib/components/DocsNavCustom.svelte @@ -0,0 +1,109 @@ + + + + + + +
(show_nav = false)} + class:hidden={!show_nav} + class="w-64 flex-shrink-0 max-h-[calc(100vh-4rem)] overflow-y-auto fixed inset-0 z-50 bg-white lg:bg-transparent dark:bg-neutral-900 lg:dark:bg-transparent p-6 lg:sticky lg:top-8 lg:self-start lg:block" + id="mobile-nav" +> + + +
+

+ {title} +

+
    + {#each Object.entries(items) as [name, url] (name)} +
  • + {name} +
  • + {/each} +
+
+
diff --git a/js/_website/src/lib/components/EventListeners.svelte b/js/_website/src/lib/components/EventListeners.svelte new file mode 100644 index 0000000..e87f062 --- /dev/null +++ b/js/_website/src/lib/components/EventListeners.svelte @@ -0,0 +1,175 @@ + + +
+

Description

+

+ Event listeners allow you to respond to user interactions with the UI + components you've defined in a Gradio Blocks app. When a user interacts with + an element, such as changing a slider value or uploading an image, a + function is called. +

+
+ +
+

+ Supported Event Listeners +

+

+ The {fns[0].parent.replace("gradio.", "")} + component supports the following event listeners. Each event listener takes the + same parameters, which are listed in the + Event Parameters table below. +

+ +
+
+ Listeners +
+
+ {#each fns as fn} +
+ +
{fn.parent.replace("gradio.", "")}.{fn.name}(fn, ···)
+
+
+

{@html style_formatted_text(fn.description)}

+
+
+ {/each} +
+
+
+ +
+

+ Event Parameters +

+ +
+ + diff --git a/js/_website/src/lib/components/Footer.svelte b/js/_website/src/lib/components/Footer.svelte new file mode 100644 index 0000000..1e37638 --- /dev/null +++ b/js/_website/src/lib/components/Footer.svelte @@ -0,0 +1,26 @@ + + + diff --git a/js/_website/src/lib/components/FunctionDoc.svelte b/js/_website/src/lib/components/FunctionDoc.svelte new file mode 100644 index 0000000..7d23281 --- /dev/null +++ b/js/_website/src/lib/components/FunctionDoc.svelte @@ -0,0 +1,86 @@ + + + + +
+
+

+ {fn.name} + +

+
+ + {#if fn.override_signature} +
+
{fn.override_signature}
+
+ {:else} +
+
{fn.parent}.{fn.name}({#each fn.parameters as param}{#if !("kwargs" in param) && !("default" in param) && param.name != "self"}{param.name}, {/if}{/each}···)
+
+ {/if} + +

+ Description + +

+

{@html fn.description}

+ + {#if fn.example} +

+ Example Usage + +

+
+
{@html fn.example}
+
+ {/if} + + {#if (fn.parameters.length > 0 && fn.parameters[0].name != "self") || fn.parameters.length > 1} + + {/if} +
diff --git a/js/_website/src/lib/components/FunctionsSection.svelte b/js/_website/src/lib/components/FunctionsSection.svelte new file mode 100644 index 0000000..fb64093 --- /dev/null +++ b/js/_website/src/lib/components/FunctionsSection.svelte @@ -0,0 +1,22 @@ + + +{#if event_listeners} + {#if fns && fns.length > 0} +
+ +
+
+ {/if} +{:else} +
+ {#each fns as fn} + + {/each} +
+
+{/if} diff --git a/js/_website/src/lib/components/GuidesSection.svelte b/js/_website/src/lib/components/GuidesSection.svelte new file mode 100644 index 0000000..691a458 --- /dev/null +++ b/js/_website/src/lib/components/GuidesSection.svelte @@ -0,0 +1,31 @@ + + +{#if guides && guides.length > 0} + +{/if} diff --git a/js/_website/src/lib/components/Header.svelte b/js/_website/src/lib/components/Header.svelte new file mode 100644 index 0000000..eb1205f --- /dev/null +++ b/js/_website/src/lib/components/Header.svelte @@ -0,0 +1,251 @@ + + + + +{#if ready} +
+ + Gradio logo + + + + + + + +
+ + {#if click_nav} +
+
+
+ (click_nav = false)}> + Gradio logo + + +
+ + + +
+ + +
+
+
+ {/if} +{/if} + + diff --git a/js/_website/src/lib/components/HelpersSection.svelte b/js/_website/src/lib/components/HelpersSection.svelte new file mode 100644 index 0000000..e69de29 diff --git a/js/_website/src/lib/components/JSDocsNav.svelte b/js/_website/src/lib/components/JSDocsNav.svelte new file mode 100644 index 0000000..e286178 --- /dev/null +++ b/js/_website/src/lib/components/JSDocsNav.svelte @@ -0,0 +1,130 @@ + + + + +
(show_nav = false)} + class:hidden={!show_nav} + class="w-64 flex-shrink-0 max-h-[calc(100vh-4rem)] overflow-y-auto fixed inset-0 z-50 bg-white lg:bg-transparent dark:bg-neutral-900 lg:dark:bg-transparent p-6 lg:sticky lg:top-8 lg:self-start lg:block" + id="mobile-nav" +> + + +
+ {#if version_dropdown} +
+ +
+ {/if} + +

+ + + + + + Storybook → +

+ +
+

+ Components +

+
    + {#each js_components as name} +
  • + {name} +
  • + {/each} +
+
+ +
+

+ Client +

+ +
+
+
diff --git a/js/_website/src/lib/components/LogoDownloadMenu.svelte b/js/_website/src/lib/components/LogoDownloadMenu.svelte new file mode 100644 index 0000000..c318cce --- /dev/null +++ b/js/_website/src/lib/components/LogoDownloadMenu.svelte @@ -0,0 +1,77 @@ + + +{#if show} +
+
+ Download Logo +
+ {#each downloads as download} + + {/each} +
+{/if} diff --git a/js/_website/src/lib/components/MetaTags.svelte b/js/_website/src/lib/components/MetaTags.svelte new file mode 100644 index 0000000..01bbe76 --- /dev/null +++ b/js/_website/src/lib/components/MetaTags.svelte @@ -0,0 +1,32 @@ + + + + {title} + + + + + + + + + + + + + + + + + diff --git a/js/_website/src/lib/components/ParamTable.svelte b/js/_website/src/lib/components/ParamTable.svelte new file mode 100644 index 0000000..5a9d4d3 --- /dev/null +++ b/js/_website/src/lib/components/ParamTable.svelte @@ -0,0 +1,318 @@ + + + + + diff --git a/js/_website/src/lib/components/RelatedGuides.svelte b/js/_website/src/lib/components/RelatedGuides.svelte new file mode 100644 index 0000000..e2cd99c --- /dev/null +++ b/js/_website/src/lib/components/RelatedGuides.svelte @@ -0,0 +1,58 @@ + + +
+

+ Related Guides +

+ + +
+ + diff --git a/js/_website/src/lib/components/ShortcutTable.svelte b/js/_website/src/lib/components/ShortcutTable.svelte new file mode 100644 index 0000000..eead58c --- /dev/null +++ b/js/_website/src/lib/components/ShortcutTable.svelte @@ -0,0 +1,148 @@ + + +
+
+ Shortcuts +
+
+ {#each shortcuts as shortcut} +
+ +
gradio.{shortcut[0]}
+
+
+
+ Interface String Shortcut + "{shortcut[1]}" +
+
+ Initialization + {shortcut[2]} +
+
+
+ {/each} +
+
+ + diff --git a/js/_website/src/lib/components/Slider.svelte b/js/_website/src/lib/components/Slider.svelte new file mode 100644 index 0000000..6b313bb --- /dev/null +++ b/js/_website/src/lib/components/Slider.svelte @@ -0,0 +1,127 @@ + + + + +
+ + +
+ + diff --git a/js/_website/src/lib/components/ThemeToggle.svelte b/js/_website/src/lib/components/ThemeToggle.svelte new file mode 100644 index 0000000..201481c --- /dev/null +++ b/js/_website/src/lib/components/ThemeToggle.svelte @@ -0,0 +1,112 @@ + + + diff --git a/js/_website/src/lib/components/VersionDropdown.svelte b/js/_website/src/lib/components/VersionDropdown.svelte new file mode 100644 index 0000000..d6d06ef --- /dev/null +++ b/js/_website/src/lib/components/VersionDropdown.svelte @@ -0,0 +1,63 @@ + + + + + + + diff --git a/js/_website/src/lib/components/clickOutside.ts b/js/_website/src/lib/components/clickOutside.ts new file mode 100644 index 0000000..0a18c9a --- /dev/null +++ b/js/_website/src/lib/components/clickOutside.ts @@ -0,0 +1,26 @@ +/** Dispatch event on click outside of node */ +namespace svelte.JSX { + interface HTMLProps { + onclick_outside?: (e: CustomEvent) => void; + } +} + +export function clickOutside(node: Node) { + const handleClick = (event: MouseEvent) => { + if ( + node && + !node.contains(event.target as Node) && + !event.defaultPrevented + ) { + node.dispatchEvent(new CustomEvent("click_outside", node as any)); + } + }; + + document.addEventListener("click", handleClick, true); + + return { + destroy() { + document.removeEventListener("click", handleClick, true); + } + }; +} diff --git a/js/_website/src/lib/components/icons/Close.svelte b/js/_website/src/lib/components/icons/Close.svelte new file mode 100644 index 0000000..c64efc3 --- /dev/null +++ b/js/_website/src/lib/components/icons/Close.svelte @@ -0,0 +1,24 @@ + + + + + + diff --git a/js/_website/src/lib/components/icons/DownloadIcon.svelte b/js/_website/src/lib/components/icons/DownloadIcon.svelte new file mode 100644 index 0000000..9fbe4c1 --- /dev/null +++ b/js/_website/src/lib/components/icons/DownloadIcon.svelte @@ -0,0 +1,10 @@ + + diff --git a/js/_website/src/lib/components/icons/Fullscreen.svelte b/js/_website/src/lib/components/icons/Fullscreen.svelte new file mode 100644 index 0000000..da34d85 --- /dev/null +++ b/js/_website/src/lib/components/icons/Fullscreen.svelte @@ -0,0 +1,10 @@ + diff --git a/js/_website/src/lib/components/icons/IconArrowUpRight.svelte b/js/_website/src/lib/components/icons/IconArrowUpRight.svelte new file mode 100644 index 0000000..720d803 --- /dev/null +++ b/js/_website/src/lib/components/icons/IconArrowUpRight.svelte @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/js/_website/src/lib/components/icons/IconCaret.svelte b/js/_website/src/lib/components/icons/IconCaret.svelte new file mode 100644 index 0000000..06aae28 --- /dev/null +++ b/js/_website/src/lib/components/icons/IconCaret.svelte @@ -0,0 +1,39 @@ + + + + + + + diff --git a/js/_website/src/lib/components/icons/IconCheck.svelte b/js/_website/src/lib/components/icons/IconCheck.svelte new file mode 100644 index 0000000..0a1227c --- /dev/null +++ b/js/_website/src/lib/components/icons/IconCheck.svelte @@ -0,0 +1,33 @@ + + + + + diff --git a/js/_website/src/lib/components/icons/IconCopy.svelte b/js/_website/src/lib/components/icons/IconCopy.svelte new file mode 100644 index 0000000..cc3bcfb --- /dev/null +++ b/js/_website/src/lib/components/icons/IconCopy.svelte @@ -0,0 +1,40 @@ + + + + + diff --git a/js/_website/src/lib/components/icons/IconHuggingChat.svelte b/js/_website/src/lib/components/icons/IconHuggingChat.svelte new file mode 100644 index 0000000..3108cf0 --- /dev/null +++ b/js/_website/src/lib/components/icons/IconHuggingChat.svelte @@ -0,0 +1,62 @@ + + + + HuggingChat + + + + + + + diff --git a/js/_website/src/lib/components/icons/Lightbulb.svelte b/js/_website/src/lib/components/icons/Lightbulb.svelte new file mode 100644 index 0000000..4f8d0a4 --- /dev/null +++ b/js/_website/src/lib/components/icons/Lightbulb.svelte @@ -0,0 +1,14 @@ + + + diff --git a/js/_website/src/lib/components/mini/MiniAnnotatedImage.svelte b/js/_website/src/lib/components/mini/MiniAnnotatedImage.svelte new file mode 100644 index 0000000..24e4109 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniAnnotatedImage.svelte @@ -0,0 +1,117 @@ +
+
+
+
+ Annotated +
+
+
+
+
+ + +
+
+ AnnotatedImage +
+ + diff --git a/js/_website/src/lib/components/mini/MiniAudio.svelte b/js/_website/src/lib/components/mini/MiniAudio.svelte new file mode 100644 index 0000000..2cad611 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniAudio.svelte @@ -0,0 +1,98 @@ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0:15 +
+
+
+ Audio +
+ + diff --git a/js/_website/src/lib/components/mini/MiniButton.svelte b/js/_website/src/lib/components/mini/MiniButton.svelte new file mode 100644 index 0000000..c2a9622 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniButton.svelte @@ -0,0 +1,17 @@ + + +
+
+ +
+ + Button + +
diff --git a/js/_website/src/lib/components/mini/MiniChatbot.svelte b/js/_website/src/lib/components/mini/MiniChatbot.svelte new file mode 100644 index 0000000..41ccebf --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniChatbot.svelte @@ -0,0 +1,98 @@ +
+
+
+
+
+
Hi! How can I help?
+
+
+
Hello!
+
+
+
👋
+
+
+
+
+ + Chatbot + +
+ + diff --git a/js/_website/src/lib/components/mini/MiniCheckbox.svelte b/js/_website/src/lib/components/mini/MiniCheckbox.svelte new file mode 100644 index 0000000..dec108d --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniCheckbox.svelte @@ -0,0 +1,83 @@ + + +
+
+ +
+ + Checkbox + +
+ + diff --git a/js/_website/src/lib/components/mini/MiniCode.svelte b/js/_website/src/lib/components/mini/MiniCode.svelte new file mode 100644 index 0000000..562a3e7 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniCode.svelte @@ -0,0 +1,121 @@ +
+
+
+
+ 1 + 2 + 3 +
+
+
+ def + hello(): +
+
+ print("Hi") +
+
+ return + 42 +
+
+
+
+ + Code + +
+ + diff --git a/js/_website/src/lib/components/mini/MiniDataframe.svelte b/js/_website/src/lib/components/mini/MiniDataframe.svelte new file mode 100644 index 0000000..bc76af3 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniDataframe.svelte @@ -0,0 +1,52 @@ +
+
+
+
+
+ A +
+
+ B +
+
+ C +
+
+ {#each Array(4) as _} +
+
+ ··· +
+
+ ··· +
+
+ ··· +
+
+ {/each} +
+
+ + Dataframe + +
diff --git a/js/_website/src/lib/components/mini/MiniDateTime.svelte b/js/_website/src/lib/components/mini/MiniDateTime.svelte new file mode 100644 index 0000000..1e5775b --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniDateTime.svelte @@ -0,0 +1,90 @@ +
+
+
+ + +
+
+ DateTime +
+ + diff --git a/js/_website/src/lib/components/mini/MiniDraggable.svelte b/js/_website/src/lib/components/mini/MiniDraggable.svelte new file mode 100644 index 0000000..544d859 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniDraggable.svelte @@ -0,0 +1,142 @@ + + +
+
+
+ {#each draggable_items as item, idx} +
handle_drag_start(e, idx)} + on:dragover={(e) => handle_drag_over(e, idx)} + on:dragleave={handle_drag_leave} + on:drop={(e) => handle_drop(e, idx)} + role="button" + tabindex="0" + > + ⋮⋮ + {item} +
+ {/each} +
+
+ Draggable +
+ + diff --git a/js/_website/src/lib/components/mini/MiniDropdown.svelte b/js/_website/src/lib/components/mini/MiniDropdown.svelte new file mode 100644 index 0000000..e7731e9 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniDropdown.svelte @@ -0,0 +1,73 @@ +
+
+
+ + +
+
+ Dropdown +
+ + diff --git a/js/_website/src/lib/components/mini/MiniFileExplorer.svelte b/js/_website/src/lib/components/mini/MiniFileExplorer.svelte new file mode 100644 index 0000000..0e6f419 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniFileExplorer.svelte @@ -0,0 +1,78 @@ +
+
+
+
+ 📁 + Documents +
+
+ 📄 + file.txt +
+
+ 📁 + Images +
+
+
+ FileExplorer +
+ + diff --git a/js/_website/src/lib/components/mini/MiniGallery.svelte b/js/_website/src/lib/components/mini/MiniGallery.svelte new file mode 100644 index 0000000..c2356a5 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniGallery.svelte @@ -0,0 +1,146 @@ + + +
+
+ +
+ + Gallery + +
+ + diff --git a/js/_website/src/lib/components/mini/MiniHighlightedText.svelte b/js/_website/src/lib/components/mini/MiniHighlightedText.svelte new file mode 100644 index 0000000..3a28fc2 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniHighlightedText.svelte @@ -0,0 +1,61 @@ +
+
+
+ Good + sentiment + bad +
+
+ + HighlightedText + +
+ + diff --git a/js/_website/src/lib/components/mini/MiniImageSlider.svelte b/js/_website/src/lib/components/mini/MiniImageSlider.svelte new file mode 100644 index 0000000..e5cd76f --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniImageSlider.svelte @@ -0,0 +1,113 @@ + + + + +
+
+
+ Blurred + + Original +
+ +
+
+ + + +
+
+
+
+ + ImageSlider + +
+ + diff --git a/js/_website/src/lib/components/mini/MiniModel3D.svelte b/js/_website/src/lib/components/mini/MiniModel3D.svelte new file mode 100644 index 0000000..5d9a093 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniModel3D.svelte @@ -0,0 +1,110 @@ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + Model3D + +
+ + diff --git a/js/_website/src/lib/components/mini/MiniNumber.svelte b/js/_website/src/lib/components/mini/MiniNumber.svelte new file mode 100644 index 0000000..201f9ab --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniNumber.svelte @@ -0,0 +1,40 @@ +
+
+ +
+ Number +
+ + diff --git a/js/_website/src/lib/components/mini/MiniPlot.svelte b/js/_website/src/lib/components/mini/MiniPlot.svelte new file mode 100644 index 0000000..9aeaffe --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniPlot.svelte @@ -0,0 +1,160 @@ +
+
+
+ + + + + + + + + + + + + + + + + + {#each [[20, 65], [35, 50], [55, 58], [75, 35], [95, 40], [115, 25]] as [x, y]} + + {/each} + + + + + + + + 0 + 5 + 10 + 15 + +
+
+ + Plot + +
diff --git a/js/_website/src/lib/components/mini/MiniRadio.svelte b/js/_website/src/lib/components/mini/MiniRadio.svelte new file mode 100644 index 0000000..7743d97 --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniRadio.svelte @@ -0,0 +1,95 @@ +
+
+
+ + +
+
+ Radio +
+ + diff --git a/js/_website/src/lib/components/mini/MiniSlider.svelte b/js/_website/src/lib/components/mini/MiniSlider.svelte new file mode 100644 index 0000000..11d719c --- /dev/null +++ b/js/_website/src/lib/components/mini/MiniSlider.svelte @@ -0,0 +1,109 @@ + + +
+
+
+
+ 0 + + 100 +
+
+
+ + Slider + +
+ + diff --git a/js/_website/src/lib/components/search/SearchIcon.svelte b/js/_website/src/lib/components/search/SearchIcon.svelte new file mode 100644 index 0000000..794e57a --- /dev/null +++ b/js/_website/src/lib/components/search/SearchIcon.svelte @@ -0,0 +1,15 @@ + + Search + + + diff --git a/js/_website/src/lib/components/search/index.ts b/js/_website/src/lib/components/search/index.ts new file mode 100644 index 0000000..ffa4a4e --- /dev/null +++ b/js/_website/src/lib/components/search/index.ts @@ -0,0 +1 @@ +export { default } from "./search.svelte"; diff --git a/js/_website/src/lib/components/search/search-worker.ts b/js/_website/src/lib/components/search/search-worker.ts new file mode 100644 index 0000000..db2ff65 --- /dev/null +++ b/js/_website/src/lib/components/search/search-worker.ts @@ -0,0 +1,17 @@ +import { create_pages_index, search_pages_index } from "./search"; + +addEventListener("message", async (e) => { + const { type, payload } = e.data; + + if (type === "load") { + const posts = await fetch("/search-api").then((res) => res.json()); + create_pages_index(posts); + postMessage({ type: "ready" }); + } + + if (type === "search") { + const search_term = payload.search_term; + const results = search_pages_index(search_term); + postMessage({ type: "results", payload: { results, search_term } }); + } +}); diff --git a/js/_website/src/lib/components/search/search.svelte b/js/_website/src/lib/components/search/search.svelte new file mode 100644 index 0000000..641a71b --- /dev/null +++ b/js/_website/src/lib/components/search/search.svelte @@ -0,0 +1,382 @@ + + + + + + +{#if open} +
+
+ +
+ {#if results.length} + + {:else} + {#if search_term} + {#if search === "load"} +

+ Searching for results... +

+ {:else} +

+ No results found. Try using a different term. +

+ {/if} + {/if} + + {/if} +
+
+{/if} + + diff --git a/js/_website/src/lib/components/search/search.ts b/js/_website/src/lib/components/search/search.ts new file mode 100644 index 0000000..6cda2fa --- /dev/null +++ b/js/_website/src/lib/components/search/search.ts @@ -0,0 +1,71 @@ +import FlexSearch from "flexsearch"; + +export type Page = { + content: string; + slug: string; + title: string; + type: string; +}; + +export type Result = { + content: string[]; + slug: string; + title: string; + type?: string; +}; + +let pages_index: FlexSearch.Index; +let pages: Page[]; + +export function create_pages_index(data: Page[]) { + pages_index = new FlexSearch.Index({ tokenize: "forward" }); + + data.forEach((page, i) => { + const item = `${page.title} ${page.content}`; + pages_index.add(i, item); + }); + + pages = data; +} + +export function search_pages_index(search_term: string) { + const match = search_term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const results = pages_index.search(match); + return results + .map((index) => pages[index as number]) + .map(({ slug, title, content, type }) => { + return { + slug, + title: replace_text_with_marker(title, match), + content: get_matches(content, match), + type + }; + }); +} + +function replace_text_with_marker(text: string, match: string) { + const regex = new RegExp(match, "gi"); + return text.replaceAll( + regex, + (match) => `${match}` + ); +} + +function get_matches(text: string, search_term: string, limit = 1) { + const regex = new RegExp(search_term, "gi"); + const indexes = []; + let matches = 0; + let match; + + while ((match = regex.exec(text)) !== null && matches < limit) { + indexes.push(match.index); + matches++; + } + + return indexes.map((index) => { + const start = index - 20; + const end = index + 80; + const excerpt = text.substring(start, end).trim(); + return `...${replace_text_with_marker(excerpt, search_term)}...`; + }); +} diff --git a/js/_website/src/lib/icons/Close.svelte b/js/_website/src/lib/icons/Close.svelte new file mode 100644 index 0000000..fc4a558 --- /dev/null +++ b/js/_website/src/lib/icons/Close.svelte @@ -0,0 +1,19 @@ + + + + + + diff --git a/js/_website/src/lib/icons/CopyButton.svelte b/js/_website/src/lib/icons/CopyButton.svelte new file mode 100644 index 0000000..c948a25 --- /dev/null +++ b/js/_website/src/lib/icons/CopyButton.svelte @@ -0,0 +1,20 @@ + + + diff --git a/js/_website/src/lib/prism.ts b/js/_website/src/lib/prism.ts new file mode 100644 index 0000000..8fb3248 --- /dev/null +++ b/js/_website/src/lib/prism.ts @@ -0,0 +1,57 @@ +import Prism from "prismjs"; +import "prismjs/components/prism-python"; +import "prismjs/components/prism-bash"; +import "prismjs/components/prism-json"; +import "prismjs/components/prism-typescript"; +import "prismjs/components/prism-javascript"; +import "prismjs/components/prism-csv"; +import "prismjs/components/prism-markup"; +import "prism-svelte"; + +(globalThis as any).Prism = Prism; + +Prism.languages.insertBefore("python", "keyword", { + namespace: { pattern: /\b[a-zA-Z_]\w*(?=\.)/ }, + "function-call": { pattern: /\b[a-zA-Z_]\w*(?=\s*\()/ }, + "keyword-argument": { + pattern: /\b[a-zA-Z_]\w*(?=\s*=(?!=))/, + alias: "attr-name" + }, + decorator: { + pattern: /(^[\t ]*)@\w+(?:\.\w+)*/m, + lookbehind: true, + alias: "annotation" + }, + "builtin-constant": { + pattern: /\b(?:True|False|None|self|cls)\b/, + alias: "constant" + } +}); + +const langs: Record = { + python: "python", + py: "python", + bash: "bash", + shell: "bash", + csv: "csv", + html: "html", + json: "json", + typescript: "typescript", + ts: "typescript", + javascript: "javascript", + js: "javascript", + directory: "json", + svelte: "svelte", + sv: "svelte", + md: "markdown", + css: "css" +}; + +function highlight(code: string, lang: string | null | undefined): string { + const _lang = langs[lang || ""] || ""; + const grammar = Prism.languages[_lang] || Prism.languages.plaintext; + const highlighted = Prism.highlight(code, grammar, _lang || "plaintext"); + return `
${highlighted}
`; +} + +export { Prism, langs, highlight }; diff --git a/js/_website/src/lib/stores/theme.ts b/js/_website/src/lib/stores/theme.ts new file mode 100644 index 0000000..3b56ad7 --- /dev/null +++ b/js/_website/src/lib/stores/theme.ts @@ -0,0 +1,45 @@ +import { writable, get } from "svelte/store"; + +export type Theme = "light" | "dark"; + +function get_initial_theme(): Theme { + if (typeof window === "undefined") return "light"; + + const stored = localStorage.getItem("theme") as Theme | null; + if (stored) return stored; + + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; +} + +function apply_theme(theme: Theme): void { + if (typeof window === "undefined") return; + + localStorage.setItem("theme", theme); + document.documentElement.classList.toggle("dark", theme === "dark"); +} + +function createThemeStore() { + const initial_theme = get_initial_theme(); + const store = writable(initial_theme); + const { subscribe, set } = store; + + apply_theme(initial_theme); + + return { + subscribe, + toggle: () => { + const current = get(store); + const new_theme: Theme = current === "light" ? "dark" : "light"; + apply_theme(new_theme); + set(new_theme); + }, + set: (theme: Theme) => { + apply_theme(theme); + set(theme); + } + }; +} + +export const theme = createThemeStore(); diff --git a/js/_website/src/lib/templates/gradio/01_building-demos/01_interface.svx b/js/_website/src/lib/templates/gradio/01_building-demos/01_interface.svx new file mode 100644 index 0000000..c3c9654 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/01_building-demos/01_interface.svx @@ -0,0 +1,63 @@ + + + + +# {obj.name} + + +```python +gradio.Interface(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +def image_classifier(inp): + return {'cat': 0.3, 'dog': 0.7} + +demo = gr.Interface(fn=image_classifier, inputs="image", outputs="label") +demo.launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/01_building-demos/02_chatinterface.svx b/js/_website/src/lib/templates/gradio/01_building-demos/02_chatinterface.svx new file mode 100644 index 0000000..20f3ffe --- /dev/null +++ b/js/_website/src/lib/templates/gradio/01_building-demos/02_chatinterface.svx @@ -0,0 +1,87 @@ + + + + +# {obj.name} + + +```python +gradio.ChatInterface(fn, ···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +### Example Usage + +**Basic Example**: A chatbot that echoes back the users's message + +```python +import gradio as gr + +def echo(message, history): + return message + +demo = gr.ChatInterface(fn=echo, examples=["hello", "hola", "merhaba"], title="Echo Bot") +demo.launch() +``` + +**Custom Chatbot**: A `gr.ChatInterface` with a custom `gr.Chatbot` that includes a placeholder as well as upvote/downvote buttons. The upvote/downvote buttons are automatically added when a `.like()` event is attached to a `gr.Chatbot`. In order to attach event listeners to your custom chatbot, wrap the `gr.Chatbot` as well as the `gr.ChatInterface` inside of a `gr.Blocks` like this: + +```py +import gradio as gr + +def yes(message, history): + return "yes" + +def vote(data: gr.LikeData): + if data.liked: + print("You upvoted this response: " + data.value["value"]) + else: + print("You downvoted this response: " + data.value["value"]) + +with gr.Blocks() as demo: + chatbot = gr.Chatbot(placeholder="Your Personal Yes-Man
Ask Me Anything") + chatbot.like(vote, None, None) + gr.ChatInterface(fn=yes, chatbot=chatbot) + +demo.launch() +``` + + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/01_building-demos/03_tabbedinterface.svx b/js/_website/src/lib/templates/gradio/01_building-demos/03_tabbedinterface.svx new file mode 100644 index 0000000..b5d2a55 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/01_building-demos/03_tabbedinterface.svx @@ -0,0 +1,48 @@ + + + + +# {obj.name} + + +```python +gradio.TabbedInterface(interface_list, ···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/01_building-demos/04_blocks.svx b/js/_website/src/lib/templates/gradio/01_building-demos/04_blocks.svx new file mode 100644 index 0000000..1f039d4 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/01_building-demos/04_blocks.svx @@ -0,0 +1,69 @@ + + + + +# {obj.name} + + +```python +with gradio.Blocks(): +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr +def update(name): + return f"Welcome to Gradio, {name}!" + +with gr.Blocks() as demo: + gr.Markdown("Start typing below and then click **Run** to see the output.") + with gr.Row(): + inp = gr.Textbox(placeholder="What is your name?") + out = gr.Textbox() + btn = gr.Button("Run") + btn.click(fn=update, inputs=inp, outputs=out) + +demo.launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/01_building-demos/05_server.svx b/js/_website/src/lib/templates/gradio/01_building-demos/05_server.svx new file mode 100644 index 0000000..2b53719 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/01_building-demos/05_server.svx @@ -0,0 +1,66 @@ + + + + +# {obj.name} + + +```python +gradio.Server(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +from gradio import Server + +app = Server() + +@app.api(name="hello") +def hello(name: str) -> str: + return f"Hello {name}" + +@app.get("/") +def root(): + return {"message": "Hello World"} + +app.launch() +``` +{/if} + + +### Initialization + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/02_blocks-layout/01_render.svx b/js/_website/src/lib/templates/gradio/02_blocks-layout/01_render.svx new file mode 100644 index 0000000..66ed216 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/02_blocks-layout/01_render.svx @@ -0,0 +1,69 @@ + + + +# {obj.name} + + +```python +@gr.render(inputs=···) +def hello(···): + ... +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +with gr.Blocks() as demo: + textbox = gr.Textbox(label="Enter text") + + @gr.render(inputs=textbox) + def show_message(text): + if not text: + gr.Markdown("Please enter some text.") + else: + gr.Markdown(f"You entered: {text}") + +demo.launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + + + + \ No newline at end of file diff --git a/js/_website/src/lib/templates/gradio/02_blocks-layout/accordion.svx b/js/_website/src/lib/templates/gradio/02_blocks-layout/accordion.svx new file mode 100644 index 0000000..aafedf0 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/02_blocks-layout/accordion.svx @@ -0,0 +1,58 @@ + + + + +# {obj.name} + + +```python +gradio.Accordion(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +with gr.Accordion("See Details"): + gr.Markdown("lorem ipsum") +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/02_blocks-layout/column.svx b/js/_website/src/lib/templates/gradio/02_blocks-layout/column.svx new file mode 100644 index 0000000..cab25ac --- /dev/null +++ b/js/_website/src/lib/templates/gradio/02_blocks-layout/column.svx @@ -0,0 +1,64 @@ + + + + +# {obj.name} + + +```python +gradio.Column(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(scale=1): + text1 = gr.Textbox() + text2 = gr.Textbox() + with gr.Column(scale=4): + btn1 = gr.Button("Button 1") + btn2 = gr.Button("Button 2") +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/02_blocks-layout/group.svx b/js/_website/src/lib/templates/gradio/02_blocks-layout/group.svx new file mode 100644 index 0000000..0e536bf --- /dev/null +++ b/js/_website/src/lib/templates/gradio/02_blocks-layout/group.svx @@ -0,0 +1,59 @@ + + + + +# {obj.name} + + +```python +gradio.Group(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +with gr.Group(): + gr.Textbox(label="First") + gr.Textbox(label="Last") +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/02_blocks-layout/row.svx b/js/_website/src/lib/templates/gradio/02_blocks-layout/row.svx new file mode 100644 index 0000000..1aeca07 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/02_blocks-layout/row.svx @@ -0,0 +1,76 @@ + + + + +# {obj.name} + + +```python +gradio.Row(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +with gr.Blocks() as demo: + with gr.Row(): + gr.Image("lion.jpg", scale=2) + gr.Image("tiger.jpg", scale=1) +demo.launch() +``` +{/if} + + +### Initialization + + +### Controlling Width + +The widths of elements in a `Row` can be controlled via a combination of `scale` and `min_width` arguments that are present in every component. + +- `scale` is an integer that defines how an element will take up space in a `Row`. If `scale` is set to 0, the element will not expand to take up space. If `scale` is set to 1 or greater, the element will expand. Multiple elements in a row will expand proportional to their scale. Below, `btn2` will expand twice as much as `btn1`, while `btn0` will not expand at all: + +```python +with gr.Blocks() as demo: + with gr.Row(): + btn0 = gr.Button("Button 0", scale=0) + btn1 = gr.Button("Button 1", scale=1) + btn2 = gr.Button("Button 2", scale=2) +``` + +- `min_width` will set the minimum width the element will take. The Row will wrap if there isn't sufficient space to satisfy all `min_width` values. + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/02_blocks-layout/tab.svx b/js/_website/src/lib/templates/gradio/02_blocks-layout/tab.svx new file mode 100644 index 0000000..54a0500 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/02_blocks-layout/tab.svx @@ -0,0 +1,63 @@ + + + + +# {obj.name} + + +```python +gradio.Tab(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +with gr.Blocks() as demo: + with gr.Tab("Lion"): + gr.Image("lion.jpg") + gr.Button("New Lion") + with gr.Tab("Tiger"): + gr.Image("tiger.jpg") + gr.Button("New Tiger") +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/02_blocks-layout/walkthrough.svx b/js/_website/src/lib/templates/gradio/02_blocks-layout/walkthrough.svx new file mode 100644 index 0000000..a023150 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/02_blocks-layout/walkthrough.svx @@ -0,0 +1,92 @@ + + + + +# {walkthrough_obj.name} + + +```python +gradio.Walkthrough(···) +``` + + +### Description +## {@html style_formatted_text(walkthrough_obj.description)} + + + +{#if walkthrough_obj.example} +### Example Usage +```python +with gr.Walkthrough(selected=1) as walkthrough: + with gr.Step("Step 1", id=1): + btn = gr.Button("go to Step 2") + btn.click(lambda: gr.Walkthrough(selected=2), outputs=walkthrough) + with gr.Step("Step 2", id=2): + txt = gr.Textbox("Welcome to Step 2") +``` +{/if} + + +### Initialization + + + +{#if walkthrough_obj.demos && walkthrough_obj.demos.length > 0} + +### Demos + +{/if} + +{#if walkthrough_obj.fns && walkthrough_obj.fns.length > 0} + +### Methods + +{/if} + +{#if walkthrough_obj.guides && walkthrough_obj.guides.length > 0} + + + +{/if} + + +# {step_obj.name} + + +```python +gradio.Step(···) +``` + + +#### Description +## {@html style_formatted_text(step_obj.description)} + + +#### Initialization + + + +{#if step_obj.fns && step_obj.fns.length > 0} + +#### Methods + +{/if} + +{#if step_obj.guides && step_obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/01_introduction.svx b/js/_website/src/lib/templates/gradio/03_components/01_introduction.svx new file mode 100644 index 0000000..789e934 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/01_introduction.svx @@ -0,0 +1,90 @@ + + +# Components + +### Introduction + +## Gradio includes pre-built components that can be used as inputs or outputs in your Interface or Blocks with a single line of code. Components include preprocessing steps that convert user data submitted through browser to something that be can used by a Python function, and postprocessing steps to convert values returned by a Python function into something that can be displayed in a browser. + +## Consider an example with three inputs (Textbox, Number, and Image) and two outputs (Number and Gallery), below is a diagram of what our preprocessing will send to the function and what our postprocessing will require from it. + + + +### Events + +## Components also come with certain events that they support. These are methods that are triggered with user actions. Below is a table showing which events are supported for each component. All events are also listed (with parameters) in the component's docs. + + +
+ + + + + {/each} + + + + {#each Object.entries(components_no_dataclasses) as [name, obj] (name)} + + + {#each events as event} + + {/each} + + {/each} + +
+ {#each events as event} + {event}
+ {obj.name} + + {#if events_matrix[obj.name].includes(event.toLowerCase())} +

+ {:else} +

+ {/if} +
+
+ + + + \ No newline at end of file diff --git a/js/_website/src/lib/templates/gradio/03_components/annotatedimage.svx b/js/_website/src/lib/templates/gradio/03_components/annotatedimage.svx new file mode 100644 index 0000000..921258c --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/annotatedimage.svx @@ -0,0 +1,63 @@ + + + + +# {obj.name} + + +```python +gradio.AnnotatedImage(···) +``` + + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/audio.svx b/js/_website/src/lib/templates/gradio/03_components/audio.svx new file mode 100644 index 0000000..1c2d10d --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/audio.svx @@ -0,0 +1,108 @@ + + + + +# {obj.name} + + +```python +gradio.Audio(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + + +### Helper Classes +
+ + +### WaveformOptions + + + +```python +gradio.WaveformOptions(···) +``` + + +#### Description +## {@html style_formatted_text(waveform_obj.description)} + + +#### Initialization + + + +### is_audio_correct_length + +Validates that the audio length is within the specified min and max length (in seconds). +You can use this to construct a validator that will check if the user-provided audio is either too short or too long. + + +```python +import gradio as gr +demo = gr.Interface( + lambda x: x, + inputs="audio", + outputs="audio", + validator=lambda audio: gr.validators.is_audio_correct_length(audio, min_length=1, max_length=5) +) +demo.launch() +``` + + +#### Initialization + +
+ + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/barplot.svx b/js/_website/src/lib/templates/gradio/03_components/barplot.svx new file mode 100644 index 0000000..412e543 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/barplot.svx @@ -0,0 +1,148 @@ + + + + +# {obj.name} + + +```python +gradio.BarPlot(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +### Key Concepts + +`gr.LinePlot`, `gr.ScatterPlot`, and `gr.BarPlot` all share the same API. Here is a summary of the most important features. For full details and live demos, see the [Creating Plots](/guides/creating-plots) and [Time Plots](/guides/time-plots) guides. + +**Basic Usage with a DataFrame** + +Pass a `pd.DataFrame` as the value, and specify `x` and `y` column names. The y-axis must be numeric; the x-axis can be strings, numbers, categories, or datetimes. + +```python +import gradio as gr +import pandas as pd + +df = pd.DataFrame({"origin": ["US", "EU", "Asia"], "sales": [120, 95, 180]}) + +with gr.Blocks() as demo: + gr.BarPlot(df, x="origin", y="sales") +``` + +**Breaking Out Series by Color** + +Use the `color` argument to group bars by a categorical column. Use `color_map` to assign specific colors: + +```python +gr.BarPlot(df, x="origin", y="sales", color="year", + color_map={"2023": "#4488FF", "2024": "#FF8844"}) +``` + +**Aggregating Values** + +Use `x_bin` and `y_aggregate` to group and summarize data. For string x-axes, the string values act as category bins automatically. For numeric x-axes, `x_bin` sets the bin size: + +```python +gr.BarPlot(df, x="origin", y="sales", y_aggregate="sum") +gr.BarPlot(df, x="year", y="sales", x_bin=1, y_aggregate="mean") +``` + +For time-series data, pass a string suffix (`"s"`, `"m"`, `"h"`, or `"d"`) to `x_bin`: + +```python +gr.BarPlot(df, x="timestamp", y="sales", x_bin="1d", y_aggregate="sum") +``` + +**Interactive Selection and Zoom** + +Use the `.select` event listener to respond to region selections (click and drag). Combine with `.double_click` and `x_lim` to implement zoom in/out: + +```python +with gr.Blocks() as demo: + plot = gr.BarPlot(df, x="timestamp", y="sales") + + @plot.select + def zoom(selection: gr.SelectData): + return gr.BarPlot(x_lim=[selection.index[0], selection.index[1]]) + + plot.double_click(lambda: gr.BarPlot(x_lim=None), outputs=plot) +``` + +**Realtime Data** + +Use `gr.Timer` to keep plots updated with live data. You can attach the timer via `every`, or wire it up manually: + +```python +def get_data(): + return pd.DataFrame(...) # fetch latest data + +with gr.Blocks() as demo: + timer = gr.Timer(5) + plot = gr.BarPlot(get_data, x="time", y="sales", every=timer) +``` + +**Interactive Dashboards** + +Plots can be driven by other components (dropdowns, sliders, etc.) to create fully interactive dashboards: + +```python +with gr.Blocks() as demo: + year = gr.Dropdown(choices=[2022, 2023, 2024], value=2024) + plot = gr.BarPlot(x="origin", y="sales") + + def update(yr): + filtered = df[df["year"] == yr] + return gr.BarPlot(filtered) + + year.change(update, inputs=year, outputs=plot) +``` + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/button.svx b/js/_website/src/lib/templates/gradio/03_components/button.svx new file mode 100644 index 0000000..7a4e5b5 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/button.svx @@ -0,0 +1,63 @@ + + + + +# {obj.name} + + +```python +gradio.Button(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/chatbot.svx b/js/_website/src/lib/templates/gradio/03_components/chatbot.svx new file mode 100644 index 0000000..4f82996 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/chatbot.svx @@ -0,0 +1,247 @@ + + + + +# {obj.name} + + +```python +gradio.Chatbot(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + +The Chatbot component accepts a list of messages, where each message is a dictionary with `role` and `content` keys. This format is compatible with the message format expected by most LLM APIs (OpenAI, Claude, HuggingChat, etc.), making it easy to pipe model outputs directly into the component. + +The `role` key should be either `'user'` or `'assistant'`, and the `content` key can be a string (rendered as markdown/HTML) or a Gradio component (useful for displaying files, images, plots, and other media). + +As an example: + +```python +import gradio as gr + +history = [ + {"role": "assistant", "content": "I am happy to provide you that report and plot."}, + {"role": "assistant", "content": gr.Plot(value=make_plot_from_file('quaterly_sales.txt'))} +] + +with gr.Blocks() as demo: + gr.Chatbot(history) + +demo.launch() +``` + +For convenience, you can use the `ChatMessage` dataclass so that your text editor can give you autocomplete hints and typechecks. + +```python +import gradio as gr + +history = [ + gr.ChatMessage(role="assistant", content="How can I help you?"), + gr.ChatMessage(role="user", content="Can you make me a plot of quarterly sales?"), + gr.ChatMessage(role="assistant", content="I am happy to provide you that report and plot.") +] + +with gr.Blocks() as demo: + gr.Chatbot(history) + +demo.launch() +``` + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +### Examples + +**Displaying Thoughts/Tool Usage** + +You can provide additional metadata regarding any tools used to generate the response. +This is useful for displaying the thought process of LLM agents. For example, + +```python +def generate_response(history): + history.append( + ChatMessage(role="assistant", + content="The weather API says it is 20 degrees Celcius in New York.", + metadata={"title": "🛠️ Used tool Weather API"}) + ) + return history +``` + +Would be displayed as following: + +Gradio chatbot tool display + +You can also specify metadata with a plain python dictionary, + +```python +def generate_response(history): + history.append( + dict(role="assistant", + content="The weather API says it is 20 degrees Celcius in New York.", + metadata={"title": "🛠️ Used tool Weather API"}) + ) + return history +``` + +**Using Gradio Components Inside `gr.Chatbot`** + +The `Chatbot` component supports using many of the core Gradio components (such as `gr.Image`, `gr.Plot`, `gr.Audio`, and `gr.HTML`) inside of the chatbot. Simply include one of these components as the `content` of a message. Here's an example: + +```py +import gradio as gr + +def load(): + return [ + {"role": "user", "content": "Can you show me some media?"}, + {"role": "assistant", "content": "Here's an audio clip:"}, + {"role": "assistant", "content": gr.Audio("https://github.com/gradio-app/gradio/raw/main/gradio/media_assets/audio/audio_sample.wav")}, + {"role": "assistant", "content": "And here's a video:"}, + {"role": "assistant", "content": gr.Video("https://github.com/gradio-app/gradio/raw/main/gradio/media_assets/videos/world.mp4")} + ] + +with gr.Blocks() as demo: + chatbot = gr.Chatbot() + button = gr.Button("Load audio and video") + button.click(load, None, chatbot) + +demo.launch() +``` + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + + +### Helper Classes +
+ + +### ChatMessage + + + +```python +gradio.ChatMessage(···) +``` + + +#### Description +## {@html style_formatted_text(chatmessage_obj.description)} + + + + +### MetadataDict + +## {@html style_formatted_text(chatmessage_metadatadict.description)} + + + + +### OptionDict + +## {@html style_formatted_text(chatmessage_optiondict.description)} + + + +
+ +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/checkbox.svx b/js/_website/src/lib/templates/gradio/03_components/checkbox.svx new file mode 100644 index 0000000..4e8fef0 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/checkbox.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.Checkbox(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/checkboxgroup.svx b/js/_website/src/lib/templates/gradio/03_components/checkboxgroup.svx new file mode 100644 index 0000000..4915210 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/checkboxgroup.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.CheckboxGroup(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/clearbutton.svx b/js/_website/src/lib/templates/gradio/03_components/clearbutton.svx new file mode 100644 index 0000000..e110863 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/clearbutton.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.ClearButton(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/code.svx b/js/_website/src/lib/templates/gradio/03_components/code.svx new file mode 100644 index 0000000..218214b --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/code.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.Code(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/colorpicker.svx b/js/_website/src/lib/templates/gradio/03_components/colorpicker.svx new file mode 100644 index 0000000..a64cf58 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/colorpicker.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.ColorPicker(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/dataframe.svx b/js/_website/src/lib/templates/gradio/03_components/dataframe.svx new file mode 100644 index 0000000..6425d21 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/dataframe.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.Dataframe(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/dataset.svx b/js/_website/src/lib/templates/gradio/03_components/dataset.svx new file mode 100644 index 0000000..0812c80 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/dataset.svx @@ -0,0 +1,98 @@ + + + + +# {obj.name} + + +```python +gradio.Dataset(···) +``` + + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +### Examples + +**Updating a Dataset** + +In this example, we display a text dataset using `gr.Dataset` and then update it when the user clicks a button: + +```py +import gradio as gr + +philosophy_quotes = [ + ["I think therefore I am."], + ["The unexamined life is not worth living."] +] + +startup_quotes = [ + ["Ideas are easy. Implementation is hard"], + ["Make mistakes faster."] +] + +def show_startup_quotes(): + return gr.Dataset(samples=startup_quotes) + +with gr.Blocks() as demo: + textbox = gr.Textbox() + dataset = gr.Dataset(components=[textbox], samples=philosophy_quotes) + button = gr.Button() + + button.click(show_startup_quotes, None, dataset) + +demo.launch() +``` + + + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} + diff --git a/js/_website/src/lib/templates/gradio/03_components/datetime.svx b/js/_website/src/lib/templates/gradio/03_components/datetime.svx new file mode 100644 index 0000000..e61e0cd --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/datetime.svx @@ -0,0 +1,63 @@ + + + + +# {obj.name} + + +```python +gradio.DateTime(···) +``` + + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/dialogue.svx b/js/_website/src/lib/templates/gradio/03_components/dialogue.svx new file mode 100644 index 0000000..ca616c4 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/dialogue.svx @@ -0,0 +1,60 @@ + + + + +# {obj.name} + + +```python +gradio.Dialogue(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/downloadbutton.svx b/js/_website/src/lib/templates/gradio/03_components/downloadbutton.svx new file mode 100644 index 0000000..d6140b6 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/downloadbutton.svx @@ -0,0 +1,60 @@ + + + + +# {obj.name} + + +```python +gradio.DownloadButton(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/dropdown.svx b/js/_website/src/lib/templates/gradio/03_components/dropdown.svx new file mode 100644 index 0000000..170025a --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/dropdown.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.Dropdown(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/duplicatebutton.svx b/js/_website/src/lib/templates/gradio/03_components/duplicatebutton.svx new file mode 100644 index 0000000..26f0f1a --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/duplicatebutton.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.DuplicateButton(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/file.svx b/js/_website/src/lib/templates/gradio/03_components/file.svx new file mode 100644 index 0000000..179daee --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/file.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.File(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/fileexplorer.svx b/js/_website/src/lib/templates/gradio/03_components/fileexplorer.svx new file mode 100644 index 0000000..ba0ec54 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/fileexplorer.svx @@ -0,0 +1,60 @@ + + + + +# {obj.name} + + +```python +gradio.FileExplorer(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/gallery.svx b/js/_website/src/lib/templates/gradio/03_components/gallery.svx new file mode 100644 index 0000000..5e17ce7 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/gallery.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.Gallery(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/highlightedtext.svx b/js/_website/src/lib/templates/gradio/03_components/highlightedtext.svx new file mode 100644 index 0000000..1afa08e --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/highlightedtext.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.HighlightedText(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/html.svx b/js/_website/src/lib/templates/gradio/03_components/html.svx new file mode 100644 index 0000000..11235fa --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/html.svx @@ -0,0 +1,64 @@ + + + + +# {obj.name} + + +```python +gradio.HTML(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/image.svx b/js/_website/src/lib/templates/gradio/03_components/image.svx new file mode 100644 index 0000000..762f285 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/image.svx @@ -0,0 +1,139 @@ + + + + +# {obj.name} + + +```python +gradio.Image(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + + +### Understanding Image Types + +The `type` parameter controls the format of the data passed to your Python function. Choosing the right type avoids unnecessary conversions in your code: + +| `type` | Your function receives | Shape / Format | Best for | +| --- | --- | --- | --- | +| `"numpy"` (default) | `numpy.ndarray` | `(height, width, 3)`, dtype `uint8`, values 0–255 | ML models (PyTorch, TensorFlow, scikit-learn) | +| `"pil"` | `PIL.Image.Image` | PIL Image object | Image processing with Pillow | +| `"filepath"` | `str` | Path to a temporary file on disk | Large images, GIFs, SVGs, or when you need to read the file yourself | + +```python +import gradio as gr + +# For a model that expects a numpy array: +def predict(img): + # img is a numpy array with shape (H, W, 3) + return model(img) + +demo = gr.Interface(fn=predict, inputs=gr.Image(type="numpy"), outputs="label") + +# For a pipeline that works with file paths: +def process(path): + # path is a string like "/tmp/gradio/abcdef.webp" + return my_pipeline(path) + +demo = gr.Interface(fn=process, inputs=gr.Image(type="filepath"), outputs="image") +``` + +If you need grayscale input, set `image_mode="L"` — the array shape becomes `(height, width)` instead of `(height, width, 3)`. + +### `GIF` and `SVG` Image Formats + +The `gr.Image` component can process or display any image format that is [supported by the PIL library](https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html), including animated GIFs. In addition, it also supports the SVG image format. + +When the `gr.Image` component is used as an input component, the image is converted into a `str` filepath, a `PIL.Image` object, or a `numpy.array`, depending on the `type` parameter. However, animated GIF and SVG images are treated differently: + +* Animated `GIF` images can only be converted to `str` filepaths or `PIL.Image` objects. If they are converted to a `numpy.array` (which is the default behavior), only the first frame will be used. So if your demo expects an input `GIF` image, make sure to set the `type` parameter accordingly, e.g. + +```py +import gradio as gr + +demo = gr.Interface( + fn=lambda x:x, + inputs=gr.Image(type="filepath"), + outputs=gr.Image() +) + +demo.launch() +``` + +* For `SVG` images, the `type` parameter is ignored altogether and the image is always returned as an image filepath. This is because `SVG` images cannot be processed as `PIL.Image` or `numpy.array` objects. + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + + +### Helper Classes +
+ + + +### Webcam Options + + +```python +gradio.WebcamOptions(···) +``` + + +#### Description +## {@html style_formatted_text(webcam_options_obj.description)} + + +#### Initialization + + +
+ +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/imageeditor.svx b/js/_website/src/lib/templates/gradio/03_components/imageeditor.svx new file mode 100644 index 0000000..7a585b0 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/imageeditor.svx @@ -0,0 +1,141 @@ + + + + +# {obj.name} + + +```python +gradio.ImageEditor(···) +``` + + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + + +### Helper Classes +
+ + + +### Brush + + +```python +gradio.Brush(···) +``` + + +#### Description +## {@html style_formatted_text(brush_obj.description)} + + +#### Initialization + + + + +### Eraser + + +```python +gradio.Eraser(···) +``` + + +#### Description +## {@html style_formatted_text(eraser_obj.description)} + + +#### Initialization + + + + +### Layer Options + + +```python +gradio.LayerOptions(···) +``` + + +#### Description +## {@html style_formatted_text(layeroptions_obj.description)} + + +#### Initialization + + + + +### Webcam Options + + +```python +gradio.WebcamOptions(···) +``` + + +#### Description +## {@html style_formatted_text(webcam_options_obj.description)} + + +#### Initialization + + +
+ +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/imageslider.svx b/js/_website/src/lib/templates/gradio/03_components/imageslider.svx new file mode 100644 index 0000000..eda9c99 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/imageslider.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.ImageSlider(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/json.svx b/js/_website/src/lib/templates/gradio/03_components/json.svx new file mode 100644 index 0000000..5eea4fb --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/json.svx @@ -0,0 +1,73 @@ + + + + +# {obj.name} + + +```python +gradio.JSON(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/label.svx b/js/_website/src/lib/templates/gradio/03_components/label.svx new file mode 100644 index 0000000..25a4c93 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/label.svx @@ -0,0 +1,63 @@ + + + + +# {obj.name} + + +```python +gradio.Label(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/lineplot.svx b/js/_website/src/lib/templates/gradio/03_components/lineplot.svx new file mode 100644 index 0000000..5305a9b --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/lineplot.svx @@ -0,0 +1,151 @@ + + + + +# {obj.name} + + +```python +gradio.LinePlot(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +### Key Concepts + +`gr.LinePlot`, `gr.ScatterPlot`, and `gr.BarPlot` all share the same API. Here is a summary of the most important features. For full details and live demos, see the [Creating Plots](/guides/creating-plots) and [Time Plots](/guides/time-plots) guides. + +**Basic Usage with a DataFrame** + +Pass a `pd.DataFrame` as the value, and specify `x` and `y` column names. The y-axis must be numeric; the x-axis can be strings, numbers, categories, or datetimes. + +```python +import gradio as gr +import pandas as pd + +df = pd.DataFrame({"week": [1, 2, 3, 4], "price": [10, 25, 18, 30]}) + +with gr.Blocks() as demo: + gr.LinePlot(df, x="week", y="price") +``` + +**Breaking Out Series by Color** + +Use the `color` argument to split data into multiple series. Use `color_map` to assign specific colors: + +```python +gr.LinePlot(df, x="week", y="price", color="origin", + color_map={"US": "#FF9988", "EU": "#88EEAA"}) +``` + +**Aggregating Values** + +Use `x_bin` and `y_aggregate` to group and summarize data. For numeric x-axes, `x_bin` creates histogram-style bins. For string x-axes, strings act as category bins automatically: + +```python +gr.LinePlot(df, x="week", y="price", x_bin=2, y_aggregate="mean") +``` + +For time-series data, pass a string suffix (`"s"`, `"m"`, `"h"`, or `"d"`) to `x_bin`: + +```python +gr.LinePlot(df, x="timestamp", y="price", x_bin="1d", y_aggregate="mean") +``` + +**Interactive Selection and Zoom** + +Use the `.select` event listener to respond to region selections (click and drag). Combine with `.double_click` and `x_lim` to implement zoom in/out: + +```python +with gr.Blocks() as demo: + plot = gr.LinePlot(df, x="week", y="price") + + @plot.select + def zoom(selection: gr.SelectData): + return gr.LinePlot(x_lim=[selection.index[0], selection.index[1]]) + + plot.double_click(lambda: gr.LinePlot(x_lim=None), outputs=plot) +``` + +**Realtime Data** + +Use `gr.Timer` to keep plots updated with live data. You can attach the timer via `every`, or wire it up manually: + +```python +def get_data(): + return pd.DataFrame(...) # fetch latest data + +with gr.Blocks() as demo: + timer = gr.Timer(5) + plot = gr.LinePlot(get_data, x="time", y="price", every=timer) +``` + +**Interactive Dashboards** + +Plots can be driven by other components (dropdowns, sliders, etc.) to create fully interactive dashboards: + +```python +with gr.Blocks() as demo: + origin = gr.Dropdown(choices=["US", "EU", "Asia"], value="US") + plot = gr.LinePlot(x="week", y="price") + + def update(origin): + filtered = df[df["origin"] == origin] + return gr.LinePlot(filtered) + + origin.change(update, inputs=origin, outputs=plot) +``` + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/loginbutton.svx b/js/_website/src/lib/templates/gradio/03_components/loginbutton.svx new file mode 100644 index 0000000..b69ded7 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/loginbutton.svx @@ -0,0 +1,60 @@ + + + + +# {obj.name} + + +```python +gradio.LoginButton(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/markdown.svx b/js/_website/src/lib/templates/gradio/03_components/markdown.svx new file mode 100644 index 0000000..996414a --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/markdown.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.Markdown(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/model3d.svx b/js/_website/src/lib/templates/gradio/03_components/model3d.svx new file mode 100644 index 0000000..9cc166b --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/model3d.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.Model3D(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/multimodaltextbox.svx b/js/_website/src/lib/templates/gradio/03_components/multimodaltextbox.svx new file mode 100644 index 0000000..e433247 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/multimodaltextbox.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.MultimodalTextbox(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/navbar.svx b/js/_website/src/lib/templates/gradio/03_components/navbar.svx new file mode 100644 index 0000000..b5cf943 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/navbar.svx @@ -0,0 +1,59 @@ + + + +# {obj.name} + + +```python +gradio.Navbar(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} \ No newline at end of file diff --git a/js/_website/src/lib/templates/gradio/03_components/number.svx b/js/_website/src/lib/templates/gradio/03_components/number.svx new file mode 100644 index 0000000..47060ee --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/number.svx @@ -0,0 +1,62 @@ + + + + +# {obj.name} + + +```python +gradio.Number(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/paramviewer.svx b/js/_website/src/lib/templates/gradio/03_components/paramviewer.svx new file mode 100644 index 0000000..4480eb1 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/paramviewer.svx @@ -0,0 +1,75 @@ + + + + +# {obj.name} + + +```python +gradio.ParamViewer(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/plot.svx b/js/_website/src/lib/templates/gradio/03_components/plot.svx new file mode 100644 index 0000000..49d6ff5 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/plot.svx @@ -0,0 +1,63 @@ + + + + +# {obj.name} + + +```python +gradio.Plot(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/radio.svx b/js/_website/src/lib/templates/gradio/03_components/radio.svx new file mode 100644 index 0000000..b2bc35c --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/radio.svx @@ -0,0 +1,63 @@ + + + + +# {obj.name} + + +```python +gradio.Radio(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/scatterplot.svx b/js/_website/src/lib/templates/gradio/03_components/scatterplot.svx new file mode 100644 index 0000000..9b0f95b --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/scatterplot.svx @@ -0,0 +1,149 @@ + + + + +# {obj.name} + + +```python +gradio.ScatterPlot(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + + +### Initialization + + + +### Key Concepts + +`gr.LinePlot`, `gr.ScatterPlot`, and `gr.BarPlot` all share the same API. Here is a summary of the most important features. For full details and live demos, see the [Creating Plots](/guides/creating-plots) and [Time Plots](/guides/time-plots) guides. + +**Basic Usage with a DataFrame** + +Pass a `pd.DataFrame` as the value, and specify `x` and `y` column names. The y-axis must be numeric; the x-axis can be strings, numbers, categories, or datetimes. + +```python +import gradio as gr +import pandas as pd + +df = pd.DataFrame({"weight": [50, 70, 90, 60], "height": [160, 175, 180, 165]}) + +with gr.Blocks() as demo: + gr.ScatterPlot(df, x="weight", y="height") +``` + +**Breaking Out Series by Color** + +Use the `color` argument to split data into multiple series. The color column can be string/categorical or numeric. Use `color_map` to assign specific colors: + +```python +gr.ScatterPlot(df, x="weight", y="height", color="gender", + color_map={"M": "#4488FF", "F": "#FF8844"}) +``` + +**Aggregating Values** + +Use `x_bin` and `y_aggregate` to group and summarize data. For numeric x-axes, `x_bin` creates histogram-style bins. For string x-axes, strings act as category bins automatically: + +```python +gr.ScatterPlot(df, x="age_group", y="height", y_aggregate="mean") +gr.ScatterPlot(df, x="weight", y="height", x_bin=10, y_aggregate="mean") +``` + +For time-series data, pass a string suffix (`"s"`, `"m"`, `"h"`, or `"d"`) to `x_bin`: + +```python +gr.ScatterPlot(df, x="timestamp", y="value", x_bin="1h", y_aggregate="mean") +``` + +**Interactive Selection and Zoom** + +Use the `.select` event listener to respond to region selections (click and drag). Combine with `.double_click` and `x_lim` to implement zoom in/out: + +```python +with gr.Blocks() as demo: + plot = gr.ScatterPlot(df, x="weight", y="height") + + @plot.select + def zoom(selection: gr.SelectData): + return gr.ScatterPlot(x_lim=[selection.index[0], selection.index[1]]) + + plot.double_click(lambda: gr.ScatterPlot(x_lim=None), outputs=plot) +``` + +**Realtime Data** + +Use `gr.Timer` to keep plots updated with live data. You can attach the timer via `every`, or wire it up manually: + +```python +def get_data(): + return pd.DataFrame(...) # fetch latest data + +with gr.Blocks() as demo: + timer = gr.Timer(5) + plot = gr.ScatterPlot(get_data, x="weight", y="height", every=timer) +``` + +**Interactive Dashboards** + +Plots can be driven by other components (dropdowns, sliders, etc.) to create fully interactive dashboards: + +```python +with gr.Blocks() as demo: + category = gr.Dropdown(choices=["A", "B", "C"], value="A") + plot = gr.ScatterPlot(x="weight", y="height") + + def update(cat): + filtered = df[df["category"] == cat] + return gr.ScatterPlot(filtered) + + category.change(update, inputs=category, outputs=plot) +``` + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/sidebar.svx b/js/_website/src/lib/templates/gradio/03_components/sidebar.svx new file mode 100644 index 0000000..5ebeb5f --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/sidebar.svx @@ -0,0 +1,59 @@ + + + +# {obj.name} + + +```python +gradio.Sidebar(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +with gr.Blocks() as demo: + with gr.Sidebar(): + gr.Textbox() + gr.Button() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/simpleimage.svx b/js/_website/src/lib/templates/gradio/03_components/simpleimage.svx new file mode 100644 index 0000000..23796eb --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/simpleimage.svx @@ -0,0 +1,60 @@ + + + + +# {obj.name} + + +```python +gradio.SimpleImage(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/slider.svx b/js/_website/src/lib/templates/gradio/03_components/slider.svx new file mode 100644 index 0000000..aed9a5a --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/slider.svx @@ -0,0 +1,96 @@ + + + + +# {obj.name} + + +```python +gradio.Slider(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +### Common Patterns + +#### Logarithmic scale + +Sliders are linear by default. For parameters that vary over several orders of magnitude (e.g. learning rate), map the slider value inside your function: + +```python +import gradio as gr + +def train(lr_exp): + lr = 10 ** lr_exp # slider value -5 → lr 0.00001 + return f"Training with lr={lr}" + +demo = gr.Interface( + fn=train, + inputs=gr.Slider(-5, 0, value=-3, step=0.5, label="Learning rate (log₁₀)"), + outputs="text", +) +``` + +#### Reacting on release only + +By default, the slider triggers a `change` event on every movement. For expensive operations, listen to `release` instead so the function only runs when the user lets go: + +```python +import gradio as gr + +with gr.Blocks() as demo: + slider = gr.Slider(0, 100, label="Epochs") + output = gr.Textbox() + slider.release(fn=lambda v: f"Will train for {int(v)} epochs", inputs=slider, outputs=output) +``` + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/state.svx b/js/_website/src/lib/templates/gradio/03_components/state.svx new file mode 100644 index 0000000..5eb065f --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/state.svx @@ -0,0 +1,60 @@ + + + + +# {obj.name} + + +```python +gradio.State(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/textbox.svx b/js/_website/src/lib/templates/gradio/03_components/textbox.svx new file mode 100644 index 0000000..cb7b486 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/textbox.svx @@ -0,0 +1,99 @@ + + + + +# {obj.name} + + +```python +gradio.Textbox(···) +``` + + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} +### Common Patterns + +#### Multi-line text input + +By default, `gr.Textbox` renders as a single-line input. For longer text such as paragraphs or code, set `lines` to show multiple rows and optionally `max_lines` to cap the height: + +```python +import gradio as gr + +def summarize(text): + return f"Received {len(text.split())} words." + +demo = gr.Interface( + fn=summarize, + inputs=gr.Textbox(lines=5, max_lines=10, label="Paste your text here"), + outputs="text", +) +``` + +#### Password and sensitive input + +Set `type="password"` to mask characters as the user types. The value is passed as plain text to your function — masking is display-only: + +```python +import gradio as gr + +def check(password): + return "Access granted" if password == "secret" else "Wrong password" + +demo = gr.Interface( + fn=check, + inputs=gr.Textbox(type="password", label="Enter password"), + outputs="text", +) +``` + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/timer.svx b/js/_website/src/lib/templates/gradio/03_components/timer.svx new file mode 100644 index 0000000..1ca0eb3 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/timer.svx @@ -0,0 +1,60 @@ + + + + +# {obj.name} + + +```python +gradio.Timer(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/uploadbutton.svx b/js/_website/src/lib/templates/gradio/03_components/uploadbutton.svx new file mode 100644 index 0000000..cd2bb68 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/uploadbutton.svx @@ -0,0 +1,64 @@ + + + + +# {obj.name} + + +```python +gradio.UploadButton(···) +``` + + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/03_components/video.svx b/js/_website/src/lib/templates/gradio/03_components/video.svx new file mode 100644 index 0000000..054b8f5 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/03_components/video.svx @@ -0,0 +1,110 @@ + + + + +# {obj.name} + + +```python +gradio.Video(···) +``` + + + +### Description +## {@html style_formatted_text(obj.description)} + + +### Behavior + + + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + + +### Helper Classes +
+ + + +### Webcam Options + + +```python +gradio.WebcamOptions(···) +``` + + +#### Description +## {@html style_formatted_text(webcam_options_obj.description)} + + +#### Initialization + + + +### is_video_correct_length + +Validates that the audio length is within the specified min and max length (in seconds). +You can use this to construct a validator that will check if the user-provided audio is either too short or too long. + + +```python +import gradio as gr +demo = gr.Interface( + lambda x: x, + inputs="video", + outputs="video", + validator=lambda video: gr.validators.is_video_correct_length(video, min_length=1, max_length=5) +) +demo.launch() +``` + + +#### Initialization + + +
+ +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/01_eventdata.svx b/js/_website/src/lib/templates/gradio/04_helpers/01_eventdata.svx new file mode 100644 index 0000000..af917d2 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/01_eventdata.svx @@ -0,0 +1,73 @@ + + + + +# {obj.name} + + +```python +gradio.EventData(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +with gr.Blocks() as demo: + table = gr.Dataframe([[1, 2, 3], [4, 5, 6]]) + gallery = gr.Gallery([("cat.jpg", "Cat"), ("dog.jpg", "Dog")]) + textbox = gr.Textbox("Hello World!") + statement = gr.Textbox() + + def on_select(value, evt: gr.EventData): + return f"The {evt.target} component was selected, and its value was {value}." + + table.select(on_select, table, statement) + gallery.select(on_select, gallery, statement) + textbox.select(on_select, textbox, statement) + +demo.launch() +``` +{/if} + + +### Attributes + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/02_deletedfiledata.svx b/js/_website/src/lib/templates/gradio/04_helpers/02_deletedfiledata.svx new file mode 100644 index 0000000..2696a13 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/02_deletedfiledata.svx @@ -0,0 +1,76 @@ + + + + +# {obj.name} + + +```python +gradio.DeletedFileData(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +def test(delete_data: gr.DeletedFileData): + return delete_data.file.path + +with gr.Blocks() as demo: + files = gr.File(file_count="multiple") + deleted_file = gr.File() + files.delete(test, None, deleted_file) + +demo.launch() +``` +{/if} + + +### Attributes + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/03_keyupdata.svx b/js/_website/src/lib/templates/gradio/04_helpers/03_keyupdata.svx new file mode 100644 index 0000000..8084d0e --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/03_keyupdata.svx @@ -0,0 +1,85 @@ + + + + +# {obj.name} + + +```python +gradio.KeyUpData(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +def test(value, key_up_data: gr.KeyUpData): + return { + "component value": value, + "input value": key_up_data.input_value, + "key": key_up_data.key + } + +with gr.Blocks() as demo: + d = gr.Dropdown(["abc", "def"], allow_custom_value=True) + t = gr.JSON() + d.key_up(test, d, t) +demo.launch() +``` +{/if} + + +### Attributes + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/04_likedata.svx b/js/_website/src/lib/templates/gradio/04_helpers/04_likedata.svx new file mode 100644 index 0000000..25c6978 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/04_likedata.svx @@ -0,0 +1,93 @@ + + + + +# {obj.name} + + +```python +gradio.LikeData(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +def test(value, like_data: gr.LikeData): + return { + "chatbot_value": value, + "liked_message": like_data.value, + "liked_index": like_data.index, + "liked_or_disliked_as_bool": like_data.liked + } + +with gr.Blocks() as demo: + c = gr.Chatbot([("abc", "def")]) + t = gr.JSON() + c.like(test, c, t) + +demo.launch() +``` +{/if} + + +### Attributes + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/05_selectdata.svx b/js/_website/src/lib/templates/gradio/04_helpers/05_selectdata.svx new file mode 100644 index 0000000..98af165 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/05_selectdata.svx @@ -0,0 +1,105 @@ + + + + +# {obj.name} + + +```python +gradio.SelectData(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +with gr.Blocks() as demo: + table = gr.Dataframe([[1, 2, 3], [4, 5, 6]]) + gallery = gr.Gallery([("cat.jpg", "Cat"), ("dog.jpg", "Dog")]) + textbox = gr.Textbox("Hello World!") + statement = gr.Textbox() + + def on_select(evt: gr.SelectData): + return f"You selected {evt.value} at {evt.index} from {evt.target}" + + table.select(on_select, None, statement) + gallery.select(on_select, None, statement) + textbox.select(on_select, None, statement) + +demo.launch() +``` +{/if} + + +### Attributes + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/06_filedata.svx b/js/_website/src/lib/templates/gradio/04_helpers/06_filedata.svx new file mode 100644 index 0000000..20b471f --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/06_filedata.svx @@ -0,0 +1,109 @@ + + + +# {obj.name} + + +```python +gradio.FileData(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +### Example Usage +```python +from gradio_client import Client, FileData, handle_file + +def get_url_on_server(data: FileData): + print(data['url']) + +client = Client("gradio/gif_maker_main", download_files=False) +job = client.submit([handle_file("./cheetah.jpg")], api_name="/predict") +data = job.result() +video: FileData = data['video'] + +get_url_on_server(video) +``` + + +### Attributes + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/07_retrydata.svx b/js/_website/src/lib/templates/gradio/04_helpers/07_retrydata.svx new file mode 100644 index 0000000..d730cd6 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/07_retrydata.svx @@ -0,0 +1,84 @@ + + + + +# {obj.name} + + +```python +gradio.RetryData(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +def retry(retry_data: gr.RetryData, history: list[gr.MessageDict]): + history_up_to_retry = history[:retry_data.index] + new_response = "" + for token in api.chat_completion(history): + new_response += token + yield history + [new_response] + +with gr.Blocks() as demo: + chatbot = gr.Chatbot() + chatbot.retry(retry, chatbot, chatbot) +demo.launch() +``` +{/if} + + +### Attributes + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/08_undodata.svx b/js/_website/src/lib/templates/gradio/04_helpers/08_undodata.svx new file mode 100644 index 0000000..97f2612 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/08_undodata.svx @@ -0,0 +1,81 @@ + + + + +# {obj.name} + + +```python +gradio.UndoData(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +def undo(retry_data: gr.UndoData, history: list[gr.MessageDict]): + history_up_to_retry = history[:retry_data.index] + return history_up_to_retry + +with gr.Blocks() as demo: + chatbot = gr.Chatbot() + chatbot.undo(undo, chatbot, chatbot) +demo.launch() +``` +{/if} + + +### Attributes + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/09_editdata.svx b/js/_website/src/lib/templates/gradio/04_helpers/09_editdata.svx new file mode 100644 index 0000000..37550a3 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/09_editdata.svx @@ -0,0 +1,88 @@ + + + + +# {obj.name} + + +```python +gradio.EditData(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +def edit(edit_data: gr.EditData, history: list[gr.MessageDict]): + history_up_to_edit = history[:edit_data.index] + history_up_to_edit[-1] = edit_data.value + return history_up_to_edit + +with gr.Blocks() as demo: + chatbot = gr.Chatbot() + chatbot.undo(edit, chatbot, chatbot) +demo.launch() +``` +{/if} + + +### Attributes + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/10_downloaddata.svx b/js/_website/src/lib/templates/gradio/04_helpers/10_downloaddata.svx new file mode 100644 index 0000000..ccd4962 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/10_downloaddata.svx @@ -0,0 +1,73 @@ + + + + +# {obj.name} + + +```python +gradio.DownloadData(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr +def on_download(download_data: gr.DownloadData): + return f"Downloaded file: {download_data.file.path}" +with gr.Blocks() as demo: + files = gr.File() + textbox = gr.Textbox() + files.download(on_download, None, textbox) +demo.launch() +``` +{/if} + + +### Attributes + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/11_copydata.svx b/js/_website/src/lib/templates/gradio/04_helpers/11_copydata.svx new file mode 100644 index 0000000..ab3d9e9 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/11_copydata.svx @@ -0,0 +1,73 @@ + + + + +# {obj.name} + + +```python +gradio.CopyData(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr +def on_copy(copy_data: gr.CopyData): + return f"Copied text: {copy_data.value}" +with gr.Blocks() as demo: + textbox = gr.Textbox("Hello World!") + copied = gr.Textbox() + textbox.copy(on_copy, None, copied) +demo.launch() +``` +{/if} + + +### Attributes + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/12_examples.svx b/js/_website/src/lib/templates/gradio/04_helpers/12_examples.svx new file mode 100644 index 0000000..1fe413d --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/12_examples.svx @@ -0,0 +1,107 @@ + + + + +# {obj.name} + + +```python +gradio.Examples(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +None +``` +{/if} + + +### Initialization + + + +### Attributes + + + +### Examples + +**Updating Examples** + +In this demo, we show how to update the examples by updating the samples of the underlying dataset. Note that this only works if `cache_examples=False` as updating the underlying dataset does not update the cache. + +```py +import gradio as gr + +def update_examples(country): + if country == "USA": + return gr.Dataset(samples=[["Chicago"], ["Little Rock"], ["San Francisco"]]) + else: + return gr.Dataset(samples=[["Islamabad"], ["Karachi"], ["Lahore"]]) + +with gr.Blocks() as demo: + dropdown = gr.Dropdown(label="Country", choices=["USA", "Pakistan"], value="USA") + textbox = gr.Textbox() + examples = gr.Examples([["Chicago"], ["Little Rock"], ["San Francisco"]], textbox) + dropdown.change(update_examples, dropdown, examples.dataset) + +demo.launch() +``` + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/13_progress.svx b/js/_website/src/lib/templates/gradio/04_helpers/13_progress.svx new file mode 100644 index 0000000..465b0a0 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/13_progress.svx @@ -0,0 +1,65 @@ + + + + +# {obj.name} + + +```python +gradio.Progress(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr +import time +def my_function(x, progress=gr.Progress()): + progress(0, desc="Starting...") + time.sleep(1) + for i in progress.tqdm(range(100)): + time.sleep(0.1) + return x +gr.Interface(my_function, gr.Textbox(), gr.Textbox()).queue().launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/14_dependency.svx b/js/_website/src/lib/templates/gradio/04_helpers/14_dependency.svx new file mode 100644 index 0000000..ba71a3b --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/14_dependency.svx @@ -0,0 +1,59 @@ + + + + +# {obj.name} + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +with gr.Blocks() as demo: + first_textbox = gr.Textbox() + second_textbox = gr.Textbox() + button = gr.Button("Submit") + + dependency = button.click(lambda x: "Hello, " + x, first_textbox, second_textbox) + dependency.success(lambda: gr.Info("Greeting successful"), None, None) + dependency.failure(lambda: gr.Warning("Greeting failed"), None, None) + +demo.launch() +``` +{/if} + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/api.svx b/js/_website/src/lib/templates/gradio/04_helpers/api.svx new file mode 100644 index 0000000..53f5f83 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/api.svx @@ -0,0 +1,81 @@ + + + + +# {obj.name} + + +```python +gradio.api(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr +with gr.Blocks() as demo: + with gr.Row(): + input = gr.Textbox() + button = gr.Button("Submit") + output = gr.Textbox() + def fn(a: int, b: int, c: list[str]) -> tuple[int, str]: + return a + b, c[a:b] + gr.api(fn, api_name="add_and_slice") +_, url, _ = demo.launch() + +from gradio_client import Client +client = Client(url) +result = client.predict( + a=3, + b=5, + c=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + api_name="/add_and_slice" +) +print(result) +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/cache-class.svx b/js/_website/src/lib/templates/gradio/04_helpers/cache-class.svx new file mode 100644 index 0000000..7472a51 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/cache-class.svx @@ -0,0 +1,70 @@ + + + + +# {obj.name} + + +```python +gradio.Cache(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +def generate(prompt, c=gr.Cache(per_session=True)): + hit = c.get(prompt) + if hit is not None: + return hit["result"] + result = llm(prompt) + c.set(prompt, result=result) + return result +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/cache.svx b/js/_website/src/lib/templates/gradio/04_helpers/cache.svx new file mode 100644 index 0000000..cd4db3a --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/cache.svx @@ -0,0 +1,70 @@ + + + + +# {obj.name} + + +```python +@gradio.cache(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +@gr.cache +def classify(image): + return model.predict(image) + +@gr.cache(max_size=256, per_session=True) +def generate(prompt): + return llm(prompt) +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/load.svx b/js/_website/src/lib/templates/gradio/04_helpers/load.svx new file mode 100644 index 0000000..c10fbcd --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/load.svx @@ -0,0 +1,64 @@ + + + + +# {obj.name} + + +```python +gradio.load(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr +demo = gr.load("gradio/question-answering", src="spaces") +demo.launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/load_chat.svx b/js/_website/src/lib/templates/gradio/04_helpers/load_chat.svx new file mode 100644 index 0000000..521469a --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/load_chat.svx @@ -0,0 +1,64 @@ + + + + +# {obj.name} + + +```python +gradio.load_chat(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr +demo = gr.load_chat("http://localhost:11434/v1", model="deepseek-r1") +demo.launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/on.svx b/js/_website/src/lib/templates/gradio/04_helpers/on.svx new file mode 100644 index 0000000..a79b873 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/on.svx @@ -0,0 +1,76 @@ + + + + +# {obj.name} + + +```python +gradio.on(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +with gr.Blocks() as demo: + with gr.Row(): + input = gr.Textbox() + button = gr.Button("Submit") + output = gr.Textbox() + gr.on( + triggers=[button.click, input.submit], + fn=lambda x: x, + inputs=[input], + outputs=[output] + ) + +demo.launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/04_helpers/set_static_paths.svx b/js/_website/src/lib/templates/gradio/04_helpers/set_static_paths.svx new file mode 100644 index 0000000..6adbc7a --- /dev/null +++ b/js/_website/src/lib/templates/gradio/04_helpers/set_static_paths.svx @@ -0,0 +1,77 @@ + + + + +# {obj.name} + + +```python +gradio.set_static_paths(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr + +# Paths can be a list of strings or pathlib.Path objects +# corresponding to filenames or directories. +gr.set_static_paths(paths=["test/test_files/"]) + +# The example files and the default value of the input +# will not be copied to the gradio cache and will be served directly. +demo = gr.Interface( + lambda s: s.rotate(45), + gr.Image(value="test/test_files/cheetah1.jpg", type="pil"), + gr.Image(), + examples=["test/test_files/bus.png"], +) + +demo.launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/05_modals/error.svx b/js/_website/src/lib/templates/gradio/05_modals/error.svx new file mode 100644 index 0000000..a736612 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/05_modals/error.svx @@ -0,0 +1,67 @@ + + + + +# {obj.name} + + +```python +raise gradio.Error("An error occurred 💥!", duration=5) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + +## You can control for how long the error message is displayed with the `duration` parameter. If it's `None`, the message will be displayed forever until the user closes it. If it's a number, it will be shown for that many seconds. +## You can also hide the error modal from being shown in the UI by setting `visible=False`. +## Below is a demo of how different values of duration control the error, info, and warning messages. You can see the code [here](https://huggingface.co/spaces/freddyaboulton/gradio-error-duration/blob/244331cf53f6b5fa2fd406ece3bf55c6ccb9f5f2/app.py#L17). + +![modal_control](https://github.com/gradio-app/gradio/assets/41651716/f0977bcd-eaec-4eca-a2fd-ede95fdb8fd2) + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr +def divide(numerator, denominator): + if denominator == 0: + raise gr.Error("Cannot divide by zero!") +gr.Interface(divide, ["number", "number"], "number").launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/05_modals/info.svx b/js/_website/src/lib/templates/gradio/05_modals/info.svx new file mode 100644 index 0000000..9d41aea --- /dev/null +++ b/js/_website/src/lib/templates/gradio/05_modals/info.svx @@ -0,0 +1,69 @@ + + + + +# {obj.name} + + +```python +gradio.Info("Helpful info message ℹ️", duration=5) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr +def hello_world(): + gr.Info('This is some info.') + return "hello world" +with gr.Blocks() as demo: + md = gr.Markdown() + demo.load(hello_world, inputs=None, outputs=[md]) +demo.queue().launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/05_modals/warning.svx b/js/_website/src/lib/templates/gradio/05_modals/warning.svx new file mode 100644 index 0000000..ceaa6a4 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/05_modals/warning.svx @@ -0,0 +1,69 @@ + + + + +# {obj.name} + + +```python +gradio.Warning("A warning occured ⛔️!", duration=5) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr +def hello_world(): + gr.Warning('This is a warning message.') + return "hello world" +with gr.Blocks() as demo: + md = gr.Markdown() + demo.load(hello_world, inputs=None, outputs=[md]) +demo.queue().launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/06_routes/mount_gradio_app.svx b/js/_website/src/lib/templates/gradio/06_routes/mount_gradio_app.svx new file mode 100644 index 0000000..ec999c1 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/06_routes/mount_gradio_app.svx @@ -0,0 +1,70 @@ + + + + +# {obj.name} + + +```python +gradio.mount_gradio_app(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +from fastapi import FastAPI +import gradio as gr +app = FastAPI() +@app.get("/") +def read_main(): + return {"message": "This is your main app"} +io = gr.Interface(lambda x: "Hello, " + x + "!", "textbox", "textbox") +app = gr.mount_gradio_app(app, io, path="/gradio") +``` +##### Then run `uvicorn run:app` from the terminal and navigate to http://localhost:8000/gradio. +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/06_routes/request.svx b/js/_website/src/lib/templates/gradio/06_routes/request.svx new file mode 100644 index 0000000..c616b5b --- /dev/null +++ b/js/_website/src/lib/templates/gradio/06_routes/request.svx @@ -0,0 +1,65 @@ + + + + +# {obj.name} + + +```python +gradio.Request(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +{#if obj.example} +### Example Usage +```python +import gradio as gr +def echo(text, request: gr.Request): + if request: + print("Request headers dictionary:", request.headers) + print("IP address:", request.client.host) + print("Query parameters:", dict(request.query_params)) + print("Session hash:", request.session_hash) + return text +io = gr.Interface(echo, "textbox", "textbox").launch() +``` +{/if} + + +### Initialization + + + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Methods + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/gradio/other/01_flagging.svx b/js/_website/src/lib/templates/gradio/other/01_flagging.svx new file mode 100644 index 0000000..bd5241a --- /dev/null +++ b/js/_website/src/lib/templates/gradio/other/01_flagging.svx @@ -0,0 +1,129 @@ + + + + +# Flagging + + +### Description +## A Gradio Interface includes a 'Flag' button that appears underneath the output. By default, clicking on the Flag button sends the input and output data back to the machine where the gradio demo is running, and saves it to a CSV log file. But this default behavior can be changed. To set what happens when the Flag button is clicked, you pass an instance of a subclass of _FlaggingCallback_ to the _flagging_callback_ parameter in the _Interface_ constructor. You can use one of the _FlaggingCallback_ subclasses that are listed below, or you can create your own, which lets you do whatever you want with the data that is being flagged. + + + + + +# {simple_csv_logger_obj.name} + + + +```python +gradio.SimpleCSVLogger(···) +``` + + +### Description +## {@html style_formatted_text(simple_csv_logger_obj.description)} + + + +{#if simple_csv_logger_obj.example} +### Example Usage +```python +import gradio as gr +def image_classifier(inp): + return {'cat': 0.3, 'dog': 0.7} +demo = gr.Interface(fn=image_classifier, inputs="image", outputs="label", + flagging_callback=SimpleCSVLogger()) +``` +{/if} + +{#if (simple_csv_logger_obj.parameters.length > 0 && simple_csv_logger_obj.parameters[0].name != "self") || simple_csv_logger_obj.parameters.length > 1} + +### Initialization + +{/if} + +{#if simple_csv_logger_obj.demos && simple_csv_logger_obj.demos.length > 0} + +### Demos + +{/if} + +{#if simple_csv_logger_obj.fns && simple_csv_logger_obj.fns.length > 0} + +### Methods + +{/if} + +{#if simple_csv_logger_obj.guides && simple_csv_logger_obj.guides.length > 0} + + + +{/if} + + + + + + +# {csv_logger_obj.name} + + + +```python +gradio.CSVLogger(···) +``` + + +### Description +## {@html style_formatted_text(csv_logger_obj.description)} + + + +{#if csv_logger_obj.example} +### Example Usage +```python +import gradio as gr +def image_classifier(inp): + return {'cat': 0.3, 'dog': 0.7} +demo = gr.Interface(fn=image_classifier, inputs="image", outputs="label", + flagging_callback=CSVLogger()) +``` +{/if} + +{#if (csv_logger_obj.parameters.length > 0 && csv_logger_obj.parameters[0].name != "self") || csv_logger_obj.parameters.length > 1} + +### Initialization + +{/if} + +{#if csv_logger_obj.demos && csv_logger_obj.demos.length > 0} + +### Demos + +{/if} + +{#if csv_logger_obj.fns && csv_logger_obj.fns.length > 0} + +### Methods + +{/if} + +{#if csv_logger_obj.guides && csv_logger_obj.guides.length > 0} + + + +{/if} + diff --git a/js/_website/src/lib/templates/gradio/other/02_themes.svx b/js/_website/src/lib/templates/gradio/other/02_themes.svx new file mode 100644 index 0000000..6678cc3 --- /dev/null +++ b/js/_website/src/lib/templates/gradio/other/02_themes.svx @@ -0,0 +1,435 @@ + + +# Theming + +### Introduction + +## Gradio features a built-in theming engine that lets you customize the look and feel of your app. You can choose from a variety of themes, or create your own. To do so, pass the `theme=` kwarg to the `Blocks` or `Interface` constructor. For example: + +```python +with gr.Blocks(theme=gr.themes.Soft()) as demo: + ... +``` + +
+ +
+ +## Gradio comes with a set of prebuilt themes which you can load from `gr.themes.*`. These are: + +- -- `gr.themes.Base()` +- -- `gr.themes.Default()` +- -- `gr.themes.Glass()` +- -- `gr.themes.Monochrome()` +- -- `gr.themes.Soft()` + +## Each of these themes set values for hundreds of CSS variables. You can use prebuilt themes as a starting point for your own custom themes, or you can create your own themes from scratch. Let's take a look at each approach. + +### Using the Theme Builder + +## The easiest way to build a theme is using the Theme Builder. To launch the Theme Builder locally, run the following code: + +```python +import gradio as gr + +gr.themes.builder() +``` + +
+ +
+ +## You can use the Theme Builder running on Spaces above, though it runs much faster when you launch it locally via `gr.themes.builder()`. + +## As you edit the values in the Theme Builder, the app will preview updates in real time. You can download the code to generate the theme you've created so you can use it in any Gradio app. + +## In the rest of the guide, we will cover building themes programmatically. + +### Extending Themes via the Constructor + +## Although each theme has hundreds of CSS variables, the values for most these variables are drawn from 8 core variables which can be set through the constructor of each prebuilt theme. Modifying these 8 arguments allows you to quickly change the look and feel of your app. + +### Core Colors + +## The first 3 constructor arguments set the colors of the theme and are `gradio.themes.Color` objects. Internally, these Color objects hold brightness values for the palette of a single hue, ranging from 50, 100, 200..., 800, 900, 950. Other CSS variables are derived from these 3 colors. + +## The 3 color constructor arguments are: + +- -- `primary_hue`: This is the color draws attention in your theme. In the default theme, this is set to `gradio.themes.colors.orange`. +- -- `secondary_hue`: This is the color that is used for secondary elements in your theme. In the default theme, this is set to `gradio.themes.colors.blue`. +- -- `neutral_hue`: This is the color that is used for text and other neutral elements in your theme. In the default theme, this is set to `gradio.themes.colors.gray`. + +## You could modify these values using their string shortcuts, such as + +```python +with gr.Blocks(theme=gr.themes.Default(primary_hue="red", secondary_hue="pink")) as demo: + ... +``` + +## or you could use the `Color` objects directly, like this: + +```python +with gr.Blocks(theme=gr.themes.Default(primary_hue=gr.themes.colors.red, secondary_hue=gr.themes.colors.pink)) as demo: + ... +``` + +
+ +
+ +## Predefined colors are: + +- -- `slate` +- -- `gray` +- -- `zinc` +- -- `neutral` +- -- `stone` +- -- `red` +- -- `orange` +- -- `amber` +- -- `yellow` +- -- `lime` +- -- `green` +- -- `emerald` +- -- `teal` +- -- `cyan` +- -- `sky` +- -- `blue` +- -- `indigo` +- -- `violet` +- -- `purple` +- -- `fuchsia` +- -- `pink` +- -- `rose` + +You could also create your own custom `Color` objects and pass them in. + +### Core Sizing + +The next 3 constructor arguments set the sizing of the theme and are `gradio.themes.Size` objects. Internally, these Size objects hold pixel size values that range from `xxs` to `xxl`. Other CSS variables are derived from these 3 sizes. + +- -- `spacing_size`: This sets the padding within and spacing between elements. In the default theme, this is set to `gradio.themes.sizes.spacing_md`. +- -- `radius_size`: This sets the roundedness of corners of elements. In the default theme, this is set to `gradio.themes.sizes.radius_md`. +- -- `text_size`: This sets the font size of text. In the default theme, this is set to `gradio.themes.sizes.text_md`. + +## You could modify these values using their string shortcuts, such as + +```python +with gr.Blocks(theme=gr.themes.Default(spacing_size="sm", radius_size="none")) as demo: + ... +``` + +## or you could use the `Size` objects directly, like this: + +```python +with gr.Blocks(theme=gr.themes.Default(spacing_size=gr.themes.sizes.spacing_sm, radius_size=gr.themes.sizes.radius_none)) as demo: + ... +``` + +
+ +
+ +## The predefined size objects are: + +- -- `radius_none` +- -- `radius_sm` +- -- `radius_md` +- -- `radius_lg` +- -- `spacing_sm` +- -- `spacing_md` +- -- `spacing_lg` +- -- `text_sm` +- -- `text_md` +- -- `text_lg` + +## You could also create your own custom `Size` objects and pass them in. + +### Core Fonts + +## The final 2 constructor arguments set the fonts of the theme. You can pass a list of fonts to each of these arguments to specify fallbacks. If you provide a string, it will be loaded as a system font. If you provide a `gradio.themes.GoogleFont`, the font will be loaded from Google Fonts. + +- -- `font`: This sets the primary font of the theme. In the default theme, this is set to `gradio.themes.GoogleFont("Source Sans Pro")`. +- -- `font_mono`: This sets the monospace font of the theme. In the default theme, this is set to `gradio.themes.GoogleFont("IBM Plex Mono")`. + +## You could modify these values such as the following: + +```python +with gr.Blocks(theme=gr.themes.Default(font=[gr.themes.GoogleFont("Inconsolata"), "Arial", "sans-serif"])) as demo: + ... +``` + +
+ +
+ +### Extending Themes via `.set()` + +## You can also modify the values of CSS variables after the theme has been loaded. To do so, use the `.set()` method of the theme object to get access to the CSS variables. For example: + +```python +theme = gr.themes.Default(primary_hue="blue").set( + loader_color="#FF0000", + slider_color="#FF0000", +) + +with gr.Blocks(theme=theme) as demo: + ... +``` + +## In the example above, we've set the `loader_color` and `slider_color` variables to `#FF0000`, despite the overall `primary_color` using the blue color palette. You can set any CSS variable that is defined in the theme in this manner. + +## Your IDE type hinting should help you navigate these variables. Since there are so many CSS variables, let's take a look at how these variables are named and organized. + +### CSS Variable Naming Conventions + +## CSS variable names can get quite long, like `button_primary_background_fill_hover_dark`! However they follow a common naming convention that makes it easy to understand what they do and to find the variable you're looking for. Separated by underscores, the variable name is made up of: + +- -- 1. The target element, such as `button`, `slider`, or `block`. +- -- 2. The target element type or sub-element, such as `button_primary`, or `block_label`. +- -- 3. The property, such as `button_primary_background_fill`, or `block_label_border_width`. +- -- 4. Any relevant state, such as `button_primary_background_fill_hover`. +- -- 5. If the value is different in dark mode, the suffix `_dark`. For example, `input_border_color_focus_dark`. + +## Of course, many CSS variable names are shorter than this, such as `table_border_color`, or `input_shadow`. + +### CSS Variable Organization + +## Though there are hundreds of CSS variables, they do not all have to have individual values. They draw their values by referencing a set of core variables and referencing each other. This allows us to only have to modify a few variables to change the look and feel of the entire theme, while also getting finer control of individual elements that we may want to modify. + +#### Referencing Core Variables + +## To reference one of the core constructor variables, precede the variable name with an asterisk. To reference a core color, use the `*primary_`, `*secondary_`, or `*neutral_` prefix, followed by the brightness value. For example: + +```python +theme = gr.themes.Default(primary_hue="blue").set( + button_primary_background_fill="*primary_200", + button_primary_background_fill_hover="*primary_300", +) +``` + +## In the example above, we've set the `button_primary_background_fill` and `button_primary_background_fill_hover` variables to `*primary_200` and `*primary_300`. These variables will be set to the 200 and 300 brightness values of the blue primary color palette, respectively. + +## Similarly, to reference a core size, use the `*spacing_`, `*radius_`, or `*text_` prefix, followed by the size value. For example: + +```python +theme = gr.themes.Default(radius_size="md").set( + button_primary_border_radius="*radius_xl", +) +``` + +## In the example above, we've set the `button_primary_border_radius` variable to `*radius_xl`. This variable will be set to the `xl` setting of the medium radius size range. + +### Referencing Other Variables + +## Variables can also reference each other. For example, look at the example below: + +```python +theme = gr.themes.Default().set( + button_primary_background_fill="#FF0000", + button_primary_background_fill_hover="#FF0000", + button_primary_border="#FF0000", +) +``` + +## Having to set these values to a common color is a bit tedious. Instead, we can reference the `button_primary_background_fill` variable in the `button_primary_background_fill_hover` and `button_primary_border` variables, using a `*` prefix. + +```python +theme = gr.themes.Default().set( + button_primary_background_fill="#FF0000", + button_primary_background_fill_hover="*button_primary_background_fill", + button_primary_border="*button_primary_background_fill", +) +``` + +## Now, if we change the `button_primary_background_fill` variable, the `button_primary_background_fill_hover` and `button_primary_border` variables will automatically update as well. + +## This is particularly useful if you intend to share your theme - it makes it easy to modify the theme without having to change every variable. + +## Note that dark mode variables automatically reference each other. For example: + +```python +theme = gr.themes.Default().set( + button_primary_background_fill="#FF0000", + button_primary_background_fill_dark="#AAAAAA", + button_primary_border="*button_primary_background_fill", + button_primary_border_dark="*button_primary_background_fill_dark", +) +``` + +## `button_primary_border_dark` will draw its value from `button_primary_background_fill_dark`, because dark mode always draw from the dark version of the variable. + +### Creating a Full Theme + +## Let's say you want to create a theme from scratch! We'll go through it step by step - you can also see the source of prebuilt themes in the gradio source repo for reference - [here's the source](https://github.com/gradio-app/gradio/blob/main/gradio/themes/monochrome.py) for the Monochrome theme. + +## Our new theme class will inherit from `gradio.themes.Base`, a theme that sets a lot of convenient defaults. Let's make a simple demo that creates a dummy theme called Seafoam, and make a simple app that uses it. + +$code_theme_new_step_1 + +
+ +
+ + +## The Base theme is very barebones, and uses `gr.themes.Blue` as it primary color - you'll note the primary button and the loading animation are both blue as a result. Let's change the defaults core arguments of our app. We'll overwrite the constructor and pass new defaults for the core constructor arguments. + +## We'll use `gr.themes.Emerald` as our primary color, and set secondary and neutral hues to `gr.themes.Blue`. We'll make our text larger using `text_lg`. We'll use `Quicksand` as our default font, loaded from Google Fonts. + +$code_theme_new_step_2 + +
+ +
+ +See how the primary button and the loading animation are now green? These CSS variables are tied to the `primary_hue` variable. + +Let's modify the theme a bit more directly. We'll call the `set()` method to overwrite CSS variable values explicitly. We can use any CSS logic, and reference our core constructor arguments using the `*` prefix. + +$code_theme_new_step_3 + +
+ +
+ +Look how fun our theme looks now! With just a few variable changes, our theme looks completely different. + +You may find it helpful to explore the [source code of the other prebuilt themes](https://github.com/gradio-app/gradio/blob/main/gradio/themes) to see how they modified the base theme. You can also find your browser's Inspector useful to select elements from the UI and see what CSS variables are being used in the styles panel. + +## Sharing Themes + +Once you have created a theme, you can upload it to the HuggingFace Hub to let others view it, use it, and build off of it! + +### Uploading a Theme + +There are two ways to upload a theme, via the theme class instance or the command line. We will cover both of them with the previously created `seafoam` theme. + +- Via the class instance + +Each theme instance has a method called `push_to_hub` we can use to upload a theme to the HuggingFace hub. + +```python +seafoam.push_to_hub(repo_name="seafoam", + version="0.0.1", + hf_token="") +``` + +- Via the command line + +First save the theme to disk + +```python +seafoam.dump(filename="seafoam.json") +``` + +Then use the `upload_theme` command: + +```bash +upload_theme\ +"seafoam.json"\ +"seafoam"\ +--version "0.0.1"\ +--hf_token "" +``` + +In order to upload a theme, you must have a HuggingFace account and pass your [Access Token](https://huggingface.co/docs/huggingface_hub/quick-start#login) +as the `hf_token` argument. However, if you log in via the [HuggingFace command line](https://huggingface.co/docs/huggingface_hub/quick-start#login) (which comes installed with `gradio`), +you can omit the `hf_token` argument. + +The `version` argument lets you specify a valid [semantic version](https://www.geeksforgeeks.org/introduction-semantic-versioning/) string for your theme. +That way your users are able to specify which version of your theme they want to use in their apps. This also lets you publish updates to your theme without worrying +about changing how previously created apps look. The `version` argument is optional. If omitted, the next patch version is automatically applied. + +### Theme Previews + +By calling `push_to_hub` or `upload_theme`, the theme assets will be stored in a [HuggingFace space](https://huggingface.co/docs/hub/spaces-overview). + +The theme preview for our seafoam theme is here: [seafoam preview](https://huggingface.co/spaces/gradio/seafoam). + +
+ +
+ +### Discovering Themes + +The [Theme Gallery](https://huggingface.co/spaces/gradio/theme-gallery) shows all the public gradio themes. After publishing your theme, +it will automatically show up in the theme gallery after a couple of minutes. + +You can sort the themes by the number of likes on the space and from most to least recently created as well as toggling themes between light and dark mode. + +
+ +
+ +### Downloading + +To use a theme from the hub, use the `from_hub` method on the `ThemeClass` and pass it to your app: + +```python +my_theme = gr.Theme.from_hub("gradio/seafoam") + +with gr.Blocks(theme=my_theme) as demo: + .... +``` + +You can also pass the theme string directly to `Blocks` or `Interface` (`gr.Blocks(theme="gradio/seafoam")`) + +You can pin your app to an upstream theme version by using semantic versioning expressions. + +For example, the following would ensure the theme we load from the `seafoam` repo was between versions `0.0.1` and `0.1.0`: + +```python +with gr.Blocks(theme="gradio/seafoam@>=0.0.1,<0.1.0") as demo: + .... +``` + +Enjoy creating your own themes! If you make one you're proud of, please share it with the world by uploading it to the hub! +If you tag us on [Twitter](https://twitter.com/gradio) we can give your theme a shout out! + + diff --git a/js/_website/src/lib/templates/gradio/other/NO_RELOAD.svx b/js/_website/src/lib/templates/gradio/other/NO_RELOAD.svx new file mode 100644 index 0000000..c1de13b --- /dev/null +++ b/js/_website/src/lib/templates/gradio/other/NO_RELOAD.svx @@ -0,0 +1,29 @@ + + + + +# NO_RELOAD + + +```python +if gr.NO_RELOAD: +``` + + +### Description +## Any code in a `if gr.NO_RELOAD` code-block will not be re-evaluated when the source file is reloaded. This is helpful for importing modules that do not like to be reloaded (tiktoken, numpy) as well as database connections and long running set up code. + + + +### Example Usage +```python +import gradio as gr + +if gr.NO_RELOAD: + from transformers import pipeline + pipe = pipeline("text-classification", model="cardiffnlp/twitter-roberta-base-sentiment-latest") + +gr.Interface.from_pipeline(pipe).launch() +``` diff --git a/js/_website/src/lib/templates/process_json.ts b/js/_website/src/lib/templates/process_json.ts new file mode 100644 index 0000000..ba69b5f --- /dev/null +++ b/js/_website/src/lib/templates/process_json.ts @@ -0,0 +1,27 @@ +import docs_json from "./docs.json"; + +let docs: { [key: string]: any } = docs_json.docs; + +export function get_object(name: string) { + let obj: any; + if (name === "events" || name === "events_matrix") { + obj = docs["gradio"][name]; + return obj; + } + for (const library in docs) { + for (const key in docs[library]) { + if (key === name && key !== "chatinterface") { + obj = docs[library][key]; + break; + } else { + for (const o in docs[library][key]) { + if (o === name) { + obj = docs[library][key][o]; + break; + } + } + } + } + } + return obj; +} diff --git a/js/_website/src/lib/templates/python-client/gradio_client/01_introduction.svx b/js/_website/src/lib/templates/python-client/gradio_client/01_introduction.svx new file mode 100644 index 0000000..232e15a --- /dev/null +++ b/js/_website/src/lib/templates/python-client/gradio_client/01_introduction.svx @@ -0,0 +1,350 @@ + + +# Introduction + +## The lightweight Gradio client libraries make it easy to use any Gradio app as an API. We currently support both a Python client library as well as a JavaScript client library. + +## The Python client library is gradio_client. It's included in the latest versions of the gradio package, but for a more lightweight experience, you can install it using pip without having to install gradio: + +```bash +pip install gradio_client +``` + +# Getting Started with the Gradio Python client + + +## The Gradio Python client makes it very easy to use any Gradio app as an API. As an example, consider this [Hugging Face Space that transcribes audio files](https://huggingface.co/spaces/abidlabs/whisper) that are recorded from the microphone. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/whisper-screenshot.jpg) + +## Using the `gradio_client` library, we can easily use the Gradio as an API to transcribe audio files programmatically. + +## Here's the entire code to do it: + +```python +from gradio_client import Client, file + +client = Client("abidlabs/whisper") + +client.predict( + audio=file("audio_sample.wav") +) + +>> "This is a test of the whisper speech recognition model." +``` + +The Gradio client works with any hosted Gradio app! Although the Client is mostly used with apps hosted on [Hugging Face Spaces](https://hf.space), your app can be hosted anywhere, such as your own server. + +**Prerequisites**: To use the Gradio client, you do _not_ need to know the `gradio` library in great detail. However, it is helpful to have general familiarity with Gradio's concepts of input and output components. + +### Installation + +If you already have a recent version of `gradio`, then the `gradio_client` is included as a dependency. But note that this documentation reflects the latest version of the `gradio_client`, so upgrade if you're not sure! + +The lightweight `gradio_client` package can be installed from pip (or pip3) and is tested to work with **Python versions 3.9 or higher**: + +```bash +$ pip install --upgrade gradio_client +``` + +### Connecting to a Gradio App on Hugging Face Spaces + +Start by connecting instantiating a `Client` object and connecting it to a Gradio app that is running on Hugging Face Spaces. + +```python +from gradio_client import Client + +client = Client("abidlabs/en2fr") # a Space that translates from English to French +``` + +You can also connect to private Spaces by passing in your HF token with the `hf_token` parameter. You can get your HF token here: https://huggingface.co/settings/tokens + +```python +from gradio_client import Client + +client = Client("abidlabs/my-private-space", hf_token="...") +``` + + +### Duplicating a Space for private use + +While you can use any public Space as an API, you may get rate limited by Hugging Face if you make too many requests. For unlimited usage of a Space, simply duplicate the Space to create a private Space, +and then use it to make as many requests as you'd like! + +The `gradio_client` includes a class method: `Client.duplicate()` to make this process simple (you'll need to pass in your [Hugging Face token](https://huggingface.co/settings/tokens) or be logged in using the Hugging Face CLI): + +```python +import os +from gradio_client import Client, file + +HF_TOKEN = os.environ.get("HF_TOKEN") + +client = Client.duplicate("abidlabs/whisper", hf_token=HF_TOKEN) +client.predict(file("audio_sample.wav")) + +>> "This is a test of the whisper speech recognition model." +``` + +If you have previously duplicated a Space, re-running `duplicate()` will _not_ create a new Space. Instead, the Client will attach to the previously-created Space. So it is safe to re-run the `Client.duplicate()` method multiple times. + +**Note:** if the original Space uses GPUs, your private Space will as well, and your Hugging Face account will get billed based on the price of the GPU. To minimize charges, your Space will automatically go to sleep after 1 hour of inactivity. You can also set the hardware using the `hardware` parameter of `duplicate()`. + +### Connecting a general Gradio app + +If your app is running somewhere else, just provide the full URL instead, including the "http://" or "https://". Here's an example of making predictions to a Gradio app that is running on a share URL: + +```python +from gradio_client import Client + +client = Client("https://bec81a83-5b5c-471e.gradio.live") +``` + +### Inspecting the API endpoints + +Once you have connected to a Gradio app, you can view the APIs that are available to you by calling the `Client.view_api()` method. For the Whisper Space, we see the following: + +```bash +Client.predict() Usage Info +--------------------------- +Named API endpoints: 1 + + - predict(audio, api_name="/predict") -> output + Parameters: + - [Audio] audio: filepath (required) + Returns: + - [Textbox] output: str +``` + +We see that we have 1 API endpoint in this space, and shows us how to use the API endpoint to make a prediction: we should call the `.predict()` method (which we will explore below), providing a parameter `input_audio` of type `str`, which is a `filepath or URL`. + +We should also provide the `api_name='/predict'` argument to the `predict()` method. Although this isn't necessary if a Gradio app has only 1 named endpoint, it does allow us to call different endpoints in a single app if they are available. + +### The "View API" Page + +As an alternative to running the `.view_api()` method, you can click on the "Use via API" link in the footer of the Gradio app, which shows us the same information, along with example usage. + +![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/gradio-guides/view-api.png) + +The View API page also includes an "API Recorder" that lets you interact with the Gradio UI normally and converts your interactions into the corresponding code to run with the Python Client. + +### Making a prediction + +The simplest way to make a prediction is simply to call the `.predict()` function with the appropriate arguments: + +```python +from gradio_client import Client + +client = Client("abidlabs/en2fr", api_name='/predict') +client.predict("Hello") + +>> Bonjour +``` + +If there are multiple parameters, then you should pass them as separate arguments to `.predict()`, like this: + +```python +from gradio_client import Client + +client = Client("gradio/calculator") +client.predict(4, "add", 5) + +>> 9.0 +``` + +It is recommended to provide key-word arguments instead of positional arguments: + + +```python +from gradio_client import Client + +client = Client("gradio/calculator") +client.predict(num1=4, operation="add", num2=5) + +>> 9.0 +``` + +This allows you to take advantage of default arguments. For example, this Space includes the default value for the Slider component so you do not need to provide it when accessing it with the client. + +```python +from gradio_client import Client + +client = Client("abidlabs/image_generator") +client.predict(text="an astronaut riding a camel") +``` + +The default value is the initial value of the corresponding Gradio component. If the component does not have an initial value, but if the corresponding argument in the predict function has a default value of `None`, then that parameter is also optional in the client. Of course, if you'd like to override it, you can include it as well: + +```python +from gradio_client import Client + +client = Client("abidlabs/image_generator") +client.predict(text="an astronaut riding a camel", steps=25) +``` + +For providing files or URLs as inputs, you should pass in the filepath or URL to the file enclosed within `gradio_client.file()`. This takes care of uploading the file to the Gradio server and ensures that the file is preprocessed correctly: + +```python +from gradio_client import Client, file + +client = Client("abidlabs/whisper") +client.predict( + audio=file("https://audio-samples.github.io/samples/mp3/blizzard_unconditional/sample-0.mp3") +) + +>> "My thought I have nobody by a beauty and will as you poured. Mr. Rochester is serve in that so don't find simpus, and devoted abode, to at might in a r—" +``` + +### Running jobs asynchronously + +Oe should note that `.predict()` is a _blocking_ operation as it waits for the operation to complete before returning the prediction. + +In many cases, you may be better off letting the job run in the background until you need the results of the prediction. You can do this by creating a `Job` instance using the `.submit()` method, and then later calling `.result()` on the job to get the result. For example: + +```python +from gradio_client import Client + +client = Client(space="abidlabs/en2fr") +job = client.submit("Hello", api_name="/predict") # This is not blocking + +# Do something else + +job.result() # This is blocking + +>> Bonjour +``` + +### Adding callbacks + +Alternatively, one can add one or more callbacks to perform actions after the job has completed running, like this: + +```python +from gradio_client import Client + +def print_result(x): + print("The translated result is: {x}") + +client = Client(space="abidlabs/en2fr") + +job = client.submit("Hello", api_name="/predict", result_callbacks=[print_result]) + +# Do something else + +>> The translated result is: Bonjour + +``` + +### Status + +The `Job` object also allows you to get the status of the running job by calling the `.status()` method. This returns a `StatusUpdate` object with the following attributes: `code` (the status code, one of a set of defined strings representing the status. See the `utils.Status` class), `rank` (the current position of this job in the queue), `queue_size` (the total queue size), `eta` (estimated time this job will complete), `success` (a boolean representing whether the job completed successfully), and `time` (the time that the status was generated). + +```py +from gradio_client import Client + +client = Client(src="gradio/calculator") +job = client.submit(5, "add", 4, api_name="/predict") +job.status() + +>> +``` + +_Note_: The `Job` class also has a `.done()` instance method which returns a boolean indicating whether the job has completed. + +### Cancelling Jobs + +The `Job` class also has a `.cancel()` instance method that cancels jobs that have been queued but not started. For example, if you run: + +```py +client = Client("abidlabs/whisper") +job1 = client.submit(file("audio_sample1.wav")) +job2 = client.submit(file("audio_sample2.wav")) +job1.cancel() # will return False, assuming the job has started +job2.cancel() # will return True, indicating that the job has been canceled +``` + +If the first job has started processing, then it will not be canceled. If the second job +has not yet started, it will be successfully canceled and removed from the queue. + +### Generator Endpoints + +Some Gradio API endpoints do not return a single value, rather they return a series of values. You can get the series of values that have been returned at any time from such a generator endpoint by running `job.outputs()`: + +```py +from gradio_client import Client + +client = Client(src="gradio/count_generator") +job = client.submit(3, api_name="/count") +while not job.done(): + time.sleep(0.1) +job.outputs() + +>> ['0', '1', '2'] +``` + +Note that running `job.result()` on a generator endpoint only gives you the _first_ value returned by the endpoint. + +The `Job` object is also iterable, which means you can use it to display the results of a generator function as they are returned from the endpoint. Here's the equivalent example using the `Job` as a generator: + +```py +from gradio_client import Client + +client = Client(src="gradio/count_generator") +job = client.submit(3, api_name="/count") + +for o in job: + print(o) + +>> 0 +>> 1 +>> 2 +``` + +You can also cancel jobs that that have iterative outputs, in which case the job will finish as soon as the current iteration finishes running. + +```py +from gradio_client import Client +import time + +client = Client("abidlabs/test-yield") +job = client.submit("abcdef") +time.sleep(3) +job.cancel() # job cancels after 2 iterations +``` + +### Demos with Session State + +Gradio demos can include [session state](https://www.gradio.app/guides/state-in-blocks), which provides a way for demos to persist information from user interactions within a page session. + +For example, consider the following demo, which maintains a list of words that a user has submitted in a `gr.State` component. When a user submits a new word, it is added to the state, and the number of previous occurrences of that word is displayed: + +```python +import gradio as gr + +def count(word, list_of_words): + return list_of_words.count(word), list_of_words + [word] + +with gr.Blocks() as demo: + words = gr.State([]) + textbox = gr.Textbox() + number = gr.Number() + textbox.submit(count, inputs=[textbox, words], outputs=[number, words]) + +demo.launch() +``` + +If you were to connect this this Gradio app using the Python Client, you would notice that the API information only shows a single input and output: + +```csv +Client.predict() Usage Info +--------------------------- +Named API endpoints: 1 + + - predict(word, api_name="/count") -> value_31 + Parameters: + - [Textbox] word: str (required) + Returns: + - [Number] value_31: float +``` + +That is because the Python client handles state automatically for you -- as you make a series of requests, the returned state from one request is stored internally and automatically supplied for the subsequent request. If you'd like to reset the state, you can do that by calling `Client.reset_session()`. diff --git a/js/_website/src/lib/templates/python-client/gradio_client/02_version-1-release.svx b/js/_website/src/lib/templates/python-client/gradio_client/02_version-1-release.svx new file mode 100644 index 0000000..198c52b --- /dev/null +++ b/js/_website/src/lib/templates/python-client/gradio_client/02_version-1-release.svx @@ -0,0 +1,157 @@ + + +# Clients 1.0 Launch! + +We're excited to unveil the first major release of the Gradio clients. +We've made it even easier to turn any Gradio application into a production endpoint thanks to the clients' **ergonomic**, **transparent**, and **portable** design. + + +### Ergonomic API 💆 + +
+ +**Stream From a Gradio app in 5 lines** +
+ +Use the `submit` method to get a job you can iterate over. + +
+ +In python: +```python +from gradio_client import Client + +client = Client("gradio/llm_stream") + +for result in client.submit("What's the best UI framework in Python?"): + print(result) +``` +
+ +In typescript: +```ts +import { Client } from "@gradio/client"; + +const client = await Client.connect("gradio/llm_stream") +const job = client.submit("/predict", {"text": "What's the best UI framework in Python?"}) + +for await (const msg of job) console.log(msg.data) +``` + +
+ +**Use the same keyword arguments as the app** +
+In the examples below, the upstream app has a function with parameters called `message`, `system_prompt`, and `tokens`. +We can see that the client `predict` call uses the same arguments. + +In python: +```python +from gradio_client import Client + +client = Client("http://127.0.0.1:7860/") +result = client.predict( + message="Hello!!", + system_prompt="You are helpful AI.", + tokens=10, + api_name="/chat" +) +print(result) +``` + +In typescript: +```ts +import { Client } from "@gradio/client"; + +const client = await Client.connect("http://127.0.0.1:7860/"); +const result = await client.predict("/chat", { + message: "Hello!!", + system_prompt: "Hello!!", + tokens: 10, +}); + +console.log(result.data); +``` +
+ +**Better Error Messages** +
+If something goes wrong in the upstream app, the client will raise the same exception as the app provided that `show_error=True` in the original app's `launch()` function, or it's a `gr.Error` exception. + +### Transparent Design 🪟 + +Anything you can do in the UI, you can do with the client: +* 🔐Authentication +* 🛑 Job Cancelling +* ℹ️ Access Queue Position and API +* 📕 View the API information + +
+Here's an example showing how to display the queue position of a pending job: + +```python +from gradio_client import Client + +client = Client("gradio/diffusion_model") + +job = client.submit("A cute cat") +while not job.done(): + status = job.status() + print(f"Current in position {status.rank} out of {status.queue_size}") +``` + + +### Portable Design ⛺️ +
+The client can run from pretty much any python and javascript environment (node, deno, the browser, Service Workers). +
+Here's an example using the client from a Flask server using gevent: + +```python +from gevent import monkey +monkey.patch_all() + +from gradio_client import Client +from flask import Flask, send_file +import time + +app = Flask(__name__) + +imageclient = Client("gradio/diffusion_model") + +@app.route("/gen") +def gen(): + result = imageclient.predict( + "A cute cat", + api_name="/predict" + ) + return send_file(result) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) +``` + +### v1.0 Migration Guide and Breaking Changes + +
+ +**Python** +
    +
  • The `serialize` argument of the `Client` class was removed and has no effect.
  • +
  • The `upload_files` argument of the `Client` was removed.
  • +
  • All filepaths must be wrapped in the `handle_file` method. For example, `caption = client.predict(handle_file('./dog.jpg'))`.
  • +
  • The `output_dir` argument was removed. It is not specified in the `download_files` argument.
  • +
+
+ +**Javascript** +
+The client has been redesigned entirely. It was refactored from a function into a class. An instance can now be constructed by awaiting the `connect` method. + +```js +const app = await Client.connect("gradio/whisper") +``` +The app variable has the same methods as the python class (`submit`, `predict`, `view_api`, `duplicate`). + diff --git a/js/_website/src/lib/templates/python-client/gradio_client/03_using-zero-gpu-spaces.svx b/js/_website/src/lib/templates/python-client/gradio_client/03_using-zero-gpu-spaces.svx new file mode 100644 index 0000000..3de71a0 --- /dev/null +++ b/js/_website/src/lib/templates/python-client/gradio_client/03_using-zero-gpu-spaces.svx @@ -0,0 +1,58 @@ + + +# Using ZeroGPU Spaces with the Clients + +Hugging Face Spaces now offers a new hardware option called ZeroGPU. +ZeroGPU is a "serverless" cluster of spaces that let Gradio applications run on A100 GPUs for free. +These kinds of spaces are a great foundation to build new applications on top of with the python gradio client, but you need to take care to avoid ZeroGPU's rate limiting. + +### Explaining Rate Limits for ZeroGPU + +ZeroGPU spaces are rate-limited to ensure that a single user does not hog all of the available GPUs. +The limit is controlled by a special token that the Hugging Face Hub infrastructure adds to all incoming requests to Spaces. +This token is a request header called `X-IP-Token` and its value changes depending on the user who makes a request to the ZeroGPU space. +
+ +Let's say you want to create a space (Space A) that uses a ZeroGPU space (Space B) programmatically. +Normally, calling Space B from Space A with the Gradio Python client would quickly exhaust Space B's rate limit, as all the requests to the ZeroGPU space +would be missing the `X-IP-Token` request header and would therefore be treated as unauthenticated. + +In order to avoid this, we need to extract the `X-IP-Token` of the user using Space A before we call Space B programmatically. +Where possible, specifically in the case of functions that are passed into event listeners directly, Gradio automatically +extracts the `X-IP-Token` from the incoming request and passes it into the Gradio Client. But if the Client +is instantiated outside of such a function, then you may need to pass in the token manually. + +How to do this will be explained in the following section. + +### Avoiding Rate Limits by Manually Passing an IP Token + +In the following hypothetical example, when a user presses enter in the textbox, the `generate()` function +is called, which calls a second function, `text_to_image()`. Because the Gradio Client is being +instantiated indirectly, in `text_to_image()`, we will need to extract their token from the `X-IP-Token` header of the incoming request. +We will use this header when constructing the gradio client. + + +```python +import gradio as gr +from gradio_client import Client + +def text_to_image(prompt, request: gr.Request): + x_ip_token = request.headers['x-ip-token'] + client = Client("hysts/SDXL", headers={"x-ip-token": x_ip_token}) + img = client.predict(prompt, api_name="/predict") + return img + +def generate(prompt, request: gr.Request): + prompt = prompt[:300] + return text_to_image(prompt, request) + +with gr.Blocks() as demo: + image = gr.Image() + prompt = gr.Textbox(max_lines=1) + prompt.submit(generate, [prompt], [image]) + +demo.launch() +``` + diff --git a/js/_website/src/lib/templates/python-client/gradio_client/client.svx b/js/_website/src/lib/templates/python-client/gradio_client/client.svx new file mode 100644 index 0000000..aabb82a --- /dev/null +++ b/js/_website/src/lib/templates/python-client/gradio_client/client.svx @@ -0,0 +1,68 @@ + + + + +# {obj.name} + + +```python +gradio_client.Client(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + +### Example usage +```python +from gradio_client import Client + +client = Client("abidlabs/whisper-large-v2") # connecting to a Hugging Face Space +client.predict("test.mp4", api_name="/predict") +>> What a nice recording! # returns the result of the remote API call + +client = Client("https://bec81a83-5b5c-471e.gradio.live") # connecting to a temporary Gradio share URL +job = client.submit("hello", api_name="/predict") # runs the prediction in a background thread +job.result() +>> 49 # returns the result of the remote API call (blocking call) +``` + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/python-client/gradio_client/job.svx b/js/_website/src/lib/templates/python-client/gradio_client/job.svx new file mode 100644 index 0000000..185ff7c --- /dev/null +++ b/js/_website/src/lib/templates/python-client/gradio_client/job.svx @@ -0,0 +1,55 @@ + + + + +# {obj.name} + + +```python +gradio_client.Job(···) +``` + + +### Description +## {@html style_formatted_text(obj.description)} + + + +### Initialization + + + +{#if obj.string_shortcuts && obj.string_shortcuts.length > 0} + +### Shortcuts + +{/if} + +{#if obj.demos && obj.demos.length > 0} + +### Demos + +{/if} + +{#if obj.fns && obj.fns.length > 0} + +### Event Listeners + +{/if} + +{#if obj.guides && obj.guides.length > 0} + + + +{/if} diff --git a/js/_website/src/lib/templates/third-party-clients/third-party-clients/01_introduction.svx b/js/_website/src/lib/templates/third-party-clients/third-party-clients/01_introduction.svx new file mode 100644 index 0000000..73a0c96 --- /dev/null +++ b/js/_website/src/lib/templates/third-party-clients/third-party-clients/01_introduction.svx @@ -0,0 +1,18 @@ + + +# Introduction + +### Gradio Clients +## Gradio applications support programmatic requests from many environments: + +- The [Python Client](/docs/python-client): `gradio-client` allows you to make requests from Python environments. +- The [JavaScript Client](/docs/js-client): `@gradio/client` allows you to make requests in TypeScript from the browser or server-side. +- You can also query gradio apps [directly from cURL](/guides/querying-gradio-apps-with-curl). + +### Community Clients +## We also encourage the development and use of third party clients built by the community: + +- [Rust Client](/docs/third-party-clients/rust-client): `gradio-rs` built by [@JacobLinCool](https://github.com/JacobLinCool) allows you to make requests in Rust. +- [Powershell Client](https://github.com/rrg92/powershai): `powershai` built by [@rrg92](https://github.com/rrg92) allows you to make requests to Gradio apps directly from Powershell. See [here for documentation](https://github.com/rrg92/powershai/blob/main/docs/en-US/providers/HUGGING-FACE.md) \ No newline at end of file diff --git a/js/_website/src/lib/templates/third-party-clients/third-party-clients/rust-client.svx b/js/_website/src/lib/templates/third-party-clients/third-party-clients/rust-client.svx new file mode 100644 index 0000000..f9b96a2 --- /dev/null +++ b/js/_website/src/lib/templates/third-party-clients/third-party-clients/rust-client.svx @@ -0,0 +1,89 @@ + + +# gradio-rs + +### Introduction +## `gradio-rs` is a Gradio Client in Rust built by [@JacobLinCool](https://github.com/JacobLinCool). You can find the repo [here](https://github.com/JacobLinCool/gradio-rs), and more in depth API documentation [here](https://docs.rs/gradio/latest/gradio/). + +### Usage + +## Here is an example of using BS-RoFormer model to separate vocals and background music from an audio file. + +```rust +use gradio::{PredictionInput, Client, ClientOptions}; + +#[tokio::main] +async fn main() { + if std::env::args().len() < 2 { + println!("Please provide an audio file path as an argument"); + std::process::exit(1); + } + let args: Vec = std::env::args().collect(); + let file_path = &args[1]; + println!("File: {}", file_path); + + let client = Client::new("JacobLinCool/vocal-separation", ClientOptions::default()) + .await + .unwrap(); + + let output = client + .predict( + "/separate", + vec![ + PredictionInput::from_file(file_path), + PredictionInput::from_value("BS-RoFormer"), + ], + ) + .await + .unwrap(); + println!( + "Vocals: {}", + output[0].clone().as_file().unwrap().url.unwrap() + ); + println!( + "Background: {}", + output[1].clone().as_file().unwrap().url.unwrap() + ); +} +``` + +## You can find more examples [here](https://github.com/JacobLinCool/gradio-rs/tree/main/examples). + +### Command Line Interface + +```bash +cargo install gradio +gr --help +``` +## Take [stabilityai/stable-diffusion-3-medium](https://huggingface.co/spaces/stabilityai/stable-diffusion-3-medium) HF Space as an example: + +```bash +> gr list stabilityai/stable-diffusion-3-medium +API Spec for stabilityai/stable-diffusion-3-medium: + /infer + Parameters: + prompt ( str ) + negative_prompt ( str ) + seed ( float ) numeric value between 0 and 2147483647 + randomize_seed ( bool ) + width ( float ) numeric value between 256 and 1344 + height ( float ) numeric value between 256 and 1344 + guidance_scale ( float ) numeric value between 0.0 and 10.0 + num_inference_steps ( float ) numeric value between 1 and 50 + Returns: + Result ( filepath ) + Seed ( float ) numeric value between 0 and 2147483647 + +> gr run stabilityai/stable-diffusion-3-medium infer 'Rusty text "AI & CLI" on the snow.' '' 0 true 1024 1024 5 28 +Result: https://stabilityai-stable-diffusion-3-medium.hf.space/file=/tmp/gradio/5735ca7775e05f8d56d929d8f57b099a675c0a01/image.webp +Seed: 486085626 +``` + +## For file input, simply use the file path as the argument: + +```bash +gr run hf-audio/whisper-large-v3 predict 'test-audio.wav' 'transcribe' +output: " Did you know you can try the coolest model on your command line?" +``` \ No newline at end of file diff --git a/js/_website/src/lib/text.ts b/js/_website/src/lib/text.ts new file mode 100644 index 0000000..31723d0 --- /dev/null +++ b/js/_website/src/lib/text.ts @@ -0,0 +1,28 @@ +const regex_italic = /\*(.*?)\*/g; +const regex_code = /`(.*?)`/g; +const regex_curly_brackets = /\{(.*?)\}/g; +const regex_auto_links = /\b(https?:\/\/\S+)/g; +const regex_double_dash = /(^|\n)--\s+(.+)/g; +const regex_single_dash = /(^|\n)-\s+(.+)/g; +const regex_new_line = /\\n/g; + +export function style_formatted_text(formatted_text: string | null): string { + if (!formatted_text) return ""; + return formatted_text + .replace(regex_new_line, "\n") + .replace(regex_italic, "$1") + .replace( + regex_code, + "$1" + ) + .replace( + regex_curly_brackets, + "$1" + ) + .replace( + regex_auto_links, + "$1" + ) + .replace(regex_double_dash, "$1
    • $2
") + .replace(regex_single_dash, "$1
  • $2
"); +} diff --git a/js/_website/src/lib/utils.ts b/js/_website/src/lib/utils.ts new file mode 100644 index 0000000..f08445e --- /dev/null +++ b/js/_website/src/lib/utils.ts @@ -0,0 +1,67 @@ +import { writable } from "svelte/store"; + +const sizes = { + sm: "(min-width: 640px)", + md: "(min-width: 768px)", + lg: "(min-width: 1024px)", + xl: "(min-width: 1280px)", + "2xl": "(min-width: 1536px)", + "3xl": "(min-width: 1920px)" +} as const; + +const _default = { + sm: false, + md: false, + lg: false, + xl: false, + "2xl": false, + "3xl": false +}; + +export const media_query = () => { + const { subscribe, update } = writable(_default); + + const listeners: { + [key: string]: [MediaQueryList, (ev: MediaQueryListEvent) => any]; + } = {}; + const onChange = (key: string) => () => + update((s) => ({ ...s, [key]: !!listeners[key][0].matches })); + + if (typeof window !== "undefined") { + for (const key in sizes) { + const mql = window.matchMedia(sizes[key as keyof typeof sizes]); + const listener = onChange(key); + + mql.addEventListener("change", listener); + + listeners[key] = [mql, listener]; + } + } + + const cleanup = () => { + for (const key in listeners) { + const [_mql, _listener] = listeners[key]; + _mql.removeEventListener("change", _listener); + } + }; + + return { subscribe, cleanup }; +}; + +import slugify from "@sindresorhus/slugify"; + +export function make_slug_processor() { + const seen_slugs = new Map(); + + return function (name: string) { + const slug = slugify(name, { separator: "-", lowercase: true }); + let count = seen_slugs.get(slug); + if (count) { + seen_slugs.set(slug, count + 1); + return `${slug}-${count + 1}`; + } else { + seen_slugs.set(slug, 1); + return slug; + } + }; +} diff --git a/js/_website/src/lib/utils/clean-guide-html.ts b/js/_website/src/lib/utils/clean-guide-html.ts new file mode 100644 index 0000000..1ee9db2 --- /dev/null +++ b/js/_website/src/lib/utils/clean-guide-html.ts @@ -0,0 +1,54 @@ +/** + * Convert HTML tip/warning divs back to their original markdown "Tip: " / "Warning: " format, + * and replace embeds with the actual demo source code. + * Used when serving guide content as markdown (copy page button, content negotiation). + */ +export async function cleanGuideHtml(content: string): Promise { + // Convert tip divs back to "Tip: " prefix + content = content.replace( + /
\s*]*>[\s\S]*?<\/svg>\s*
([\s\S]*?)<\/div>\s*<\/div>/g, + (_match, inner) => { + const text = inner.replace(/<\/?p>/g, "").trim(); + return `\nTip: ${text}`; + } + ); + + // Convert warning divs back to "Warning: " prefix + content = content.replace( + /
\s*]*>[\s\S]*?<\/span>\s*([\s\S]*?)<\/div>/g, + (_match, inner) => { + const text = inner.replace(/<\/?p>/g, "").trim(); + return `\nWarning: ${text}`; + } + ); + + // Replace embeds with demo source code from Hugging Face + const appRegex = /<\/gradio-app>/g; + const matches = [...content.matchAll(appRegex)]; + + const results = await Promise.all( + matches.map(async (match) => { + const spaceName = match[1]; + try { + const res = await fetch( + `https://huggingface.co/spaces/gradio/${spaceName}/raw/main/app.py`, + { referrerPolicy: "no-referrer" } + ); + if (res.ok) { + const code = await res.text(); + return { + full: match[0], + replacement: `\`\`\`python\n${code}\n\`\`\`` + }; + } + } catch {} + return { full: match[0], replacement: "" }; + }) + ); + + for (const { full, replacement } of results) { + content = content.replace(full, replacement); + } + + return content; +} diff --git a/js/_website/src/lib/utils/svx-to-markdown.ts b/js/_website/src/lib/utils/svx-to-markdown.ts new file mode 100644 index 0000000..e15e59b --- /dev/null +++ b/js/_website/src/lib/utils/svx-to-markdown.ts @@ -0,0 +1,708 @@ +import { compile } from "mdsvex"; + +let docs: { [key: string]: any } | null = null; + +async function getDocs(): Promise<{ [key: string]: any }> { + if (docs === null) { + const docs_json = await import("$lib/templates/docs.json"); + docs = docs_json.default?.docs || docs_json.docs; + } + return docs; +} + +async function get_object(name: string): Promise { + const docsData = await getDocs(); + let obj: any; + + if (name === "events" || name === "events_matrix") { + obj = docsData["gradio"][name]; + return obj; + } + + for (const library in docsData) { + for (const key in docsData[library]) { + if (key === name && key !== "chatinterface") { + obj = docsData[library][key]; + break; + } else { + for (const o in docsData[library][key]) { + if (o === name) { + obj = docsData[library][key][o]; + break; + } + } + } + } + } + return obj; +} + +async function parseScriptObjects( + svxContent: string +): Promise<{ [varName: string]: any }> { + const objects: { [varName: string]: any } = {}; + + const scriptMatch = svxContent.match(/]*>([\s\S]*?)<\/script>/); + if (!scriptMatch) return objects; + + const scriptContent = scriptMatch[1]; + + // Find all get_object() calls and extract variable names and component names + // we need to do this because some svx files have multiple get_object() calls + const getObjectRegex = /let\s+(\w+)\s*=\s*get_object\(["']([^"']+)["']\)/g; + let match; + + while ((match = getObjectRegex.exec(scriptContent)) !== null) { + const varName = match[1]; + const componentName = match[2]; + + const obj = await get_object(componentName); + if (obj) { + objects[varName] = obj; + } + } + + return objects; +} + +function extractAttrVariable( + htmlValue: string, + attrName: string +): string | null { + const regex = new RegExp(`${attrName}=\\{(\\w+)\\.`); + const match = htmlValue.match(regex); + return match ? match[1] : null; +} + +function decode_html_entities(text: string | null): string { + if (text == null) return ""; + + const entities: { [key: string]: string } = { + """: '"', + "'": "'", + "&": "&", + "<": "<", + ">": ">", + " ": " ", + "¡": "¡", + "'": "'", + "'": "'" + }; + + const decimal_regex = /&#(\d+);/g; + const hex_regex = /&#x([0-9A-Fa-f]+);/g; + + const named_regex = new RegExp(Object.keys(entities).join("|"), "g"); + + return text + .replace(decimal_regex, (_, code) => + String.fromCharCode(parseInt(code, 10)) + ) + .replace(hex_regex, (_, code) => String.fromCharCode(parseInt(code, 16))) + .replace(named_regex, (match) => entities[match]); +} + +function strip_html_tags(text: string): string { + return text.replace(/<[^>]*>/g, "").trim(); +} + +function evaluateExpression( + text: string, + obj: any, + objects: { [varName: string]: any } = {} +): string { + let result = text.replace(/\{(\w+)\.([^}]+)\}/g, (_, varName, path) => { + const targetObj = objects[varName] || (varName === "obj" ? obj : null); + if (!targetObj) return ""; + + const value = getNestedValue(targetObj, path); + + if (typeof value === "string") { + return decode_html_entities(strip_html_tags(value)); + } + return String(value ?? ""); + }); + + result = result.replace( + /\{@html\s+style_formatted_text\((\w+)\.([\w\[\]\.]+)\)\}/g, + (_, varName, path) => { + const targetObj = objects[varName] || (varName === "obj" ? obj : null); + if (!targetObj) return ""; + + const value = getNestedValue(targetObj, path); + if (typeof value === "string") { + return decode_html_entities(strip_html_tags(value)); + } + return String(value ?? ""); + } + ); + + result = result.replace(/\{@html\s+[^}]+\}/g, ""); + + return result; +} + +function getNestedValue(obj: any, path: string): any { + const parts = path.split(/\.|\[|\]/).filter(Boolean); + + let value = obj; + for (const part of parts) { + if (value == null) return null; + value = value[part]; + } + return value; +} + +function evaluateCondition(condition: string, obj: any): boolean { + if (condition.includes("obj.")) { + const match = condition.match(/obj\.(\w+)/); + if (match) { + const prop = match[1]; + const value = obj[prop]; + + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + } + } + return true; +} + +function parametersToMarkdownTable(parameters: any[]): string { + if (!parameters || parameters.length === 0) return ""; + + let table = "| Parameter | Type | Default | Description |\n"; + table += "|-----------|------|---------|-------------|\n"; + + for (const param of parameters) { + const name = param.name || ""; + + const type = (param.annotation || "") + .replaceAll("Sequence[", "list[") + .replaceAll("AbstractSet[", "set[") + .replaceAll("Mapping[", "dict[") + .replace(/\|/g, "\\|"); + + const defaultVal = (param.default || "").replace(/\|/g, "\\|"); + + const doc = decode_html_entities(strip_html_tags(param.doc || "")).replace( + /\|/g, + "\\|" + ); + + table += `| \`${name}\` | \`${type}\` | \`${defaultVal}\` | ${doc} |\n`; + } + + return table; +} + +function demosToMarkdown(demos: any[]): string { + if (!demos || demos.length === 0) return ""; + + let md = ""; + for (const demo of demos) { + const name = demo[0]; + const code = demo[1]; + + md += `**${name}**\n\n`; + + md += `[See demo on Hugging Face Spaces](https://huggingface.co/spaces/gradio/${name})\n\n`; + + md += "```python\n"; + md += code; + if (!code.endsWith("\n")) md += "\n"; + md += "```\n\n"; + } + + return md; +} + +function shortcutsToMarkdownTable(shortcuts: any[]): string { + if (!shortcuts || shortcuts.length === 0) return ""; + + let table = "| Class | Interface String Shortcut | Initialization |\n"; + table += "|-------|--------------------------|----------------|\n"; + + for (const shortcut of shortcuts) { + const className = (shortcut[0] || "").replace(/\|/g, "\\|"); + const shortcutStr = (shortcut[1] || "").replace(/\|/g, "\\|"); + const initialization = (shortcut[2] || "").replace(/\|/g, "\\|"); + table += `| \`gradio.${className}\` | \`"${shortcutStr}"\` | ${initialization} |\n`; + } + + return table; +} + +function eventListenersToMarkdown(fns: any[]): string { + if (!fns || fns.length === 0) return ""; + + const parent = fns[0]?.parent?.replace("gradio.", "") || ""; + + let md = `#### Description + +Event listeners allow you to respond to user interactions with the UI components you've defined in a Gradio Blocks app. When a user interacts with an element, such as changing a slider value or uploading an image, a function is called. + +#### Supported Event Listeners + +The \`${parent}\` component supports the following event listeners: + +`; + + for (const fn of fns) { + const name = fn.name; + const description = decode_html_entities( + strip_html_tags(fn.description || "") + ); + md += `- \`${parent}.${name}(fn, ...)\`: ${description}\n`; + } + + md += "\n#### Event Parameters\n\n"; + if (fns[0]?.parameters) { + md += parametersToMarkdownTable(fns[0].parameters); + } + + return md; +} + +function guidesToMarkdown(guides: any[]): string { + if (!guides || guides.length === 0) return ""; + + let md = ""; + for (const guide of guides) { + md += `- [${guide.pretty_name}](https://www.gradio.app${guide.url})\n`; + } + + return md; +} + +interface MDASTNode { + type: string; // "heading", "paragraph", "code", "html", "text", etc. + children?: MDASTNode[]; // Child nodes (for container nodes like headings) + value?: string; // Text content (for leaf nodes like text, code) + depth?: number; // Heading depth (1-6 for h1-h6) + lang?: string; // Code block language (e.g., "python") + name?: string; // For svelteBlock nodes: "if", "each", etc. + label?: string; // For linkReference nodes + position?: any; // Source position info (not used by us) +} + +// ============================================================================= +// AST NODE PROCESSING +// ============================================================================= + +/** + * Get the raw text content from an AST node WITHOUT evaluating expressions. + * + * This is used to reconstruct fragmented text before evaluation. + * + * WHY IS THIS NEEDED? + * When the markdown parser sees something like: + * {@html style_formatted_text(obj.postprocess.parameter_doc[0].doc)} + * + * It incorrectly parses the [0] as a markdown link reference, creating: + * - text node: "{@html style_formatted_text(obj.postprocess.parameter_doc" + * - linkReference node: "[0]" + * - text node: ".doc)}" + * + * We need to reconstruct the original text before we can evaluate it. + * + * @param node - An MDAST node + * @returns The raw text content, preserving markdown formatting markers + */ +function getRawText(node: MDASTNode): string { + // Text nodes: return the text value directly + if (node.type === "text" && node.value) { + return node.value; + } + + // Strong (bold) nodes: wrap children in ** + if (node.type === "strong" && node.children) { + return `**${node.children.map(getRawText).join("")}**`; + } + + // Emphasis (italic) nodes: wrap children in * + if (node.type === "emphasis" && node.children) { + return `*${node.children.map(getRawText).join("")}*`; + } + + // Inline code nodes: wrap in backticks + if (node.type === "inlineCode" && node.value) { + return `\`${node.value}\``; + } + + // Link reference nodes: these are often mis-parsed array access like [0] + // Return the bracketed content so we can reconstruct expressions + if (node.type === "linkReference") { + return `[${node.children?.map(getRawText).join("") || node.label || ""}]`; + } + + // For any other node with children, recursively get text from children + if (node.children) { + return node.children.map(getRawText).join(""); + } + + return ""; +} + +/** + * Convert an array of child nodes to markdown text. + * + * This first reconstructs the full raw text (handling fragmented expressions), + * then evaluates any Svelte expressions in that text. + * + * @param children - Array of MDAST child nodes + * @param obj - The main component's documentation object (fallback) + * @param objects - Map of all variable names to their documentation objects + * @returns Evaluated markdown text + */ +function childrenToMarkdown( + children: MDASTNode[], + obj: any, + objects: { [varName: string]: any } = {} +): string { + // STEP 1: Get raw text from all children, concatenated together + // This reconstructs fragmented expressions + const rawText = children.map(getRawText).join(""); + + // STEP 2: Evaluate any Svelte expressions in the combined text + // Pass the objects map so expressions like {waveform_obj.description} work + const evaluated = evaluateExpression(rawText, obj, objects); + + return evaluated; +} + +/** + * Convert a single AST node to markdown, evaluating expressions. + * + * This is used for inline content within paragraphs. + * + * @param node - An MDAST node + * @param obj - The main component's documentation object (fallback) + * @param objects - Map of all variable names to their documentation objects + * @returns Markdown text + */ +function textNodeToMarkdown( + node: MDASTNode, + obj: any, + objects: { [varName: string]: any } = {} +): string { + // Text nodes: evaluate expressions in the text + if (node.type === "text" && node.value) { + return evaluateExpression(node.value, obj, objects); + } + + // Strong (bold) nodes: process children and wrap in ** + if (node.type === "strong" && node.children) { + const content = childrenToMarkdown(node.children, obj, objects); + return `**${content}**`; + } + + // Emphasis (italic) nodes: process children and wrap in * + if (node.type === "emphasis" && node.children) { + const content = childrenToMarkdown(node.children, obj, objects); + return `*${content}*`; + } + + // Inline code: just wrap in backticks (no expression evaluation in code) + if (node.type === "inlineCode" && node.value) { + return `\`${node.value}\``; + } + + // Link references: return bracketed content for reconstruction + if (node.type === "linkReference") { + return `[${node.children?.map((c) => textNodeToMarkdown(c, obj, objects)).join("") || ""}]`; + } + + return ""; +} + +/** + * Convert a heading AST node to markdown. + * + * Handles special cases where headings are used for styling rather than structure: + * - h2 (##) is used in SVX for description text, not actual headings + * - h5 (#####) is used for "Your function should accept..." helper text + * + * @param node - A heading MDAST node + * @param obj - The main component's documentation object (fallback) + * @param objects - Map of all variable names to their documentation objects + * @returns Markdown text (heading or plain text depending on depth) + */ +function headingToMarkdown( + node: MDASTNode, + obj: any, + objects: { [varName: string]: any } = {} +): string { + const depth = node.depth || 1; + + // Process children, handling fragmented expressions + // Pass objects map so expressions like {waveform_obj.name} work + const content = node.children + ? childrenToMarkdown(node.children, obj, objects) + : ""; + + // Skip empty headings (can happen after expression evaluation) + if (!content.trim()) return ""; + + // h2 (##) in SVX is used for styled description text, not actual headings + // Convert to regular paragraph text + if (depth === 2) { + return `${content}\n\n`; + } + + // h5 (#####) is used for helper text like "Your function should accept..." + // Convert to italic text + if (depth === 5) { + return `*${content}*\n\n`; + } + + // For other heading levels (h1, h3, h4, h6), output as normal markdown headings + const prefix = "#".repeat(depth); + return `${prefix} ${content}\n\n`; +} + +/** + * Process an HTML AST node and convert to markdown. + * + * In SVX files, "html" nodes contain: + * - Script tags (which we skip) + * - HTML comments (which we skip) + * - Svelte components like , (which we convert) + * - Other HTML elements + * + * @param node - An HTML MDAST node + * @param obj - The main component's documentation object (fallback) + * @param objects - Map of all variable names to their documentation objects + * @returns Markdown text (or empty string to skip) + */ +function htmlNodeToMarkdown( + node: MDASTNode, + obj: any, + objects: { [varName: string]: any } +): string { + const value = node.value || ""; + + // Skip + +
+
+ +

+ {$page.status} +

+

+ {$page.error?.message} +

+
+
+ + diff --git a/js/_website/src/routes/+layout.server.ts b/js/_website/src/routes/+layout.server.ts new file mode 100644 index 0000000..4c34aea --- /dev/null +++ b/js/_website/src/routes/+layout.server.ts @@ -0,0 +1,10 @@ +import { redirect } from "@sveltejs/kit"; +import { redirects } from "./redirects.js"; + +export const prerender = true; + +export async function load({ url }: any) { + if (url.pathname in redirects) { + throw redirect(308, redirects[url.pathname as keyof typeof redirects]); + } +} diff --git a/js/_website/src/routes/+layout.svelte b/js/_website/src/routes/+layout.svelte new file mode 100644 index 0000000..ab29006 --- /dev/null +++ b/js/_website/src/routes/+layout.svelte @@ -0,0 +1,110 @@ + + + + + + + + + + + +
+
+ + + +
+
+ + diff --git a/js/_website/src/routes/+page.server.ts b/js/_website/src/routes/+page.server.ts new file mode 100644 index 0000000..8290a18 --- /dev/null +++ b/js/_website/src/routes/+page.server.ts @@ -0,0 +1,25 @@ +import { logos, tweets } from "$lib/assets"; + +const STAR_COUNT_FALLBACK = 36_000; + +export async function load({ fetch }: { fetch: typeof globalThis.fetch }) { + let star_count = STAR_COUNT_FALLBACK; + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + const r = await fetch("https://api.github.com/repos/gradio-app/gradio", { + signal: controller.signal + }); + clearTimeout(timeout); + const j = await r.json(); + star_count = j.stargazers_count; + } catch { + // timeout or network error — use fallback + } + + return { + logos, + tweets, + star_count + }; +} diff --git a/js/_website/src/routes/+page.svelte b/js/_website/src/routes/+page.svelte new file mode 100644 index 0000000..969dabb --- /dev/null +++ b/js/_website/src/routes/+page.svelte @@ -0,0 +1,550 @@ + + + + + + + + +
+
+
+ + +
+ +

+ Build machine learning apps in Python +

+ +

+ Create web interfaces for your ML models in minutes. Deploy anywhere, + share with anyone. +

+ + +
+ + + +
+
+
+ +
+
+

+ Everything you need to build +

+

+ Gradio handles the frontend so you can focus on building. From prototypes + to production-ready web apps. +

+
+ +
+
+
+
+
+ + + +
+

Lightning Fast Setup

+

+ One command to install. A few lines of Python to launch. No + Javascript, CSS, or frontend experience required. +

+
+
+
+
+
+
+
+
+

+ $ pip install gradio +

+

Successfully installed gradio

+

+ $ python app.py +

+

Running on http://127.0.0.1:7860

+
+
+
+
+
+
+
+ + + +
+

40+ Components

+

+ Input and output for any data type: Images, Audio, Video, 3D, + Dataframes, and more. +

+ +
+ {#each ["Audio", "Image", "Chat", "Video", "Plot", "JSON"] as item} + {item} + {/each} + + many more +
+
+
+
+
+
+ + + +
+

Permanent Hosting

+

+ Deploy to Hugging Face Spaces for free. Always online, auto-scaling, + and shareable with a simple URL. +

+
+ + Live on HF Spaces +
+
+
+
+
+
+
+ + + +
+

Share Instantly

+

+ Create a public link to your machine learning demo running on your + local computer in seconds. Great for showing clients or colleagues. +

+
+ demo.launch(share=True) +
+
+
+
+
+
+
+ + + Live + +
+ +
+
+
+
+
+
+
+
+
+ https://78620.gradio.app +
+
+
+
+
+
+ What's the weather like? +
+
+
+
+
+
+ It's sunny and 72°F today! +
+
+
+
+
+
+ Perfect, thanks! +
+
+
+
+
+
+ ... +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + diff --git a/js/_website/src/routes/[[version]]/docs/+layout.server.ts b/js/_website/src/routes/[[version]]/docs/+layout.server.ts new file mode 100644 index 0000000..3eeec05 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/+layout.server.ts @@ -0,0 +1,64 @@ +import { redirect } from "@sveltejs/kit"; +import version from "$lib/json/version.json"; +import wheel from "$lib/json/wheel.json"; + +export const prerender = true; + +const DOCS_BUCKET = "https://gradio-docs-json.s3.us-west-2.amazonaws.com"; +const VERSION = version.version; + +let cache = new Map(); + +const components_to_document = ["dataframe", "js-client"]; + +async function load_release_docs( + version: string +): Promise { + if (cache.has(version)) { + return cache.get(version); + } + let docs_json = await fetch(`${DOCS_BUCKET}/${version}/docs.json`); + + let json = await docs_json.json(); + cache.set(version, json); + + return json; +} + +async function load_main_docs(): Promise { + return await import("$lib/json/docs.json"); +} + +export async function load({ params, url }) { + if (params?.version === VERSION) { + throw redirect(302, url.href.replace(`/${params.version}`, "")); + } + let on_main = params.version === "main" || !params.version; + let docs_json = on_main + ? await load_main_docs() + : await load_release_docs(params.version); + await load_main_docs(); + + let docs: { [key: string]: any } = docs_json.docs; + let js = docs_json.js || {}; + let js_pages = + docs_json.js_pages.filter((p: string) => + components_to_document.includes(p) + ) || []; + let js_client = docs_json.js_client; + let pages: any = docs_json.pages; + + let url_version = on_main ? "main" : params.version; + + return { + docs, + js, + js_pages, + on_main, + wheel, + pages, + js_client, + url_version, + VERSION + }; +} diff --git a/js/_website/src/routes/[[version]]/docs/+page.server.ts b/js/_website/src/routes/[[version]]/docs/+page.server.ts new file mode 100644 index 0000000..4d5b239 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/+page.server.ts @@ -0,0 +1,29 @@ +import { redirect } from "@sveltejs/kit"; +import * as version from "$lib/json/version.json"; + +const COLOR_SETS = [ + "green", + "yellow", + "red", + "blue", + "pink", + "purple", + "green", + "yellow", + "red", + "blue", + "pink", + "purple" +]; + +const VERSION = version.version; + +export async function load({ params, url }) { + if (params?.version === VERSION) { + throw redirect(302, url.href.replace(`/${params.version}`, "")); + } + + return { + COLOR_SETS + }; +} diff --git a/js/_website/src/routes/[[version]]/docs/+page.svelte b/js/_website/src/routes/[[version]]/docs/+page.svelte new file mode 100644 index 0000000..6412c28 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/+page.svelte @@ -0,0 +1,290 @@ + + + + + + + diff --git a/js/_website/src/routes/[[version]]/docs/gradio/+page.server.ts b/js/_website/src/routes/[[version]]/docs/gradio/+page.server.ts new file mode 100644 index 0000000..b1a7528 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/gradio/+page.server.ts @@ -0,0 +1,8 @@ +import { redirect } from "@sveltejs/kit"; + +export function load({ params }) { + if (params?.version) + throw redirect(302, `/${params?.version}/docs/gradio/interface`); + + throw redirect(302, `/docs/gradio/interface`); +} \ No newline at end of file diff --git a/js/_website/src/routes/[[version]]/docs/gradio/[doc]/+page.svelte b/js/_website/src/routes/[[version]]/docs/gradio/[doc]/+page.svelte new file mode 100644 index 0000000..cfcd05a --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/gradio/[doc]/+page.svelte @@ -0,0 +1,303 @@ + + + + + + +
+
+ + {#if category_and_name} +
    +
  1. {category_and_name[0]} + +
  2. +
  3. + {category_and_name[1]} +
  4. +
+ {/if} +
+ +
+ + +
+
+

+ New to Gradio? Start here: Getting Started +

+

+ See the Release History +

+
+ + {#if on_main} +
+

+ To install Gradio from main, run the following command: +

+
+
{install_command}
+
+

+ *Note: Setting share=True in + launch() will not work. +

+
+ {/if} + + +
+ {#if prev_obj} + +
+ +

{prev_obj.pretty_name}

+
+
+ {:else} +
+ {/if} + {#if next_obj} + +
+

{next_obj.pretty_name}

+ +
+
+ {:else} +
+ {/if} +
+ +
+ +
+ +
+
+
+ +
+
+
+ +
+ {#if prev_obj} + +
+ +

{prev_obj.pretty_name}

+
+
+ {:else} +
+ {/if} + {#if next_obj} + +
+

{next_obj.pretty_name}

+ +
+
+ {:else} +
+ {/if} +
+
+ + +
+
+ + diff --git a/js/_website/src/routes/[[version]]/docs/gradio/[doc]/+page.ts b/js/_website/src/routes/[[version]]/docs/gradio/[doc]/+page.ts new file mode 100644 index 0000000..86ab195 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/gradio/[doc]/+page.ts @@ -0,0 +1,62 @@ +import { error, redirect } from "@sveltejs/kit"; + +export async function load({ params, parent }) { + const { + on_main, + wheel, + pages, + url_version, + VERSION + } = await parent(); + + const modules : any = import.meta.glob("/src/lib/templates*/gradio/**/*.svx"); + let name = params.doc; + let version = params.version || VERSION; + let page_path : string | null = null; + + for (const category of pages.gradio) { + for (const page of category.pages) { + if (page.name === name) { + page_path = page.path; + } + } + } + + if (page_path === null) { + if (!params.version) { + throw redirect(308, `/docs/gradio/interface`); + } else { + throw redirect(308, `/${params.version}/docs/gradio/interface`); + } + } + + let version_append = on_main ? "/" : "_" + version.replace(/\./g, "-") + "/"; + + let module; + for (const path in modules) { + if (path.includes(page_path) && path.includes("templates" + version_append)) { + module = await modules[path](); + } + + } + + if (module === undefined) { + if (!params.version) { + throw redirect(302, `/docs/gradio/interface`); + } else { + throw redirect(302, `/${params.version}/docs/gradio/interface`); + } + } + + + + return { + name, + on_main, + wheel, + url_version, + pages, + page_path, + module + }; + } diff --git a/js/_website/src/routes/[[version]]/docs/js-client/+page.server.ts b/js/_website/src/routes/[[version]]/docs/js-client/+page.server.ts new file mode 100644 index 0000000..5165eae --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/js-client/+page.server.ts @@ -0,0 +1,82 @@ +import { compile } from "mdsvex"; +import anchor from "$lib/assets/img/anchor.svg"; +import { make_slug_processor } from "$lib/utils"; +import { toString as to_string } from "hast-util-to-string"; +import { highlight } from "$lib/prism"; + +export async function load({ parent }) { + const { + components, + helpers, + modals, + py_client, + routes, + js_client, + on_main, + wheel + } = await parent(); + + const guide_slug = []; + + const get_slug = make_slug_processor(); + function plugin() { + return function transform(tree: any) { + tree.children.forEach((n: any) => { + if ( + n.type === "element" && + ["h2", "h3", "h4", "h5", "h6"].includes(n.tagName) + ) { + const str_of_heading = to_string(n); + const slug = get_slug(str_of_heading); + + guide_slug.push({ + text: str_of_heading, + href: `#${slug}`, + level: parseInt(n.tagName.replace("h", "")) + }); + + if (!n.children) n.children = []; + n.properties.className = ["group"]; + n.properties.id = [slug]; + n.children.push({ + type: "element", + tagName: "a", + properties: { + href: `#${slug}`, + className: ["invisible", "group-hover-visible"] + }, + children: [ + { + type: "element", + tagName: "img", + properties: { + src: anchor, + className: ["anchor-img"] + }, + children: [] + } + ] + }); + } + }); + }; + } + + const compiled = await compile(js_client, { + rehypePlugins: [plugin], + highlight: { + highlighter: highlight + } + }); + let readme_html = await compiled?.code; + + return { + readme_html, + components, + helpers, + modals, + routes, + py_client, + wheel + }; +} diff --git a/js/_website/src/routes/[[version]]/docs/js-client/+page.svelte b/js/_website/src/routes/[[version]]/docs/js-client/+page.svelte new file mode 100644 index 0000000..83b8ec1 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/js-client/+page.svelte @@ -0,0 +1,122 @@ + + + + +
+
+ + +
+
+

+ New to Gradio? Start here: Getting Started +

+

+ See the Release History +

+
+
+

+ To install the Gradio JS Client from main, run the following command: +

+
+
{install_command}
+
+
+ +
+
+
+
+ {@html readme_html} +
+
+
+ + +
+
+
+ + diff --git a/js/_website/src/routes/[[version]]/docs/js/+page.server.ts b/js/_website/src/routes/[[version]]/docs/js/+page.server.ts new file mode 100644 index 0000000..3bc9f6d --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/js/+page.server.ts @@ -0,0 +1,28 @@ +import { redirect } from "@sveltejs/kit"; + +export const prerender = true; + +async function urlExists(fetch: any, url: string): Promise { + try { + const res = await fetch(url, { method: "HEAD" }); + return res.ok; + } catch (e) { + return false; + } +} + +export async function load({ params, fetch }) { + const url = params.version + ? `/${params.version}/docs/js/dataframe` + : `/docs/js/dataframe`; + const fallback_url = params.version + ? `/${params.version}/docs/js/js-client` + : `/docs/js/js-client`; + const exists = await urlExists(fetch, url); + + if (exists) { + throw redirect(302, url); + } + + throw redirect(302, fallback_url); +} diff --git a/js/_website/src/routes/[[version]]/docs/js/[jsdoc]/+page.server.ts b/js/_website/src/routes/[[version]]/docs/js/[jsdoc]/+page.server.ts new file mode 100644 index 0000000..06eaf3b --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/js/[jsdoc]/+page.server.ts @@ -0,0 +1,85 @@ +import { compile } from "mdsvex"; +import anchor from "$lib/assets/img/anchor.svg"; +import { make_slug_processor } from "$lib/utils"; +import { toString as to_string } from "hast-util-to-string"; +import { highlight } from "$lib/prism"; +import { error } from "@sveltejs/kit"; + +export const prerender = true; + +export async function load({ params, parent }) { + const { js, js_pages } = await parent(); + + let name = params.jsdoc; + const guide_slug = []; + + const get_slug = make_slug_processor(); + + if (!js_pages.some((p: string) => p === params.jsdoc)) { + throw error(404); + } + + function plugin() { + return function transform(tree: any) { + tree.children.forEach((n: any) => { + if ( + n.type === "element" && + ["h2", "h3", "h4", "h5", "h6"].includes(n.tagName) + ) { + const str_of_heading = to_string(n); + const slug = get_slug(str_of_heading); + + guide_slug.push({ + text: str_of_heading, + href: `#${slug}`, + level: parseInt(n.tagName.replace("h", "")) + }); + + if (!n.children) n.children = []; + n.properties.className = ["group"]; + n.properties.id = [slug]; + n.children.push({ + type: "element", + tagName: "a", + properties: { + href: `#${slug}`, + className: ["invisible", "group-hover-visible"] + }, + children: [ + { + type: "element", + tagName: "img", + properties: { + src: anchor, + className: ["anchor-img"] + }, + children: [] + } + ] + }); + } + }); + }; + } + + let readme_html; + + for (const key in js) { + if (key == name) { + const compiled = await compile(js[key], { + rehypePlugins: [plugin], + highlight: { + highlighter: highlight + } + }); + + readme_html = await compiled?.code; + } + } + + return { + name, + readme_html, + js_pages + }; +} diff --git a/js/_website/src/routes/[[version]]/docs/js/[jsdoc]/+page.svelte b/js/_website/src/routes/[[version]]/docs/js/[jsdoc]/+page.svelte new file mode 100644 index 0000000..79ead7e --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/js/[jsdoc]/+page.svelte @@ -0,0 +1,76 @@ + + + + +
+
+ + +
+
+

+ New to Gradio? Start here: Getting Started +

+

+ See the Release History +

+
+
+
+
+ {@html readme_html} +
+
+
+
+
+
+ + diff --git a/js/_website/src/routes/[[version]]/docs/js/storybook/+page.server.ts b/js/_website/src/routes/[[version]]/docs/js/storybook/+page.server.ts new file mode 100644 index 0000000..824bd77 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/js/storybook/+page.server.ts @@ -0,0 +1,80 @@ +import { compile } from "mdsvex"; +import anchor from "$lib/assets/img/anchor.svg"; +import { make_slug_processor } from "$lib/utils"; +import { toString as to_string } from "hast-util-to-string"; +import { highlight } from "$lib/prism"; + +export const prerender = true; + +export async function load({ params, parent }) { + const { js, js_pages } = await parent(); + + let name = params.jsdoc; + const guide_slug = []; + + const get_slug = make_slug_processor(); + + function plugin() { + return function transform(tree: any) { + tree.children.forEach((n: any) => { + if ( + n.type === "element" && + ["h2", "h3", "h4", "h5", "h6"].includes(n.tagName) + ) { + const str_of_heading = to_string(n); + const slug = get_slug(str_of_heading); + + guide_slug.push({ + text: str_of_heading, + href: `#${slug}`, + level: parseInt(n.tagName.replace("h", "")) + }); + + if (!n.children) n.children = []; + n.properties.className = ["group"]; + n.properties.id = [slug]; + n.children.push({ + type: "element", + tagName: "a", + properties: { + href: `#${slug}`, + className: ["invisible", "group-hover-visible"] + }, + children: [ + { + type: "element", + tagName: "img", + properties: { + src: anchor, + className: ["anchor-img"] + }, + children: [] + } + ] + }); + } + }); + }; + } + + let readme_html; + + for (const key in js) { + if (key == name) { + const compiled = await compile(js[key], { + rehypePlugins: [plugin], + highlight: { + highlighter: highlight + } + }); + + readme_html = await compiled?.code; + } + } + + return { + name, + readme_html, + js_pages + }; +} diff --git a/js/_website/src/routes/[[version]]/docs/js/storybook/+page.svelte b/js/_website/src/routes/[[version]]/docs/js/storybook/+page.svelte new file mode 100644 index 0000000..64917e1 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/js/storybook/+page.svelte @@ -0,0 +1,63 @@ + + + + +
+
+ + +
+
+

+ New to Gradio? Start here: Getting Started +

+

+ See the Release History +

+
+
+ +
+
+
+
+ + diff --git a/js/_website/src/routes/[[version]]/docs/python-client/+page.server.ts b/js/_website/src/routes/[[version]]/docs/python-client/+page.server.ts new file mode 100644 index 0000000..b7047f6 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/python-client/+page.server.ts @@ -0,0 +1,8 @@ +import { redirect } from "@sveltejs/kit"; + +export function load({ params }) { + if (params?.version) + throw redirect(302, `/${params?.version}/docs/python-client/introduction`); + + throw redirect(302, `/docs/python-client/introduction`); +} diff --git a/js/_website/src/routes/[[version]]/docs/python-client/[doc]/+page.svelte b/js/_website/src/routes/[[version]]/docs/python-client/[doc]/+page.svelte new file mode 100644 index 0000000..ea32554 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/python-client/[doc]/+page.svelte @@ -0,0 +1,254 @@ + + + + + + +
+
+ + +
+
+

+ New to Gradio? Start here: Getting Started +

+

+ See the Release History +

+
+ + {#if on_main} +
+

+ To install the Gradio Python Client from main, run the following + command: +

+
+
{install_command}
+
+
+ {/if} + +
+ {#if prev_obj} + +
+ +

+ {prev_obj.pretty_name} +

+
+
+ {:else} +
+ {/if} + {#if next_obj} + +
+

+ {next_obj.pretty_name} +

+ +
+
+ {:else} +
+ {/if} +
+ +
+
+
+ +
+
+
+ +
+ {#if prev_obj} + +
+ +

+ {prev_obj.pretty_name} +

+
+
+ {:else} +
+ {/if} + {#if next_obj} + +
+

+ {next_obj.pretty_name} +

+ +
+
+ {:else} +
+ {/if} +
+
+ + +
+
+ + diff --git a/js/_website/src/routes/[[version]]/docs/python-client/[doc]/+page.ts b/js/_website/src/routes/[[version]]/docs/python-client/[doc]/+page.ts new file mode 100644 index 0000000..a15e8e9 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/python-client/[doc]/+page.ts @@ -0,0 +1,57 @@ +import { error } from "@sveltejs/kit"; + +export async function load({ params, parent }) { + const { on_main, wheel, pages, url_version, VERSION } = await parent(); + + const modules: any = import.meta.glob( + "/src/lib/templates*/python-client/**/*.svx" + ); + + let name = params.doc; + let page_path: string | null = null; + + for (const category of pages["python-client"]) { + for (const page of category.pages) { + if (page.name === name) { + page_path = page.path; + } + } + } + + if (page_path === null) { + throw error(404); + } + + let version_append = on_main ? "/" : "_" + VERSION.replace(/\./g, "-") + "/"; + + let module; + + for (const path in modules) { + if (!path.includes(page_path)) { + continue; + } + + if (path.includes("templates" + version_append)) { + module = await modules[path](); + break; + } + + if (module === undefined && path.includes("templates/")) { + module = await modules[path](); + } + } + + if (module === undefined) { + throw error(404); + } + + return { + name, + on_main, + wheel, + url_version, + pages, + page_path, + module + }; +} diff --git a/js/_website/src/routes/[[version]]/docs/third-party-clients/+page.server.ts b/js/_website/src/routes/[[version]]/docs/third-party-clients/+page.server.ts new file mode 100644 index 0000000..6a70219 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/third-party-clients/+page.server.ts @@ -0,0 +1,11 @@ +import { redirect } from "@sveltejs/kit"; + +export function load({ params }) { + if (params?.version) + throw redirect( + 302, + `/${params?.version}/docs/third-party-clients/introduction` + ); + + throw redirect(302, `/docs/third-party-clients/introduction`); +} diff --git a/js/_website/src/routes/[[version]]/docs/third-party-clients/[doc]/+page.svelte b/js/_website/src/routes/[[version]]/docs/third-party-clients/[doc]/+page.svelte new file mode 100644 index 0000000..a511d73 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/third-party-clients/[doc]/+page.svelte @@ -0,0 +1,224 @@ + + + + + + +
+
+ + +
+
+

+ New to Gradio? Start here: Getting Started +

+

+ See the Release History +

+
+ +
+ {#if prev_obj} + +
+ + {prev_obj.pretty_name} +
+
+ {:else} +
+ {/if} + {#if next_obj} + +
+ {next_obj.pretty_name} + +
+
+ {:else} +
+ {/if} +
+ +
+
+
+ +
+
+
+ +
+ {#if prev_obj} + +
+ + {prev_obj.pretty_name} +
+
+ {:else} +
+ {/if} + {#if next_obj} + +
+ {next_obj.pretty_name} + +
+
+ {:else} +
+ {/if} +
+
+ + +
+
+ + diff --git a/js/_website/src/routes/[[version]]/docs/third-party-clients/[doc]/+page.ts b/js/_website/src/routes/[[version]]/docs/third-party-clients/[doc]/+page.ts new file mode 100644 index 0000000..482f1b3 --- /dev/null +++ b/js/_website/src/routes/[[version]]/docs/third-party-clients/[doc]/+page.ts @@ -0,0 +1,55 @@ +import { error } from "@sveltejs/kit"; + +export async function load({ params, parent }) { + const { on_main, wheel, pages, url_version, VERSION } = await parent(); + + const modules: any = import.meta.glob( + "/src/lib/templates*/third-party-clients/**/*.svx" + ); + let name = params.doc; + let page_path: string | null = null; + + for (const category of pages["third-party-clients"]) { + for (const page of category.pages) { + if (page.name === name) { + page_path = page.path; + } + } + } + + if (page_path === null) { + throw error(404); + } + + let version_append = on_main ? "/" : "_" + VERSION.replace(/\./g, "-") + "/"; + let module; + + for (const path in modules) { + if (!path.includes(page_path)) { + continue; + } + + if (path.includes("templates" + version_append)) { + module = await modules[path](); + break; + } + + if (module === undefined && path.includes("templates/")) { + module = await modules[path](); + } + } + + if (module === undefined) { + throw error(404); + } + + return { + name, + on_main, + wheel, + url_version, + pages, + page_path, + module + }; +} diff --git a/js/_website/src/routes/[[version]]/guides/+layout.server.ts b/js/_website/src/routes/[[version]]/guides/+layout.server.ts new file mode 100644 index 0000000..bff3978 --- /dev/null +++ b/js/_website/src/routes/[[version]]/guides/+layout.server.ts @@ -0,0 +1,82 @@ +import version from "$lib/json/version.json"; +import { redirect } from "@sveltejs/kit"; + +export const prerender = true; + +const DOCS_BUCKET = "https://gradio-docs-json.s3.us-west-2.amazonaws.com"; +const VERSION = version.version; + +let cache = new Map(); + +async function load_release_guide_names( + version: string +): Promise { + if (cache.has(`${version}_guide_names`)) { + return cache.get(`${version}_guide_names`); + } + let guide_names_json = await fetch( + `${DOCS_BUCKET}/${version}/guides/guide_names.json` + ); + + let json = await guide_names_json.json(); + cache.set(`${version}_guide_names`, json); + return json; +} + +async function load_main_guide_names() { + if (cache.has(`main_guide_names`)) { + return cache.get(`main_guide_names`); + } + let guide_names_json = await import( + `../../../lib/json/guides/guide_names.json` + ); + cache.set(`main_guide_names`, guide_names_json); + return guide_names_json; +} + +async function load_release_guides(version: string, guide_urls: string[]) { + if (cache.has(`${version}_guides`)) { + return cache.get(`${version}_guides`); + } + let guides: { [key: string]: any } = {}; + for (const guide_url of guide_urls) { + let guide_json = await fetch( + `${DOCS_BUCKET}/${version}/guides/${guide_url}.json` + ); + guides[guide_url] = await guide_json.json(); + } + cache.set(`${version}_guides`, guides); + return guides; +} + +async function load_main_guides(guide_urls: string[]) { + if (cache.has(`main_guides`)) { + return cache.get(`main_guides`); + } + let guides: { [key: string]: any } = {}; + for (const guide_url of guide_urls) { + let guide_json = await import(`../../../lib/json/guides/${guide_url}.json`); + guides[guide_url] = guide_json; + } + cache.set(`main_guides`, guides); + return guides; +} + +export async function load({ params, url }) { + if (params?.version === VERSION) { + throw redirect(302, url.href.replace(`/${params.version}`, "")); + } + let guide_names_json = + params?.version === "main" || !params?.version + ? await load_main_guide_names() + : await load_release_guide_names(params.version); + + let guides = + params?.version === "main" || !params?.version + ? await load_main_guides(guide_names_json.guide_urls) + : await load_release_guides(params.version, guide_names_json.guide_urls); + return { + guide_names_json, + guides + }; +} diff --git a/js/_website/src/routes/[[version]]/guides/+page.server.ts b/js/_website/src/routes/[[version]]/guides/+page.server.ts new file mode 100644 index 0000000..f6a210f --- /dev/null +++ b/js/_website/src/routes/[[version]]/guides/+page.server.ts @@ -0,0 +1,43 @@ +import { redirect } from "@sveltejs/kit"; +import * as version from "$lib/json/version.json"; + +const COLOR_SETS = [ + "green", + "yellow", + "red", + "blue", + "pink", + "purple", + "green", + "yellow", + "red", + "blue", + "pink", + "purple" +]; + +const DOCS_BUCKET = "https://gradio-docs-json.s3.us-west-2.amazonaws.com"; +const VERSION = version.version; + +async function load_release_guide_categories( + version: string +): Promise { + let docs_json = await fetch( + `${DOCS_BUCKET}/${version}/guides/guides_by_category.json` + ); + return await docs_json.json(); +} + +async function load_main_guide_categories() { + return await import(`../../../lib/json/guides/guides_by_category.json`); +} + +export async function load({ params, url }) { + if (params?.version === VERSION) { + throw redirect(302, url.href.replace(`/${params.version}`, "")); + } + + // Redirect to quickstart guide + const versionPrefix = params?.version ? `/${params.version}` : ""; + throw redirect(302, `${versionPrefix}/guides/quickstart`); +} diff --git a/js/_website/src/routes/[[version]]/guides/+page.svelte b/js/_website/src/routes/[[version]]/guides/+page.svelte new file mode 100644 index 0000000..5b9765f --- /dev/null +++ b/js/_website/src/routes/[[version]]/guides/+page.svelte @@ -0,0 +1,88 @@ + + + + +
+
+ + +
+
+ +
+

+ + Guides +

+ +
+

+ Learn how to build with Gradio through step-by-step guides and + tutorials. +

+
+
+
+
diff --git a/js/_website/src/routes/[[version]]/guides/[guide]/+page.server.ts b/js/_website/src/routes/[[version]]/guides/[guide]/+page.server.ts new file mode 100644 index 0000000..8746d97 --- /dev/null +++ b/js/_website/src/routes/[[version]]/guides/[guide]/+page.server.ts @@ -0,0 +1,99 @@ +import { compile } from "mdsvex"; +import { highlight } from "$lib/prism"; +import anchor from "$lib/assets/img/anchor.svg"; +import { make_slug_processor } from "$lib/utils"; +import { toString as to_string } from "hast-util-to-string"; +import { redirect } from "@sveltejs/kit"; +import { error } from "@sveltejs/kit"; + +import version from "$lib/json/version.json"; +export const prerender = true; + +const DOCS_BUCKET = "https://gradio-docs-json.s3.us-west-2.amazonaws.com"; +const VERSION = version.version; + +async function load_release_guides( + version: string, + guide: string +): Promise { + let guide_json = await fetch( + `${DOCS_BUCKET}/${version}/guides/${guide}.json` + ); + return await guide_json.json(); +} + +async function load_main_guides(guide: string) { + return await import(`../../../../lib/json/guides/${guide}.json`); +} + +export async function load({ params, url, parent }) { + const { guide_names_json, guides } = await parent(); + + if ( + !guide_names_json.guide_urls.some((guide: string) => guide === params.guide) + ) { + throw error(404); + } + + let guide_json = guides[params.guide]; + + let guide = guide_json.guide; + const guide_slug: object[] = []; + + const get_slug = make_slug_processor(); + function plugin() { + return function transform(tree: any) { + tree.children.forEach((n: any) => { + if ( + n.type === "element" && + ["h2", "h3", "h4", "h5", "h6"].includes(n.tagName) + ) { + const str_of_heading = to_string(n); + const slug = get_slug(str_of_heading); + + guide_slug.push({ + text: str_of_heading, + href: `#${slug}`, + level: parseInt(n.tagName.replace("h", "")) + }); + + if (!n.children) n.children = []; + n.properties.className = ["group"]; + n.properties.id = [slug]; + n.children.push({ + type: "element", + tagName: "a", + properties: { + href: `#${slug}`, + className: ["invisible", "group-hover-visible"] + }, + children: [ + { + type: "element", + tagName: "img", + properties: { + src: anchor, + className: ["anchor-img"] + }, + children: [] + } + ] + }); + } + }); + }; + } + + const compiled = await compile(guide.content, { + rehypePlugins: [plugin], + highlight: { highlighter: highlight }, + smartypants: false // This option converts `"` to `"` and `"` which breaks the code inside `` tags, so we disable it. + }); + guide.new_html = compiled?.code; + + return { + guide, + guide_slug, + guide_names: guide_names_json.guide_names + }; +} diff --git a/js/_website/src/routes/[[version]]/guides/[guide]/+page.svelte b/js/_website/src/routes/[[version]]/guides/[guide]/+page.svelte new file mode 100644 index 0000000..246af5b --- /dev/null +++ b/js/_website/src/routes/[[version]]/guides/[guide]/+page.svelte @@ -0,0 +1,404 @@ + + + +
+ + +
+
(show_nav = false)} + class:hidden={!show_nav} + class="max-w-max min-w-[75%] shadow overflow-y-auto fixed backdrop-blur-lg z-50 bg-white dark:bg-neutral-900 px-6 py-4 h-full inset-0" + id="mobile-nav" + > + + +
+ {#each guide_names as { category, guides } (category)} +
+

+ {category} +

+ +
+ {/each} +
+
+
+ + +
+
+ + {#if category_and_name} +
    +
  1. + {category_and_name[0]} + +
  2. +
  3. + {category_and_name[1]} +
  4. +
+ {/if} +
+ + {#if guide_page.spaces.length} +
+ + + +

Related Spaces:

+ {#each guide_page.spaces as space, i} + + {/each} +
+ {/if} +
+ + {@html guide_page.new_html} +
+
+ {#if prev_guide} + +
+ +

+ {prev_guide.pretty_name} +

+
+
+ {:else} +
+ {/if} + {#if next_guide} + +
+

+ {next_guide.pretty_name} +

+ +
+
+ {:else} +
+ {/if} +
+
+ + {#if guide_slug.length > 0} + + {/if} +
+ + + + diff --git a/js/_website/src/routes/api/markdown/[doc]/+server.ts b/js/_website/src/routes/api/markdown/[doc]/+server.ts new file mode 100644 index 0000000..93618fc --- /dev/null +++ b/js/_website/src/routes/api/markdown/[doc]/+server.ts @@ -0,0 +1,49 @@ +import { readFileSync } from "fs"; +import { resolve as pathResolve } from "path"; +import { svxToMarkdown } from "$lib/utils/svx-to-markdown"; +import docs_json from "$lib/templates/docs.json"; + +export const prerender = true; + +const MARKDOWN_HEADERS = { + "Content-Type": "text/markdown; charset=utf-8", + "X-Robots-Tag": "noindex" +}; + +export function entries() { + return docs_json.pages.gradio.flatMap((category) => + category.pages.map((page) => ({ doc: page.name })) + ); +} + +export async function GET({ params }) { + const name = params.doc; + const pages = docs_json.pages; + + let svxPath: string | null = null; + for (const category of pages.gradio) { + for (const page of category.pages) { + if (page.name === name) { + svxPath = page.path; + break; + } + } + if (svxPath) break; + } + + if (!svxPath) { + return new Response("Doc not found", { status: 404 }); + } + + try { + const fullPath = pathResolve(process.cwd(), "src/lib/templates", svxPath); + const svxContent = readFileSync(fullPath, "utf-8"); + + const markdown = await svxToMarkdown(svxContent, name); + + return new Response(markdown, { headers: MARKDOWN_HEADERS }); + } catch (error) { + console.error("Error generating markdown:", error); + return new Response("Error generating markdown", { status: 500 }); + } +} diff --git a/js/_website/src/routes/api/markdown/guide/[guide]/+server.ts b/js/_website/src/routes/api/markdown/guide/[guide]/+server.ts new file mode 100644 index 0000000..820ed3f --- /dev/null +++ b/js/_website/src/routes/api/markdown/guide/[guide]/+server.ts @@ -0,0 +1,33 @@ +import guide_names from "$lib/json/guides/guide_names.json"; +import { cleanGuideHtml } from "$lib/utils/clean-guide-html"; + +export const prerender = true; + +const MARKDOWN_HEADERS = { + "Content-Type": "text/markdown; charset=utf-8", + "X-Robots-Tag": "noindex" +}; + +export function entries() { + return guide_names.guide_urls.map((guide) => ({ guide })); +} + +export async function GET({ params }) { + const { guide } = params; + + try { + const module = await import(`$lib/json/guides/${guide}.json`); + const data = module.default || module; + const markdown = data.guide?.content; + + if (!markdown) { + return new Response("Guide not found", { status: 404 }); + } + + return new Response(await cleanGuideHtml(markdown), { + headers: MARKDOWN_HEADERS + }); + } catch { + return new Response("Guide not found", { status: 404 }); + } +} diff --git a/js/_website/src/routes/brand/+page.svelte b/js/_website/src/routes/brand/+page.svelte new file mode 100644 index 0000000..d32850b --- /dev/null +++ b/js/_website/src/routes/brand/+page.svelte @@ -0,0 +1,68 @@ + + +
+

+ Brand assets +

+ +
+
+ {#each LOGO_OPTIONS as logo} +
+
+ {logo.thumbnailAlt} +
+ +
+ {/each} +
+
+
diff --git a/js/_website/src/routes/changelog/+page.server.ts b/js/_website/src/routes/changelog/+page.server.ts new file mode 100644 index 0000000..e172226 --- /dev/null +++ b/js/_website/src/routes/changelog/+page.server.ts @@ -0,0 +1,69 @@ +import changelog_json from "$lib/json/changelog.json"; +import { compile } from "mdsvex"; +import anchor from "$lib/assets/img/anchor.svg"; +import version from "$lib/json/version.json"; + +import { make_slug_processor } from "$lib/utils"; +import { toString as to_string } from "hast-util-to-string"; + +import { highlight } from "$lib/prism"; + +const raw_content = changelog_json.content; + +export async function load() { + const changelog_slug: object[] = []; + + const get_slug = make_slug_processor(); + function plugin() { + return function transform(tree: any) { + tree.children.forEach((n: any) => { + if (n.type === "element" && ["h2"].includes(n.tagName)) { + const str_of_heading = to_string(n); + const slug = get_slug(str_of_heading); + + changelog_slug.push({ + text: str_of_heading, + href: `#${slug}`, + level: parseInt(n.tagName.replace("h", "")) + }); + + if (!n.children) n.children = []; + n.properties.className = ["group"]; + n.properties.id = [slug]; + n.children.push({ + type: "element", + tagName: "a", + properties: { + href: `#${slug}`, + className: ["invisible", "group-hover-visible"] + }, + children: [ + { + type: "element", + tagName: "img", + properties: { + src: anchor, + className: ["anchor-img"] + }, + children: [] + } + ] + }); + } + }); + }; + } + + const compiled = await compile(raw_content, { + rehypePlugins: [plugin], + highlight: { + highlighter: highlight + } + }); + const content = (await compiled?.code) || ""; + + return { + content, + changelog_slug + }; +} diff --git a/js/_website/src/routes/changelog/+page.svelte b/js/_website/src/routes/changelog/+page.svelte new file mode 100644 index 0000000..cc7f404 --- /dev/null +++ b/js/_website/src/routes/changelog/+page.svelte @@ -0,0 +1,134 @@ + + + + + + +
+
+ +
    +
  1. + Version History +
  2. +
+
+ +
+
+ +
(show_nav = false)} + class="w-64 flex-shrink-0 max-h-[calc(100vh-4rem)] overflow-y-auto max-lg:fixed max-lg:inset-0 max-lg:z-50 bg-white lg:bg-transparent dark:bg-neutral-900 lg:dark:bg-transparent p-6 lg:sticky lg:top-8 lg:self-start {show_nav + ? 'block' + : 'hidden lg:block'}" + > + + + +
+ +
+
+
+ {@html content} +
+
+
+
+
diff --git a/js/_website/src/routes/custom-components/gallery/+page.svelte b/js/_website/src/routes/custom-components/gallery/+page.svelte new file mode 100644 index 0000000..f6fb642 --- /dev/null +++ b/js/_website/src/routes/custom-components/gallery/+page.svelte @@ -0,0 +1,257 @@ + + + + +
+ +
+ Search through {components_length} components by name, keyword or description. + Read more about Custom Components. +
+
+ {#each components as component (component.id)} +
{ + handle_box_click(component); + event.stopPropagation(); + }} + class=" cursor-pointer px-3 pt-3 h-40 group font:thin relative rounded-xl shadow-sm transform hover:scale-[1.02] hover:shadow-alternate transition bg-gradient-to-tr {component.background_color}" + > +

+ {component.name.startsWith("gradio_") + ? component.name.slice(7) + : component.name} +

+ + {#if component.likes} +

+ + + {component.likes ? component.likes : ""} + +

+ {/if} +

+ {component.description} +

+

+ + @{component.author} + +

+ {#if component.template && component.template != "Fallback"} +

+ + {component.template} + +

+ {/if} +
+ {/each} +
+
+ +{#if selected_component} + {@const component = selected_component} +
{ + selected_component = null; + }} + > +
+ {#if !link_copied} + + {:else} + + Link copied to clipboard! + + {/if} + + Go to Space + +
+ +
+{/if} + + diff --git a/js/_website/src/routes/custom-components/gallery/Card.svelte b/js/_website/src/routes/custom-components/gallery/Card.svelte new file mode 100644 index 0000000..1ae3b40 --- /dev/null +++ b/js/_website/src/routes/custom-components/gallery/Card.svelte @@ -0,0 +1,146 @@ + + +
+

{data.description}

+
+
+ + pip install {data.name.replace("_", "-")} + + +
+
+ 🖋️ Author + {data.author} +
+
+ 📑 Template + {data.template} +
+
+ 🔢 Version: + {data.version} +
+
+ 🧬 + code +
+
+
+
+ 🔖 Tags: + {data.tags.split(",").join(", ")} +
+
+
+
+ 🤝 Feedback? Stuck? + + Ask for help +
+
+
+
+ {#each tabs as tab, index} +
{ + active_tab = index; + }} + > + {tab} +
+ {/each} +
+ {#if active_tab === 0} + + {:else} +
+ +
+ {/if} +
+
+ + diff --git a/js/_website/src/routes/custom-components/gallery/utils.ts b/js/_website/src/routes/custom-components/gallery/utils.ts new file mode 100644 index 0000000..15365c7 --- /dev/null +++ b/js/_website/src/routes/custom-components/gallery/utils.ts @@ -0,0 +1,60 @@ +export type ComponentData = { + id: string; + name: string; + template: string; + author: string; + description: string; + tags: string; + version: string; + subdomain: string; + background_color: string; + likes: number; +}; + +export const classToEmojiMapping: { [key: string]: string } = { + AnnotatedImage: "🖼️", + Audio: "🔊", + Plot: "📈", + Button: "🔘", + Chatbot: "🤖", + Code: "💻", + ColorPicker: "🎨", + Dataframe: "📊", + Dataset: "📚", + Fallback: "🔄", + File: "📄", + FileExplorer: "📂", + Gallery: "🎨", + HighlightedText: "✨", + HTML: "🔗", + Image: "🖼️", + JSON: "📝", + Label: "🏷️", + Markdown: "📝", + Model3D: "🗿", + State: "🔢", + UploadButton: "📤", + Video: "🎥" +}; + +export function clickOutside(element: HTMLDivElement, callbackFunction: any) { + function onClick(event: any) { + if ( + !element.contains(event.target) && + !(event.target.textContent && event.target.textContent === "Share") + ) { + callbackFunction(); + } + } + + document.body.addEventListener("click", onClick); + + return { + update(newCallbackFunction: any) { + callbackFunction = newCallbackFunction; + }, + destroy() { + document.body.removeEventListener("click", onClick); + } + }; +} diff --git a/js/_website/src/routes/custom-components/html-gallery/+page.svelte b/js/_website/src/routes/custom-components/html-gallery/+page.svelte new file mode 100644 index 0000000..523dbf1 --- /dev/null +++ b/js/_website/src/routes/custom-components/html-gallery/+page.svelte @@ -0,0 +1,563 @@ + + + + +
+
+

+ HTML Components Gallery +

+

+ Interactive components built with gr.HTML + using just HTML, CSS, and JavaScript. Click "View Code" to copy the Python class. +

+
+ + {#if loading} +
+
+

Loading components...

+
+ {:else if error} +
+

{error}

+ +
+ {:else} +
+ +
+ + {#if filtered.length === 0} +

+ No components match your search. +

+ {:else} +
+ {#each filtered as entry (entry.id)} +
+ +
+ {/each} +
+ {/if} + {/if} + + {#if maximized_component} + + + + {/if} +
+ + diff --git a/js/_website/src/routes/custom-components/html-gallery/ComponentEntry.svelte b/js/_website/src/routes/custom-components/html-gallery/ComponentEntry.svelte new file mode 100644 index 0000000..cb29c4a --- /dev/null +++ b/js/_website/src/routes/custom-components/html-gallery/ComponentEntry.svelte @@ -0,0 +1,412 @@ + + +
+
+ +
+ {#if !show_code} + + {/if} + + {#if show_code && component} + + {/if} +
+
+ +
+ {#if show_code} + {#if component} +

+ This code may be simplified. + {#if manifest.repo_url} + Visit the repo for the full implementation. + {/if} +

+
+ {@html highlighted_html} +
+ {:else} +
+
+ Loading code... +
+ {/if} + {:else if component} +
+ {#if use_iframe} + + {:else if has_children_slot} + + Click Me + + {:else} + + {/if} +
+ {:else} +
+
+ Loading component... +
+ {/if} +
+
+ + diff --git a/js/_website/src/routes/custom-components/html-gallery/types.ts b/js/_website/src/routes/custom-components/html-gallery/types.ts new file mode 100644 index 0000000..eeab83b --- /dev/null +++ b/js/_website/src/routes/custom-components/html-gallery/types.ts @@ -0,0 +1,17 @@ +export type ManifestEntry = { + id: string; + name: string; + description: string; + author: string; + tags: string[]; + repo_url?: string; +}; + +export type HTMLComponentEntry = ManifestEntry & { + html_template: string; + css_template: string; + js_on_load: string; + default_props: Record; + python_code: string; + head?: string; +}; diff --git a/js/_website/src/routes/custom-components/html-gallery/utils.ts b/js/_website/src/routes/custom-components/html-gallery/utils.ts new file mode 100644 index 0000000..4e122f2 --- /dev/null +++ b/js/_website/src/routes/custom-components/html-gallery/utils.ts @@ -0,0 +1,110 @@ +import themeCSS from "$lib/assets/theme.css?raw"; + +export function clickOutside( + element: HTMLDivElement, + callbackFunction: () => void +) { + function onClick(event: MouseEvent) { + if (!element.contains(event.target as Node)) { + callbackFunction(); + } + } + + document.body.addEventListener("click", onClick); + + return { + update(newCallbackFunction: () => void) { + callbackFunction = newCallbackFunction; + }, + destroy() { + document.body.removeEventListener("click", onClick); + } + }; +} + +/** + * Returns true if the component's CSS uses `position: fixed`, + * meaning it needs iframe isolation to render correctly in the gallery. + */ +export function needs_iframe(css_template: string | undefined): boolean { + if (!css_template) return false; + return /position\s*:\s*fixed/i.test(css_template); +} + +/** + * Render a component's template string (Handlebars + JS template literals) + * the same way BaseHTML does, but synchronously in the parent page. + */ +function render_template(template: string, props: Record): string { + try { + const keys = Object.keys(props); + const values = Object.values(props); + const fn = new Function(...keys, `return \`${template}\``); + return fn(...values); + } catch (e) { + console.error("Template rendering error:", e); + return ""; + } +} + +/** + * Build a self-contained HTML document string for rendering a component + * inside an iframe. Includes theme CSS, component CSS, rendered HTML, + * and js_on_load with a reactive props proxy. + */ +export function build_srcdoc( + component: { + html_template: string; + css_template: string; + js_on_load?: string | null; + head?: string | null; + default_props: Record; + }, + props: Record, + dark: boolean = false +): string { + const html = render_template(component.html_template, props); + const css = render_template(component.css_template, props); + + // Escape + + + + + +${component.head || ""} + + +
${html}
+ + + + +Gradio Hackathon Winners + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/_website/src/routes/llms.txt/+server.ts b/js/_website/src/routes/llms.txt/+server.ts new file mode 100644 index 0000000..aa22685 --- /dev/null +++ b/js/_website/src/routes/llms.txt/+server.ts @@ -0,0 +1,41 @@ +import { json } from "@sveltejs/kit"; +import SYSTEM_PROMPT from "$lib/json/system_prompt.json"; + +export const prerender = true; + +export async function GET({ url }) { + const worker_url = "https://playground-worker.pages.dev/api/prompt"; + // const worker_url = "http://localhost:5173/api/prompt"; + + // const query = url.searchParams.get("q")?.toLowerCase() || ""; + // const format = url.searchParams.get("format")?.toLowerCase() || "text"; + + const query = ""; + const format = "text"; + + const response = await fetch(worker_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Origin: url.origin + }, + body: JSON.stringify({ + prompt_to_embed: query, + SYSTEM_PROMPT: SYSTEM_PROMPT.SYSTEM, + FALLBACK_PROMPT: SYSTEM_PROMPT.FALLBACK + }) + }); + + const data = await response.json(); + + if (format === "json") { + return json(data); + } else { + return new Response(data.SYS_PROMPT, { + headers: { + "Content-Type": "text/plain", + "Cache-Control": "public, max-age=3600" + } + }); + } +} diff --git a/js/_website/src/routes/mcp-birthday-winners/+page.svelte b/js/_website/src/routes/mcp-birthday-winners/+page.svelte new file mode 100644 index 0000000..ef0bfd8 --- /dev/null +++ b/js/_website/src/routes/mcp-birthday-winners/+page.svelte @@ -0,0 +1,729 @@ + + + + +MCP's 1st Birthday Hackathon + + + + +
+

+ Track 2: MCP in Action Winners +

+ +

+ Enterprise Category +

+ + +
+ + 🥇 +
+
+ Vehicle Diagnostic Assistant 🚗 🛠️ +
+ castlebbs & stargarnet +
+ AI agent that connects to your car via an embedded MCP server to provide + real-time diagnostics and insights. +
+
+ + + +

+ Consumer Category +

+ + +
+ + 🥇 +
+
+ MCP Blockly +
+ owenkaplinsky +
+ AI that makes MCP servers with block-code. +
+
+ + + +

+ Creative Category +

+ + +
+ + 🥇 +
+
+ 🎬 Vidzly - Your AI-Powered Short Video Creator +
+ + tthhanh & tiena2cva & Nlag & Daphneee17 +
+ Transform raw footage into viral-ready content in seconds. No skills + required. No expensive gear needed. Just your vision and our AI. +
+
+ + +
+ + + diff --git a/js/_website/src/routes/redirects.js b/js/_website/src/routes/redirects.js new file mode 100644 index 0000000..669cd61 --- /dev/null +++ b/js/_website/src/routes/redirects.js @@ -0,0 +1,30 @@ +export const redirects = { + "/guides/creating-a-new-component": + "/guides/custom-components-in-five-minutes", + "/guides/five-minute-guide": "/guides/custom-components-in-five-minutes", + "/guides/key-features": "/guides/queuing", + "/quickstart": "/guides/quickstart", + "/sharing-your-app": "/guides/sharing-your-app", + "/introduction_to_blocks": "/guides/blocks-and-event-listeners", + "/getting_started": "/guides/quickstart", + "/guides/building-with-blocks": "/guides/blocks-and-event-listeners", + "/demos": "/docs", + "/playground": "/docs", + "/docs/client": "/docs/python-client/client", + "/docs/blocks": "/docs/gradio/blocks", + "/docs/audio": "/docs/gradio/audio", + "/docs/image": "/docs/gradio/image", + "/docs/textbox": "/docs/gradio/textbox", + "/docs/interface": "/docs/gradio/interface", + "/docs/python-client/python-client": "/docs/python-client/introduction", + "/guides/sharing-your-app#security-and-file-access": "/guides/file-access", + "/docs/gradio/interface#interface-queue": "/docs/gradio/interface", + "/using_hugging_face_integrations": "/guides/using-hugging-face-integrations", + "/main/guides/object-detection-from-webcam": + "/main/guides/object-detection-from-webcam-with-webrtc", + "/guides/chatbots/chat_interface_examples": + "/main/guides/chatinterface-examples", + "/guides/chatbots/creating-a-discord-bot-from-a-gradio-app": + "/guides/creating-a-discord-bot-from-a-gradio-app", + "/guides/chatbots/agents-and-tool-usage": "/guides/agents-and-tool-usage" +}; diff --git a/js/_website/src/routes/search-api/+server.ts b/js/_website/src/routes/search-api/+server.ts new file mode 100644 index 0000000..a57c662 --- /dev/null +++ b/js/_website/src/routes/search-api/+server.ts @@ -0,0 +1,148 @@ +import { json } from "@sveltejs/kit"; +import { render } from "svelte/server"; + +export const prerender = true; + +function removeMarkdown(markdown) { + return markdown + .replace(/^#{1,6}\s+/gm, "") + .replace(/(\*\*|__)(.*?)\1/g, "$2") + .replace(/(\*|_)(.*?)\1/g, "$2") + .replace(/~~(.*?)~~/g, "$1") + .replace(/`([^`]+)`/g, "$1") + .replace(/```[\s\S]*?```/g, "") + .replace(/!\[.*?\]\(.*?\)/g, "") + .replace(/\[(.*?)\]\(.*?\)/g, "$1") + .replace(/^>\s+/gm, "") + .replace(/^---$/gm, "") + .replace(/^\s*[-+*]\s+/gm, "") + .replace(/^\s*\d+\.\s+/gm, "") + .replace(/\n{2,}/g, "\n") + .trim(); +} + +export async function GET() { + const gradio_doc_paths = import.meta.glob( + "/src/lib/templates/gradio/**/*.svx" + ); + const gradio_doc_pages = await Promise.all( + Object.entries(gradio_doc_paths).map(async ([path, content]) => { + const module = await content(); + const rendered = render(module.default, { props: {} }); + content = rendered.body; + let match = content.match(/]*>(.*?)<\/h1>/i); + let title = ""; + if (match && match[1]) { + title = match[1]; + } + path = path.split("/").slice(-1)[0]; + path = path.match(/(?:\d{2}_)?(.+)/i)[1]; + path = "/main/docs/gradio/" + path.split(".svx")[0]; + + // content = content.replace(/
([^]*?)<\/div>/g, '') + content = content.replace(/([^]*?)<\/gradio-lite>/g, ""); + content = content.replace( + /]*>]*>([^]*?)<\/code><\/pre>/g, + "```\n$1\n```" + ); + content = content.replace( + /]*>|<\/span>|<\/?[^>]*(token)[^>]*>/g, + "" + ); + content = content.replace(/<[^>]*>?/gm, ""); + content = content.replace(/Open in 🎢.*?\n\t\t/g, ""); + + return { + title: title, + slug: path, + content: content, + type: "DOCS" + }; + }) + ); + + const client_doc_paths = import.meta.glob( + "/src/lib/templates/python-client/**/*.svx" + ); + const client_doc_pages = await Promise.all( + Object.entries(client_doc_paths).map(async ([path, content]) => { + const module = await content(); + const rendered = render(module.default, { props: {} }); + content = rendered.body; + let match = content.match(/]*>(.*?)<\/h1>/i); + let title = ""; + if (match && match[1]) { + title = match[1]; + } + path = path.split("/").slice(-1)[0]; + path = path.match(/(?:\d{2}_)?(.+)/i)[1]; + path = "/main/docs/python-client/" + path.split(".svx")[0]; + + content = content.replace( + /]*?language-(\w+)[^>]*?>]*?>([^]*?)<\/code><\/pre>/g, + "```$1\n$2\n```" + ); + content = content.replace( + /]*>|<\/span>|<\/?[^>]*(token)[^>]*>/g, + "" + ); + content = content.replace( + /]*>([^]*?)<\/gradio-lite>/g, + "```python\n$1\n```" + ); + content = content.replace(/<[^>]*>?/gm, ""); + + return { + title: title, + slug: path, + content: content, + type: "DOCS" + }; + }) + ); + + const guide_paths = import.meta.glob("/src/lib/json/guides/*.json"); + delete guide_paths["/src/lib/json/guides/guides_by_category.json"]; + delete guide_paths["/src/lib/json/guides/guide_names.json"]; + const guide_pages = await Promise.all( + Object.entries(guide_paths).map(async ([path, content]) => { + content = await content(); + content = content.default.guide; + return { + title: content.pretty_name, + slug: content.url, + content: removeMarkdown(content.content.replaceAll(/<[^>]*>?/gm, "")), + type: "GUIDE" + }; + }) + ); + + const jsons_path = import.meta.glob("/src/lib/json/docs.json"); + const jsons_content = await jsons_path["/src/lib/json/docs.json"](); + + const js_client_page = { + title: "JavaScript Client Library", + slug: "/docs/js-client", + content: removeMarkdown(jsons_content.default.js_client), + type: "DOCS" + }; + + const js_components = jsons_content.default.js; + const js_pages = await Promise.all( + Object.entries(js_components).map(async ([name, content]) => { + return { + title: name, + slug: "/docs/js/" + name, + content: removeMarkdown(content.replaceAll(/<[^>]*>?/gm, "")), + type: "DOCS" + }; + }) + ); + + let all_pages = gradio_doc_pages + .concat(client_doc_pages) + .concat(guide_pages) + .concat([js_client_page]) + .concat(js_pages); + return json(all_pages); +} diff --git a/js/_website/src/routes/themes/+page.svelte b/js/_website/src/routes/themes/+page.svelte new file mode 100644 index 0000000..b2ecd64 --- /dev/null +++ b/js/_website/src/routes/themes/+page.svelte @@ -0,0 +1,192 @@ + + + + + + {#if font_url} + + {/if} + + + + +
+
+

+ Customize Your Gradio Apps +

+

+ Give your machine learning demos a unique look with Gradio's theming + system. Choose from official themes or create your own. +

+ +
+ +
+
+

+ Official Themes +

+ +
+
+ {#each themes as theme (theme.id)} + handle_theme_click(theme.id)} + /> + {/each} +
+
+ +
+

+ Quick Start +

+

+ Apply a theme to your Gradio app in just one line of code +

+
+
import gradio as gr
+
+with gr.Blocks(theme=gr.themes.Soft()) as demo:
+    gr.Markdown("# Hello World")
+    gr.Textbox(label="Input")
+    gr.Button("Submit")
+
+
+ + +
diff --git a/js/_website/src/routes/themes/gallery/+page.svelte b/js/_website/src/routes/themes/gallery/+page.svelte new file mode 100644 index 0000000..b1e1796 --- /dev/null +++ b/js/_website/src/routes/themes/gallery/+page.svelte @@ -0,0 +1,223 @@ + + + + + + {#if font_url} + + {/if} + {#each custom_stylesheets as sheet} + + {/each} + + + + +
+
+

+ Theme Gallery +

+

+ Customize the look of your Gradio apps. Browse official and community + themes, preview colors and fonts, then copy the code to use them. +

+
+ +
+ +
+ +
+
+ + + {#if community_count > 0} + + {/if} +
+ +
+ +
+ {#each themes as theme (theme.id)} + handle_card_click(theme)} + /> + {/each} +
+ + {#if themes.length === 0} +
+

+ {#if search_query} + No themes found matching "{search_query}" + {:else if active_filter === "community"} + No community themes yet. + {:else} + No themes found + {/if} +

+
+ {/if} +
+ +{#if selected_theme} + +{/if} diff --git a/js/_website/src/routes/themes/gallery/+page.ts b/js/_website/src/routes/themes/gallery/+page.ts new file mode 100644 index 0000000..ee9a93e --- /dev/null +++ b/js/_website/src/routes/themes/gallery/+page.ts @@ -0,0 +1,12 @@ +import type { ThemeData } from "./types"; +import { BUILTIN_THEMES, fetch_community_themes } from "./utils"; + +export async function load({ fetch }: { fetch: typeof globalThis.fetch }) { + let community_themes: ThemeData[] = []; + try { + community_themes = await fetch_community_themes(fetch); + } catch { + // fail silently — show builtins only + } + return { themes: [...BUILTIN_THEMES, ...community_themes] }; +} diff --git a/js/_website/src/routes/themes/gallery/ThemeCard.svelte b/js/_website/src/routes/themes/gallery/ThemeCard.svelte new file mode 100644 index 0000000..0e42bb0 --- /dev/null +++ b/js/_website/src/routes/themes/gallery/ThemeCard.svelte @@ -0,0 +1,240 @@ + + +
{ + on_click(); + event.stopPropagation(); + }} + on:keydown={(event) => { + if (event.key === "Enter" || event.key === " ") { + on_click(); + event.preventDefault(); + } + }} + role="button" + tabindex="0" + class="card-container cursor-pointer group relative rounded-xl overflow-hidden border" + style=" + background: {current_bg}; + border-color: {is_dark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)'}; + " +> +
+
+ {theme.name} +
+ +
+
+
+
+ Prompt +
+
+ A serene mountain... +
+
+ +
+
+ + Steps + + + 25 + +
+
+
+
+
+ + +
+ +
+
+
+ Font +
+
+ {theme.fonts.main} +
+
+ +
+
+ + + +
+ Enabled +
+ + +
+
+
+
+
+ {#if theme.is_official} + + Official + + {:else if theme.likes > 0} + + + + + {theme.likes} + + {/if} +
+
+
+
+
+
+
+ + diff --git a/js/_website/src/routes/themes/gallery/ThemeDetailModal.svelte b/js/_website/src/routes/themes/gallery/ThemeDetailModal.svelte new file mode 100644 index 0000000..e3baad3 --- /dev/null +++ b/js/_website/src/routes/themes/gallery/ThemeDetailModal.svelte @@ -0,0 +1,301 @@ + + + e.key === "Escape" && on_close()} /> + + + +
+ +
+
+
+

+ {theme.name} +

+

+ by @{theme.author} + {#if theme.is_official} + + Official + + {/if} +

+
+
+ {#if !link_copied} + + {:else} + + Copied! + + {/if} + +
+
+ +
+ {#if has_preview} +
+
+ +
+
+ {/if} + +
+
+

+ Colors +

+
+ {#each [{ label: "Primary", color: theme.colors.primary }, { label: "Accent", color: theme.colors.secondary }, { label: "Neutral", color: theme.colors.neutral }, { label: "Background", color: theme.colors.background }, { label: "Background Dark", color: theme.colors.background_dark }] as { label, color }} +
+
+ + {color} + +
+ {label} +
+ {/each} +
+
+ +
+
+ Main Font +

+ {theme.fonts.main} +

+
+
+ Mono Font +

+ {theme.fonts.mono} +

+
+
+ +
+
+

+ Usage +

+ +
+
{usage_code}
+
+ + {#if !theme.is_official} + + {/if} +
+
+
+ + diff --git a/js/_website/src/routes/themes/gallery/types.ts b/js/_website/src/routes/themes/gallery/types.ts new file mode 100644 index 0000000..fa2aeac --- /dev/null +++ b/js/_website/src/routes/themes/gallery/types.ts @@ -0,0 +1,56 @@ +export type ThemeStatus = + | "RUNNING" + | "BUILDING" + | "BUILD_ERROR" + | "RUNTIME_ERROR" + | "PAUSED" + | "SLEEPING" + | "STOPPED" + | "NO_APP_FILE" + | "UNKNOWN"; + +export type ThemeData = { + id: string; + name: string; + author: string; + description: string; + is_official: boolean; + likes: number; + hf_space_id: string; + subdomain: string; + background_color: string; + status: ThemeStatus; + colors: { + primary: string; + secondary: string; + neutral: string; + background: string; + background_dark: string; + block_background: string; + block_border: string; + text_color: string; + button_primary: string; + button_secondary_border: string; + button_secondary_text: string; + }; + fonts: { + main: string; + mono: string; + }; + stylesheets?: string[]; +}; + +export type HfSpaceEntry = { + id: string; + likes: number; + subdomain: string; + runtime?: { + stage?: string; + }; + cardData?: { + title?: string; + short_description?: string; + colorFrom?: string; + colorTo?: string; + }; +}; diff --git a/js/_website/src/routes/themes/gallery/utils.ts b/js/_website/src/routes/themes/gallery/utils.ts new file mode 100644 index 0000000..4d9b73d --- /dev/null +++ b/js/_website/src/routes/themes/gallery/utils.ts @@ -0,0 +1,601 @@ +import type { ThemeData, ThemeStatus, HfSpaceEntry } from "./types"; + +const HF_API_URL = + "https://huggingface.co/api/spaces?filter=gradio-theme&limit=500&sort=lastModified&direction=-1&expand[]=subdomain&expand[]=likes&expand[]=runtime&expand[]=cardData"; + +const DEFAULT_COLORS = { + primary: "#3b82f6", + secondary: "#3b82f6", + neutral: "#71717a", + background: "#ffffff", + background_dark: "#0b0f19", + block_background: "#ffffff", + block_border: "#e4e4e7", + text_color: "#1f2937", + button_primary: "#3b82f6", + button_secondary_border: "#e4e4e7", + button_secondary_text: "#71717a" +}; + +const DEFAULT_FONTS = { + main: "IBM Plex Sans", + mono: "IBM Plex Mono" +}; + +function format_theme_name(id: string): string { + const name = id.split("/").pop() || id; + return name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); +} + +const CSS_NAMED_COLORS: Record = { + black: "#000000", + white: "#ffffff", + red: "#ff0000", + green: "#008000", + blue: "#0000ff", + yellow: "#ffff00", + cyan: "#00ffff", + magenta: "#ff00ff", + purple: "#800080", + orange: "#ffa500", + pink: "#ffc0cb", + gray: "#808080", + grey: "#808080", + teal: "#008080", + navy: "#000080", + maroon: "#800000", + lime: "#00ff00", + aqua: "#00ffff", + silver: "#c0c0c0", + olive: "#808000", + fuchsia: "#ff00ff", + indigo: "#4b0082", + violet: "#ee82ee", + coral: "#ff7f50", + salmon: "#fa8072", + tomato: "#ff6347", + gold: "#ffd700", + khaki: "#f0e68c", + plum: "#dda0dd", + orchid: "#da70d6", + tan: "#d2b48c", + crimson: "#dc143c", + turquoise: "#40e0d0" +}; + +const HEX_6_RE = /^#[0-9a-fA-F]{6}$/; +const HEX_SHORT_RE = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/; +const HEX_ANY_RE = /^#[0-9a-fA-F]{3,8}$/; +const HEX_IN_VALUE_RE = /#[0-9a-fA-F]{3,8}/; +const VAR_REF_RE = /^var\(--([\w-]+)\)$/; +const CSS_VAR_DECL_RE = /--([\w-]+):\s*([^;]+);/g; +const MAX_VAR_RESOLVE_DEPTH = 10; + +function normalize_hex(hex: string): string { + const short = HEX_SHORT_RE.exec(hex); + if (short) { + return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`; + } + return hex; +} + +function extract_hex_color(value: string | undefined): string | undefined { + if (!value) return undefined; + const trimmed = value.trim(); + if (HEX_ANY_RE.test(trimmed)) return normalize_hex(trimmed); + const named = CSS_NAMED_COLORS[trimmed.toLowerCase()]; + if (named) return named; + const hex_match = trimmed.match(HEX_IN_VALUE_RE); + if (hex_match) return normalize_hex(hex_match[0]); + return undefined; +} + +export function hex_to_rgb( + hex: string +): { r: number; g: number; b: number } | null { + const normalized = normalize_hex(hex); + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(normalized); + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } + : null; +} + +export function is_color_dark(hex: string): boolean { + const rgb = hex_to_rgb(hex); + if (!rgb) return false; + const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255; + return luminance < 0.5; +} + +function parse_css_vars(css: string): Record { + const vars: Record = {}; + let match; + CSS_VAR_DECL_RE.lastIndex = 0; + while ((match = CSS_VAR_DECL_RE.exec(css)) !== null) { + vars[match[1]] = match[2].trim(); + } + return vars; +} + +function resolve_css_var( + vars: Record, + name: string, + depth = 0 +): string | undefined { + const value = vars[name]; + if (!value || depth > MAX_VAR_RESOLVE_DEPTH) return undefined; + const ref = value.match(VAR_REF_RE); + if (ref && ref[1]) { + return resolve_css_var(vars, ref[1], depth + 1); + } + return value; +} + +const FETCH_TIMEOUT_MS = 5000; + +type SpaceConfig = { + body_css?: { + body_background_fill?: string; + body_text_color?: string; + body_background_fill_dark?: string; + body_text_color_dark?: string; + }; +}; + +function first_font_family(raw: string | undefined): string | undefined { + return raw + ?.split(",")[0] + ?.trim() + ?.replace(/^['"]|['"]$/g, ""); +} + +function fetch_with_timeout( + url: string, + fetcher: typeof fetch +): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + return fetcher(url, { + signal: controller.signal, + referrerPolicy: "no-referrer" + }).finally(() => clearTimeout(timeout)); +} + +async function fetch_theme_details( + subdomain: string, + fetcher: typeof fetch +): Promise<{ + colors: ThemeData["colors"]; + fonts: ThemeData["fonts"]; +}> { + const fallback = { colors: DEFAULT_COLORS, fonts: DEFAULT_FONTS }; + try { + const [css_res, config_res] = await Promise.all([ + fetch_with_timeout(`${subdomain}/theme.css`, fetcher), + fetch_with_timeout(`${subdomain}/config`, fetcher) + ]); + + if (!css_res.ok) return fallback; + const css = await css_res.text(); + const vars = parse_css_vars(css); + + // /config provides pre-resolved background colors; /theme.css may have unresolved var() chains + let body_css: SpaceConfig["body_css"] = undefined; + if (config_res.ok) { + try { + const config: SpaceConfig = await config_res.json(); + body_css = config.body_css; + } catch { + // ignore parse errors + } + } + + const resolved_accent = extract_hex_color( + resolve_css_var(vars, "color-accent") + ); + + const primary = + extract_hex_color(resolve_css_var(vars, "primary-500")) ?? + resolved_accent ?? + DEFAULT_COLORS.primary; + + const accent = resolved_accent ?? primary; + + const neutral = + extract_hex_color(resolve_css_var(vars, "neutral-500")) ?? + DEFAULT_COLORS.neutral; + + const background = + extract_hex_color(body_css?.body_background_fill) ?? + extract_hex_color(resolve_css_var(vars, "body-background-fill")) ?? + DEFAULT_COLORS.background; + + const background_dark = + extract_hex_color(body_css?.body_background_fill_dark) ?? + extract_hex_color(resolve_css_var(vars, "body-background-fill-dark")) ?? + DEFAULT_COLORS.background_dark; + + const block_background = + extract_hex_color(resolve_css_var(vars, "block-background-fill")) ?? + DEFAULT_COLORS.block_background; + + const block_border = + extract_hex_color(resolve_css_var(vars, "block-border-color")) ?? + DEFAULT_COLORS.block_border; + + const text_color = + extract_hex_color(resolve_css_var(vars, "body-text-color")) ?? + DEFAULT_COLORS.text_color; + + const button_primary = + extract_hex_color( + resolve_css_var(vars, "button-primary-background-fill") + ) ?? primary; + + const button_secondary_border = + extract_hex_color( + resolve_css_var(vars, "button-secondary-border-color") + ) ?? neutral; + + const button_secondary_text = + extract_hex_color(resolve_css_var(vars, "button-secondary-text-color")) ?? + neutral; + + return { + colors: { + primary, + secondary: accent, + neutral, + background, + background_dark, + block_background, + block_border, + text_color, + button_primary, + button_secondary_border, + button_secondary_text + }, + fonts: { + main: + first_font_family(resolve_css_var(vars, "font")) || + DEFAULT_FONTS.main, + mono: + first_font_family(resolve_css_var(vars, "font-mono")) || + DEFAULT_FONTS.mono + } + }; + } catch { + return fallback; + } +} + +export async function fetch_community_themes( + fetcher: typeof fetch = fetch +): Promise { + try { + const res = await fetcher(HF_API_URL, { referrerPolicy: "no-referrer" }); + if (!res.ok) return []; + const spaces: HfSpaceEntry[] = await res.json(); + + const running_spaces = spaces.filter( + (space) => space.runtime?.stage === "RUNNING" + ); + + const results = await Promise.allSettled( + running_spaces.map(async (space) => { + const subdomain = `https://${space.subdomain}.hf.space`; + const details = await fetch_theme_details(subdomain, fetcher); + + return { + id: space.id, + name: space.cardData?.title || format_theme_name(space.id), + author: space.id.split("/")[0], + description: space.cardData?.short_description || "", + is_official: false, + likes: space.likes, + hf_space_id: space.id, + subdomain, + background_color: "", + status: "RUNNING" as ThemeStatus, + colors: details.colors, + fonts: details.fonts + }; + }) + ); + + return results + .filter( + (r): r is PromiseFulfilledResult => r.status === "fulfilled" + ) + .map((r) => r.value); + } catch { + return []; + } +} + +export const COLOR_SETS = [ + "from-red-50 via-red-100 to-red-50 dark:from-red-950 dark:via-red-900 dark:to-red-950", + "from-green-50 via-green-100 to-green-50 dark:from-green-950 dark:via-green-900 dark:to-green-950", + "from-yellow-50 via-yellow-100 to-yellow-50 dark:from-yellow-950 dark:via-yellow-900 dark:to-yellow-950", + "from-pink-50 via-pink-100 to-pink-50 dark:from-pink-950 dark:via-pink-900 dark:to-pink-950", + "from-blue-50 via-blue-100 to-blue-50 dark:from-blue-950 dark:via-blue-900 dark:to-blue-950", + "from-purple-50 via-purple-100 to-purple-50 dark:from-purple-950 dark:via-purple-900 dark:to-purple-950" +]; + +export const BUILTIN_THEMES: ThemeData[] = [ + { + id: "base", + name: "Base", + author: "Gradio", + description: + "The foundation theme with blue accents. A clean, minimal starting point.", + is_official: true, + likes: 0, + hf_space_id: "", + subdomain: "", + background_color: "", + status: "RUNNING", + colors: { + primary: "#3b82f6", + secondary: "#3b82f6", + neutral: "#71717a", + background: "#ffffff", + background_dark: "#0b0f19", + block_background: "#ffffff", + block_border: "#e4e4e7", + text_color: "#1f2937", + button_primary: "#3b82f6", + button_secondary_border: "#e4e4e7", + button_secondary_text: "#71717a" + }, + fonts: { + main: "IBM Plex Sans", + mono: "IBM Plex Mono" + } + }, + { + id: "default", + name: "Default", + author: "Gradio", + description: "The standard Gradio theme with warm orange accents.", + is_official: true, + likes: 0, + hf_space_id: "", + subdomain: "", + background_color: "", + status: "RUNNING", + colors: { + primary: "#f97316", + secondary: "#3b82f6", + neutral: "#71717a", + background: "#ffffff", + background_dark: "#0b0f19", + block_background: "#ffffff", + block_border: "#e4e4e7", + text_color: "#1f2937", + button_primary: "#f97316", + button_secondary_border: "#e4e4e7", + button_secondary_text: "#71717a" + }, + fonts: { + main: "Source Sans Pro", + mono: "IBM Plex Mono" + } + }, + { + id: "soft", + name: "Soft", + author: "Gradio", + description: "A soft, rounded theme with indigo accents and gentle curves.", + is_official: true, + likes: 0, + hf_space_id: "", + subdomain: "", + background_color: "", + status: "RUNNING", + colors: { + primary: "#6366f1", + secondary: "#6366f1", + neutral: "#6b7280", + background: "#ffffff", + background_dark: "#0b0f19", + block_background: "#ffffff", + block_border: "#e4e4e7", + text_color: "#1f2937", + button_primary: "#6366f1", + button_secondary_border: "#e4e4e7", + button_secondary_text: "#6b7280" + }, + fonts: { + main: "Montserrat", + mono: "IBM Plex Mono" + } + }, + { + id: "monochrome", + name: "Monochrome", + author: "Gradio", + description: "A sleek black and white theme for a professional look.", + is_official: true, + likes: 0, + hf_space_id: "", + subdomain: "", + background_color: "", + status: "RUNNING", + colors: { + primary: "#171717", + secondary: "#737373", + neutral: "#737373", + background: "#ffffff", + background_dark: "#0f0f0f", + block_background: "#ffffff", + block_border: "#e4e4e7", + text_color: "#1f2937", + button_primary: "#171717", + button_secondary_border: "#e4e4e7", + button_secondary_text: "#737373" + }, + fonts: { + main: "Lora", + mono: "IBM Plex Mono" + } + }, + { + id: "glass", + name: "Glass", + author: "Gradio", + description: "A modern glassmorphic theme with translucent elements.", + is_official: true, + likes: 0, + hf_space_id: "", + subdomain: "", + background_color: "", + status: "RUNNING", + colors: { + primary: "#3b82f6", + secondary: "#64748b", + neutral: "#64748b", + background: "#ffffff", + background_dark: "#1e293b", + block_background: "#ffffff", + block_border: "#e4e4e7", + text_color: "#1f2937", + button_primary: "#3b82f6", + button_secondary_border: "#e4e4e7", + button_secondary_text: "#64748b" + }, + fonts: { + main: "Optima", + mono: "IBM Plex Mono" + } + }, + { + id: "origin", + name: "Origin", + author: "Gradio", + description: + "The classic Gradio 3.x style theme for a familiar experience.", + is_official: true, + likes: 0, + hf_space_id: "", + subdomain: "", + background_color: "", + status: "RUNNING", + colors: { + primary: "#f97316", + secondary: "#3b82f6", + neutral: "#6b7280", + background: "#ffffff", + background_dark: "#0b0f19", + block_background: "#ffffff", + block_border: "#e4e4e7", + text_color: "#1f2937", + button_primary: "#f97316", + button_secondary_border: "#e4e4e7", + button_secondary_text: "#6b7280" + }, + fonts: { + main: "Source Sans Pro", + mono: "IBM Plex Mono" + } + }, + { + id: "citrus", + name: "Citrus", + author: "Gradio", + description: "A bright, energetic theme with fresh lime green accents.", + is_official: true, + likes: 0, + hf_space_id: "", + subdomain: "", + background_color: "", + status: "RUNNING", + colors: { + primary: "#84cc16", + secondary: "#f59e0b", + neutral: "#78716c", + background: "#ffffff", + background_dark: "#0b0f19", + block_background: "#ffffff", + block_border: "#e4e4e7", + text_color: "#1f2937", + button_primary: "#84cc16", + button_secondary_border: "#e4e4e7", + button_secondary_text: "#78716c" + }, + fonts: { + main: "Ubuntu", + mono: "Roboto Mono" + } + }, + { + id: "ocean", + name: "Ocean", + author: "Gradio", + description: "A calming theme inspired by ocean blues and cyans.", + is_official: true, + likes: 0, + hf_space_id: "", + subdomain: "", + background_color: "", + status: "RUNNING", + colors: { + primary: "#06b6d4", + secondary: "#0ea5e9", + neutral: "#71717a", + background: "#ffffff", + background_dark: "#0b0f19", + block_background: "#ffffff", + block_border: "#e4e4e7", + text_color: "#1f2937", + button_primary: "#06b6d4", + button_secondary_border: "#e4e4e7", + button_secondary_text: "#71717a" + }, + fonts: { + main: "IBM Plex Sans", + mono: "IBM Plex Mono" + } + } +]; + +export function clickOutside( + element: HTMLDivElement, + callbackFunction: () => void +) { + function onClick(event: MouseEvent) { + const target = event.target as HTMLElement; + if ( + !element.contains(target) && + !(target.textContent && target.textContent === "Share") + ) { + callbackFunction(); + } + } + + document.body.addEventListener("click", onClick); + + return { + update(newCallbackFunction: () => void) { + callbackFunction = newCallbackFunction; + }, + destroy() { + document.body.removeEventListener("click", onClick); + } + }; +} + +export function assignColors(themes: ThemeData[]): void { + let counter = 0; + for (const theme of themes) { + if (counter >= COLOR_SETS.length) { + counter = 0; + } + theme.background_color = COLOR_SETS[counter]; + counter++; + } +} diff --git a/js/_website/src/routes/version.json b/js/_website/src/routes/version.json new file mode 100644 index 0000000..1c07274 --- /dev/null +++ b/js/_website/src/routes/version.json @@ -0,0 +1 @@ +{ "version": "3.39.0" } diff --git a/js/_website/src/routes/workflow/+page.svelte b/js/_website/src/routes/workflow/+page.svelte new file mode 100644 index 0000000..ec9e52b --- /dev/null +++ b/js/_website/src/routes/workflow/+page.svelte @@ -0,0 +1,29 @@ + + + + +
+ +
diff --git a/js/_website/static/assets/gradio.svg b/js/_website/static/assets/gradio.svg new file mode 100644 index 0000000..ff8df41 --- /dev/null +++ b/js/_website/static/assets/gradio.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/js/_website/static/dog.jpg b/js/_website/static/dog.jpg new file mode 100644 index 0000000..d298d41 Binary files /dev/null and b/js/_website/static/dog.jpg differ diff --git a/js/_website/static/dog_blurred.jpg b/js/_website/static/dog_blurred.jpg new file mode 100644 index 0000000..6b8befc Binary files /dev/null and b/js/_website/static/dog_blurred.jpg differ diff --git a/js/_website/static/favicon.png b/js/_website/static/favicon.png new file mode 100644 index 0000000..6f1ad28 Binary files /dev/null and b/js/_website/static/favicon.png differ diff --git a/js/_website/static/fonts/IBMPlexMono/IBMPlexMono-Bold.woff2 b/js/_website/static/fonts/IBMPlexMono/IBMPlexMono-Bold.woff2 new file mode 100644 index 0000000..add42af Binary files /dev/null and b/js/_website/static/fonts/IBMPlexMono/IBMPlexMono-Bold.woff2 differ diff --git a/js/_website/static/fonts/IBMPlexMono/IBMPlexMono-Regular.woff2 b/js/_website/static/fonts/IBMPlexMono/IBMPlexMono-Regular.woff2 new file mode 100644 index 0000000..d0d7ded Binary files /dev/null and b/js/_website/static/fonts/IBMPlexMono/IBMPlexMono-Regular.woff2 differ diff --git a/js/_website/svelte.config.js b/js/_website/svelte.config.js new file mode 100644 index 0000000..dce0866 --- /dev/null +++ b/js/_website/svelte.config.js @@ -0,0 +1,143 @@ +import adapter from "@sveltejs/adapter-static"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import { redirects } from "./src/routes/redirects.js"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { mdsvex, code_highlighter } from "mdsvex"; +import slugify from "@sindresorhus/slugify"; +import { toString as to_string } from "hast-util-to-string"; + +const currentDir = path.dirname(fileURLToPath(import.meta.url)); +let version = "4.0.0"; + +const get_version = async () => { + try { + const versionPath = path.join( + currentDir, + "src", + "lib", + "json", + "version.json" + ); + if (!fs.existsSync(versionPath)) { + console.error( + "Using fallback version 4.0.0 as version.json was not found. Run `generate_jsons/generate.py` to get the latest version." + ); + return version; + } else { + const versionData = fs.readFileSync(versionPath, "utf8"); + const versionJson = JSON.parse(versionData); + version = versionJson.version; + } + } catch (error) { + console.error( + "Using fallback version 4.0.0 as version.json was not found. Run `generate_jsons/generate.py` to get the latest version." + ); + } + return version; +}; + +get_version(); + +export function make_slug_processor() { + return function (name) { + const slug = slugify(name, { separator: "-", lowercase: true }); + return slug; + }; +} +const doc_slug = []; +function plugin() { + const get_slug = make_slug_processor(); + return function transform(tree) { + tree.children.forEach((n) => { + if (n.type === "element" && ["h3"].includes(n.tagName)) { + const str_of_heading = to_string(n); + const slug = get_slug(str_of_heading); + + doc_slug.push({ + text: str_of_heading, + href: `#${slug}`, + level: parseInt(n.tagName.replace("h", "")) + }); + + if (!n.children) n.children = []; + n.properties.className = ["group", "header-tag"]; + n.properties.id = [slug]; + n.children.push({ + type: "element", + tagName: "a", + properties: { + href: `#${slug}`, + className: ["invisible", "group-hover-visible"] + }, + children: [ + { + type: "element", + tagName: "img", + properties: { + src: "https://raw.githubusercontent.com/gradio-app/gradio/main/js/_website/src/lib/assets/img/anchor.svg", + className: ["anchor-img-small"] + }, + children: [] + } + ] + }); + } + }); + }; +} + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + extensions: [".svelte", ".svx"], + preprocess: [ + mdsvex({ + extensions: [".svx"], + rehypePlugins: [plugin], + highlight: { + highlighter: async (code, lang) => { + const h = (await code_highlighter(code, lang, "")).replace( + /\{@html `|`\}/g, + "" + ); + return `
${h}
`; + } + } + }), + vitePreprocess() + ], + kit: { + prerender: { + crawl: true, + entries: [ + "*", + "/workflow", + `/${version}/docs`, + `/${version}/guides`, + `/main/docs`, + `/main/guides`, + `/docs/js`, + `/docs/js/storybook`, + `/docs/js/`, + `/main/docs/js`, + `/main/docs/js/storybook`, + `/main/docs/js/`, + ...Object.keys(redirects), + `/4.44.1/docs`, + `/4.44.1/guides`, + `/5.49.1/docs`, + `/5.49.1/guides` + ], + handleMissingId: "warn" + }, + adapter: adapter({ + fallback: "404.html" + }), + paths: { + relative: false + } + } +}; + +export default config; diff --git a/js/_website/tailwind.config.cjs b/js/_website/tailwind.config.cjs new file mode 100644 index 0000000..b975c6a --- /dev/null +++ b/js/_website/tailwind.config.cjs @@ -0,0 +1,42 @@ +const defaultTheme = require("tailwindcss/defaultTheme"); + +module.exports = { + content: [ + "./src/*.{html,js,css}", + "./src/**/*.{html,js,svelte,ts,css}", + "**/@gradio/**/*.{html,js,svelte,ts,css}" + ], + + theme: { + extend: { + fontFamily: { + sans: ["IBM Plex Sans", ...defaultTheme.fontFamily.sans], + mono: ["IBM Plex Mono", ...defaultTheme.fontFamily.mono] + }, + colors: { + orange: { + 50: "#FFF2E5", + 100: "#FFE5CC", + 200: "#FFD8B4", + 300: "#FFB066", + 400: "#FF9633", + 500: "#FF7C00", + 600: "#EE7400", + 700: "#CE6400", + 800: "#A45000", + 900: "#5C2D00" + } + } + } + }, + mode: "jit", + darkMode: "class", // or 'media' or 'class' + + variants: { + extend: { + visibility: ["group-hover"] + } + }, + + plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")] +}; diff --git a/js/_website/tsconfig.json b/js/_website/tsconfig.json new file mode 100644 index 0000000..2efa6ab --- /dev/null +++ b/js/_website/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "typeRoots": ["node_modules/@types", "./app.d.ts"], + "moduleResolution": "bundler", + "module": "esnext", + "customConditions": ["gradio"], + "target": "es2022" + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/js/_website/vercel.json b/js/_website/vercel.json new file mode 100644 index 0000000..e85d6c0 --- /dev/null +++ b/js/_website/vercel.json @@ -0,0 +1,3 @@ +{ + "trailingSlash": false +} diff --git a/js/_website/vite-plugin-patch-paramviewer.ts b/js/_website/vite-plugin-patch-paramviewer.ts new file mode 100644 index 0000000..7b4a4e3 --- /dev/null +++ b/js/_website/vite-plugin-patch-paramviewer.ts @@ -0,0 +1,15 @@ +export function patchParamViewer() { + return { + name: "patch-paramviewer", + transform(code: string, id: string) { + // Only patch the ParamViewer component + if (id.includes("paramviewer") && id.includes("ParamViewer.svelte")) { + // Replace Prism.highlight call to check if language exists first + return code.replace( + /Prism\.highlight\(([^,]+),\s*Prism\.languages\[([^\]]+)\],\s*([^)]+)\)/g, + "(Prism.languages[$2] ? Prism.highlight($1, Prism.languages[$2], $3) : $1)" + ); + } + } + }; +} diff --git a/js/_website/vite.config.ts b/js/_website/vite.config.ts new file mode 100644 index 0000000..e2479c2 --- /dev/null +++ b/js/_website/vite.config.ts @@ -0,0 +1,17 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; +import { inject_component_loader } from "../build/out/index.js"; +import { patchParamViewer } from "./vite-plugin-patch-paramviewer.js"; + +//@ts-ignore +export default defineConfig(({ mode }) => ({ + plugins: [patchParamViewer(), sveltekit(), inject_component_loader({ mode })], + resolve: { + conditions: ["gradio"] + }, + build: { + rollupOptions: { + external: ["@gradio/wasm/svelte", "dompurify", "ts-dedent"] + } + } +})); diff --git a/js/_website/worker/index.ts b/js/_website/worker/index.ts new file mode 100644 index 0000000..6ab1cb4 --- /dev/null +++ b/js/_website/worker/index.ts @@ -0,0 +1,66 @@ +const AI_UA_PATTERNS: RegExp[] = [ + /\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: Request): boolean { + 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)); +} + +const DOC_PREFIXES = ["/docs/gradio/", "/main/docs/gradio/"]; +const GUIDE_PREFIXES = ["/guides/", "/main/guides/"]; + +function matchSingleSegment( + pathname: string, + prefixes: readonly string[] +): string | null { + for (const prefix of prefixes) { + if (pathname.startsWith(prefix)) { + const rest = pathname.slice(prefix.length).replace(/\/$/, ""); + if (rest && !rest.includes("/")) return rest; + } + } + return null; +} + +interface Env { + ASSETS: Fetcher; +} + +export default { + async fetch(request: Request, env: Env): Promise { + if (request.method === "GET" && isLLMRequest(request)) { + const url = new URL(request.url); + + const doc = matchSingleSegment(url.pathname, DOC_PREFIXES); + if (doc) { + return Response.redirect(`${url.origin}/api/markdown/${doc}`, 302); + } + + const guide = matchSingleSegment(url.pathname, GUIDE_PREFIXES); + if (guide) { + return Response.redirect( + `${url.origin}/api/markdown/guide/${guide}`, + 302 + ); + } + } + return env.ASSETS.fetch(request); + } +} satisfies ExportedHandler; diff --git a/js/_website/wrangler.jsonc b/js/_website/wrangler.jsonc new file mode 100644 index 0000000..6e9287b --- /dev/null +++ b/js/_website/wrangler.jsonc @@ -0,0 +1,18 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "gradio-site", + "main": "worker/index.ts", + "compatibility_date": "2026-04-22", + "preview_urls": true, + "assets": { + "directory": "./build", + "binding": "ASSETS", + "not_found_handling": "404-page", + "run_worker_first": [ + "/docs/gradio/*", + "/guides/*", + "/main/docs/gradio/*", + "/main/guides/*" + ] + } +} diff --git a/js/accordion/Accordion.stories.svelte b/js/accordion/Accordion.stories.svelte new file mode 100644 index 0000000..9d74439 --- /dev/null +++ b/js/accordion/Accordion.stories.svelte @@ -0,0 +1,47 @@ + + + + {#snippet template(args)} + + + + {/snippet} + + + + {#snippet template(args)} + + + + + {/snippet} + diff --git a/js/accordion/Accordion.test.ts b/js/accordion/Accordion.test.ts new file mode 100644 index 0000000..656efde --- /dev/null +++ b/js/accordion/Accordion.test.ts @@ -0,0 +1,363 @@ +import { test, describe, afterEach, expect } from "vitest"; +import { cleanup, render, fireEvent } from "@self/tootils/render"; +import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests"; + +import Accordion from "./Index.svelte"; + +const base_props = { + label: "Test Accordion", + open: true +}; + +// Accordion has no standard label (it renders its own label inside a button) +// and no validation_error support. visible=false maps to "hidden" rather than +// removing from the DOM (see Index.svelte visibility derived). +run_shared_prop_tests({ + component: Accordion, + name: "Accordion", + base_props, + has_label: false, + has_validation_error: false, + visible_false_hides: true +}); + +// The Block wrapper applies a "hidden" CSS class which makes getByRole unable +// to find elements without { hidden: true }. We use this helper throughout. +const hidden = { hidden: true } as const; + +// Block's overflow:hidden in the test environment prevents toBeVisible() from +// working on nested elements. We check the content div's display style via +// getByTestId instead — this is the mechanism Accordion uses to show/hide. +function expectContentOpen(getByTestId: Function): void { + expect( + (getByTestId("accordion-content") as HTMLElement).style.display + ).not.toBe("none"); +} +function expectContentClosed(getByTestId: Function): void { + expect((getByTestId("accordion-content") as HTMLElement).style.display).toBe( + "none" + ); +} + +describe("Accordion", () => { + afterEach(() => cleanup()); + + test("renders the label text in the toggle button", async () => { + const { getByRole } = await render(Accordion, { + ...base_props, + label: "My Section" + }); + + const button = getByRole("button", { ...hidden, name: /My Section/ }); + expect(button).toBeTruthy(); + }); + + test("renders with content visible when open=true", async () => { + const { getByTestId } = await render(Accordion, { + ...base_props, + open: true + }); + + expectContentOpen(getByTestId); + }); + + test("renders with content hidden when open=false", async () => { + const { getByTestId } = await render(Accordion, { + ...base_props, + open: false + }); + + expectContentClosed(getByTestId); + }); + + test("clicking the button toggles content visibility", async () => { + const { getByRole, getByTestId } = await render(Accordion, { + ...base_props, + open: true + }); + + const button = getByRole("button", { + ...hidden, + name: /Test Accordion/ + }); + + expectContentOpen(getByTestId); + + await fireEvent.click(button); + expectContentClosed(getByTestId); + }); + + test("clicking the button twice returns to original state", async () => { + const { getByRole, getByTestId } = await render(Accordion, { + ...base_props, + open: true + }); + + const button = getByRole("button", { + ...hidden, + name: /Test Accordion/ + }); + + await fireEvent.click(button); + expectContentClosed(getByTestId); + + await fireEvent.click(button); + expectContentOpen(getByTestId); + }); +}); + +describe("Events", () => { + afterEach(() => cleanup()); + + test("expand event fires when clicking to open", async () => { + const { getByRole, listen } = await render(Accordion, { + ...base_props, + open: false + }); + + const expand = listen("expand"); + const button = getByRole("button", { + ...hidden, + name: /Test Accordion/ + }); + + await fireEvent.click(button); + expect(expand).toHaveBeenCalledTimes(1); + }); + + test("collapse event fires when clicking to close", async () => { + const { getByRole, listen } = await render(Accordion, { + ...base_props, + open: true + }); + + const collapse = listen("collapse"); + const button = getByRole("button", { + ...hidden, + name: /Test Accordion/ + }); + + await fireEvent.click(button); + expect(collapse).toHaveBeenCalledTimes(1); + }); + + test("gradio_expand event fires when clicking to open", async () => { + const { getByRole, listen } = await render(Accordion, { + ...base_props, + open: false + }); + + const gradio_expand = listen("gradio_expand"); + const button = getByRole("button", { + ...hidden, + name: /Test Accordion/ + }); + + await fireEvent.click(button); + expect(gradio_expand).toHaveBeenCalledTimes(1); + }); + + test("no expand/collapse events fire on mount", async () => { + const { listen } = await render(Accordion, { + ...base_props, + open: true + }); + + const expand = listen("expand", { retrospective: true }); + const collapse = listen("collapse", { retrospective: true }); + const gradio_expand = listen("gradio_expand", { retrospective: true }); + + expect(expand).not.toHaveBeenCalled(); + expect(collapse).not.toHaveBeenCalled(); + expect(gradio_expand).not.toHaveBeenCalled(); + }); + + test("expand and collapse fire alternately on repeated toggles", async () => { + const { getByRole, listen } = await render(Accordion, { + ...base_props, + open: false + }); + + const expand = listen("expand"); + const collapse = listen("collapse"); + const button = getByRole("button", { + ...hidden, + name: /Test Accordion/ + }); + + await fireEvent.click(button); // open + expect(expand).toHaveBeenCalledTimes(1); + expect(collapse).toHaveBeenCalledTimes(0); + + await fireEvent.click(button); // close + expect(expand).toHaveBeenCalledTimes(1); + expect(collapse).toHaveBeenCalledTimes(1); + + await fireEvent.click(button); // open + expect(expand).toHaveBeenCalledTimes(2); + expect(collapse).toHaveBeenCalledTimes(1); + }); +}); + +describe("get_data / set_data", () => { + afterEach(() => cleanup()); + + test("set_data({ open: true }) opens the accordion", async () => { + const { getByTestId, set_data } = await render(Accordion, { + ...base_props, + open: false + }); + + expectContentClosed(getByTestId); + + await set_data({ open: true }); + expectContentOpen(getByTestId); + }); + + test("set_data({ open: false }) closes the accordion", async () => { + const { getByTestId, set_data } = await render(Accordion, { + ...base_props, + open: true + }); + + expectContentOpen(getByTestId); + + await set_data({ open: false }); + expectContentClosed(getByTestId); + }); + + test("set_data({ open: true }) dispatches expand and gradio_expand", async () => { + const { listen, set_data } = await render(Accordion, { + ...base_props, + open: false + }); + + const gradio_expand = listen("gradio_expand"); + const expand = listen("expand"); + + await set_data({ open: true }); + expect(gradio_expand).toHaveBeenCalledTimes(1); + expect(expand).toHaveBeenCalledTimes(1); + }); + + test("set_data({ open: false }) dispatches collapse", async () => { + const { listen, set_data } = await render(Accordion, { + ...base_props, + open: true + }); + + const collapse = listen("collapse"); + const expand = listen("expand"); + const gradio_expand = listen("gradio_expand"); + + await set_data({ open: false }); + expect(collapse).toHaveBeenCalledTimes(1); + expect(expand).not.toHaveBeenCalled(); + expect(gradio_expand).not.toHaveBeenCalled(); + }); + + test("set_data({ open: true }) with already-open does not dispatch events", async () => { + const { listen, set_data } = await render(Accordion, { + ...base_props, + open: true + }); + + const expand = listen("expand"); + const collapse = listen("collapse"); + const gradio_expand = listen("gradio_expand"); + + await set_data({ open: true }); + expect(expand).not.toHaveBeenCalled(); + expect(collapse).not.toHaveBeenCalled(); + expect(gradio_expand).not.toHaveBeenCalled(); + }); + + test("set_data({ open: false }) with already-closed does not dispatch events", async () => { + const { listen, set_data } = await render(Accordion, { + ...base_props, + open: false + }); + + const expand = listen("expand"); + const collapse = listen("collapse"); + const gradio_expand = listen("gradio_expand"); + + await set_data({ open: false }); + expect(expand).not.toHaveBeenCalled(); + expect(collapse).not.toHaveBeenCalled(); + expect(gradio_expand).not.toHaveBeenCalled(); + }); + + test("set_data with label updates the button text", async () => { + const { getByRole, set_data } = await render(Accordion, { + ...base_props, + label: "Original" + }); + + expect(getByRole("button", { ...hidden, name: /Original/ })).toBeTruthy(); + + await set_data({ label: "Updated" }); + expect(getByRole("button", { ...hidden, name: /Updated/ })).toBeTruthy(); + }); + + test("get_data returns current open state", async () => { + const { get_data } = await render(Accordion, { + ...base_props, + open: true + }); + + const data = await get_data(); + expect(data).toHaveProperty("open", true); + }); + + test("round-trip: set_data then get_data preserves open state", async () => { + const { set_data, get_data } = await render(Accordion, { + ...base_props, + open: true + }); + + await set_data({ open: false }); + expect(await get_data()).toHaveProperty("open", false); + + await set_data({ open: true }); + expect(await get_data()).toHaveProperty("open", true); + }); +}); + +describe("Edge cases", () => { + afterEach(() => cleanup()); + + test("set_data with same open value does not change DOM state", async () => { + const { getByTestId, set_data } = await render(Accordion, { + ...base_props, + open: true + }); + + expectContentOpen(getByTestId); + + await set_data({ open: true }); + expectContentOpen(getByTestId); + }); + + test("empty label renders the button with no label text", async () => { + const { getByRole } = await render(Accordion, { + ...base_props, + label: "" + }); + + const button = getByRole("button", hidden); + expect(button).toBeTruthy(); + // The label span should be empty; only the icon span has content + const spans = button.querySelectorAll("span"); + const label_span = spans[0]; + expect(label_span?.textContent?.trim()).toBe(""); + }); +}); + +test.todo( + "VISUAL: icon rotates (transform: rotate) when open state changes — needs Playwright visual regression screenshot comparison" +); + +test.todo( + "VISUAL: open state adds margin-bottom below the label button — needs Playwright visual regression screenshot comparison" +); diff --git a/js/accordion/CHANGELOG.md b/js/accordion/CHANGELOG.md new file mode 100644 index 0000000..2be8f87 --- /dev/null +++ b/js/accordion/CHANGELOG.md @@ -0,0 +1,831 @@ +# @gradio/accordion + +## 0.5.39 + +### Dependency updates + +- @gradio/statustracker@0.15.1 +- @gradio/atoms@0.26.0 + +## 0.5.38 + +### Dependency updates + +- @gradio/atoms@0.25.0 +- @gradio/statustracker@0.15.0 +- @gradio/utils@0.13.0 +- @gradio/column@0.4.0 + +## 0.5.37 + +### Fixes + +- [#13523](https://github.com/gradio-app/gradio/pull/13523) [`1b611f8`](https://github.com/gradio-app/gradio/commit/1b611f89fb85016a7879020098bd0faedf80ae5e) - Fixes grouped accordion borders and empty plot legends. Thanks @dawoodkhan82! + +## 0.5.36 + +### Dependency updates + +- @gradio/atoms@0.24.0 +- @gradio/statustracker@0.14.1 + +## 0.5.35 + +### Dependency updates + +- @gradio/atoms@0.23.1 +- @gradio/statustracker@0.14.0 +- @gradio/column@0.3.3 + +## 0.5.34 + +### Fixes + +- [#13183](https://github.com/gradio-app/gradio/pull/13183) [`1bf9bae`](https://github.com/gradio-app/gradio/commit/1bf9bae723475ad664f7d3d32596856c4881e63d) - Ensure the Accordion's `expand` and `collapse` events fire when toggled from python. Thanks @pngwn! + +### Dependency updates + +- @gradio/utils@0.12.2 +- @gradio/atoms@0.23.0 +- @gradio/statustracker@0.13.1 + +## 0.5.33 + +### Dependency updates + +- @gradio/utils@0.12.1 +- @gradio/statustracker@0.13.0 +- @gradio/column@0.3.2 + +## 0.5.32 + +### Fixes + +- [#12836](https://github.com/gradio-app/gradio/pull/12836) [`77e7871`](https://github.com/gradio-app/gradio/commit/77e7871176e50a894190ac98aa9de8fbdbf3620f) - Fixes hidden accordion's children losing all values. Thanks @aliabid94! + +### Dependency updates + +- @gradio/statustracker@0.12.5 +- @gradio/utils@0.12.0 +- @gradio/column@0.3.2 +- @gradio/atoms@0.22.2 + +## 0.5.31 + +### Fixes + +- [#12846](https://github.com/gradio-app/gradio/pull/12846) [`226daba`](https://github.com/gradio-app/gradio/commit/226daba5f65257244efc7c310500ea5366b20a87) - Fix bug where children of accordions dont get rendered when they are opened programmatically. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/utils@0.11.3 +- @gradio/atoms@0.22.0 +- @gradio/statustracker@0.12.4 + +## 0.5.30 + +### Dependency updates + +- @gradio/atoms@0.21.0 +- @gradio/statustracker@0.12.3 + +## 0.5.29 + +### Fixes + +- [#12800](https://github.com/gradio-app/gradio/pull/12800) [`7a1c321`](https://github.com/gradio-app/gradio/commit/7a1c321b6546ba05a353488f5133e8262c4a8a39) - Bump svelte/kit for security reasons. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/statustracker@0.12.2 +- @gradio/atoms@0.20.1 +- @gradio/column@0.3.2 +- @gradio/utils@0.11.2 + +## 0.5.28 + +### Fixes + +- [#12695](https://github.com/gradio-app/gradio/pull/12695) [`6eadb5b`](https://github.com/gradio-app/gradio/commit/6eadb5bf8c48b32fa9960b0e9e7c09a2ee30141c) - Migrate accordion to svelte 5. Thanks @aliabid94! + +### Dependency updates + +- @gradio/utils@0.11.1 +- @gradio/column@0.3.1 + +## 0.5.27 + +### Dependency updates + +- @gradio/atoms@0.20.0 +- @gradio/utils@0.11.0 +- @gradio/statustracker@0.12.1 +- @gradio/column@0.3.0 + +## 0.5.26 + +### Fixes + +- [#12491](https://github.com/gradio-app/gradio/pull/12491) [`4f6327b`](https://github.com/gradio-app/gradio/commit/4f6327be6815fc8d574b60272b02915c75359ace) - Load visible components in 6.0. Thanks @freddyaboulton! + +## 0.5.25 + +### Dependency updates + +- @gradio/utils@0.10.4 + +## 0.5.25 + +### Features + +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Add back default values for labels +- [#12438](https://github.com/gradio-app/gradio/pull/12438) [`25ffc03`](https://github.com/gradio-app/gradio/commit/25ffc0398f8feb43d817c02b2ab970c16de6d797) - Svelte5 migration and bugfix + +### Dependencies + +- @gradio/atoms@0.19.0 +- @gradio/column@0.3.0 +- @gradio/statustracker@0.12.0 +- @gradio/utils@0.10.3 + +## 0.5.25-dev.1 + +### Dependency updates + +- @gradio/atoms@0.19.0-dev.1 +- @gradio/statustracker@0.12.0-dev.1 +- @gradio/column@0.2.2 + +## 0.5.25-dev.0 + +### Dependency updates + +- @gradio/atoms@0.18.2-dev.0 +- @gradio/utils@0.10.3-dev.0 +- @gradio/column@0.2.2 +- @gradio/statustracker@0.12.0-dev.0 + +## 0.5.24 + +### Dependency updates + +- @gradio/atoms@0.18.1 + +## 0.5.24 + +### Fixes + +- [#11784](https://github.com/gradio-app/gradio/pull/11784) [`d9dd3f5`](https://github.com/gradio-app/gradio/commit/d9dd3f54b7fb34cf7118e549d39fc63937ca3489) - Add "hidden" option to component's `visible` kwarg to render but visually hide the component. Thanks @pngwn! + +### Dependency updates + +- @gradio/statustracker@0.11.1 +- @gradio/atoms@0.18.0 +- @gradio/column@0.2.2 + +## 0.5.23 + +### Dependency updates + +- @gradio/atoms@0.17.0 +- @gradio/statustracker@0.11.0 +- @gradio/column@0.2.1 + +## 0.5.22 + +### Dependency updates + +- @gradio/statustracker@0.10.18 + +## 0.5.22 + +### Dependency updates + +- @gradio/statustracker@0.10.17 +- @gradio/atoms@0.16.5 +- @gradio/column@0.2.1 + +## 0.5.21 + +### Dependency updates + +- @gradio/statustracker@0.10.16 +- @gradio/atoms@0.16.4 +- @gradio/column@0.2.1 + +## 0.5.20 + +### Dependency updates + +- @gradio/statustracker@0.10.15 +- @gradio/column@0.2.1 + +## 0.5.19 + +### Dependency updates + +- @gradio/atoms@0.16.3 +- @gradio/statustracker@0.10.14 +- @gradio/column@0.2.1 + +## 0.5.18 + +### Dependency updates + +- @gradio/column@0.2.1 + +## 0.5.17 + +### Dependency updates + +- @gradio/statustracker@0.10.13 +- @gradio/atoms@0.16.2 +- @gradio/column@0.2.0 + +## 0.5.16 + +### Dependency updates + +- @gradio/statustracker@0.10.12 + +## 0.5.16 + +### Dependency updates + +- @gradio/statustracker@0.10.12 + +## 0.5.16 + +### Dependency updates + +- @gradio/statustracker@0.10.12 +- @gradio/column@0.2.0 + +## 0.5.15 + +### Dependency updates + +- @gradio/statustracker@0.10.11 +- @gradio/atoms@0.16.1 +- @gradio/column@0.2.0 + +## 0.5.14 + +### Dependency updates + +- @gradio/statustracker@0.10.10 +- @gradio/atoms@0.16.0 +- @gradio/column@0.2.0 + +## 0.5.13 + +### Dependency updates + +- @gradio/statustracker@0.10.9 +- @gradio/atoms@0.15.2 +- @gradio/utils@0.10.2 +- @gradio/column@0.2.0 + +## 0.5.12 + +### Dependency updates + +- @gradio/atoms@0.15.1 +- @gradio/statustracker@0.10.8 +- @gradio/column@0.2.0 + +## 0.5.11 + +### Dependency updates + +- @gradio/statustracker@0.10.7 +- @gradio/atoms@0.15.0 +- @gradio/column@0.2.0 + +## 0.5.10 + +### Dependency updates + +- @gradio/atoms@0.14.1 +- @gradio/statustracker@0.10.6 +- @gradio/column@0.2.0 + +## 0.5.9 + +### Dependency updates + +- @gradio/statustracker@0.10.5 +- @gradio/atoms@0.14.0 +- @gradio/column@0.2.0 + +## 0.5.8 + +### Dependency updates + +- @gradio/statustracker@0.10.4 +- @gradio/atoms@0.13.3 +- @gradio/column@0.2.0 + +## 0.5.7 + +### Dependency updates + +- @gradio/statustracker@0.10.3 +- @gradio/atoms@0.13.2 +- @gradio/utils@0.10.1 +- @gradio/column@0.2.0 + +## 0.5.6 + +### Dependency updates + +- @gradio/statustracker@0.10.2 +- @gradio/column@0.2.0 + +## 0.5.5 + +### Dependency updates + +- @gradio/atoms@0.13.1 +- @gradio/statustracker@0.10.1 +- @gradio/column@0.2.0 + +## 0.5.4 + +### Dependency updates + +- @gradio/atoms@0.13.0 +- @gradio/utils@0.10.0 +- @gradio/statustracker@0.10.0 +- @gradio/column@0.2.0 + +## 0.5.3 + +### Dependency updates + +- @gradio/statustracker@0.9.7 +- @gradio/atoms@0.12.0 +- @gradio/column@0.2.0 + +## 0.5.2 + +### Dependency updates + +- @gradio/atoms@0.11.2 +- @gradio/utils@0.9.0 +- @gradio/statustracker@0.9.6 +- @gradio/column@0.2.0 + +## 0.5.1 + +### Dependency updates + +- @gradio/atoms@0.11.1 +- @gradio/utils@0.8.0 +- @gradio/statustracker@0.9.5 +- @gradio/column@0.2.0 + +## 0.5.0 + +### Features + +- [#9875](https://github.com/gradio-app/gradio/pull/9875) [`8305ff8`](https://github.com/gradio-app/gradio/commit/8305ff8712183f27174cfb891548ad7cc1c67fed) - Adds `.expand()` and `.collapse()` events to `gr.Accordion`. Thanks @abidlabs! + +### Dependency updates + +- @gradio/statustracker@0.9.4 +- @gradio/atoms@0.11.0 +- @gradio/column@0.2.0 + +## 0.4.5 + +### Dependency updates + +- @gradio/statustracker@0.9.3 +- @gradio/atoms@0.10.1 +- @gradio/column@0.2.0 + +## 0.4.4 + +### Dependency updates + +- @gradio/statustracker@0.9.2 +- @gradio/atoms@0.10.0 +- @gradio/column@0.2.0 + +## 0.4.3 + +### Dependency updates + +- @gradio/statustracker@0.9.1 +- @gradio/atoms@0.9.2 +- @gradio/column@0.2.0 + +## 0.4.2 + +### Dependency updates + +- @gradio/atoms@0.9.1 +- @gradio/statustracker@0.9.0 +- @gradio/column@0.2.0 + +## 0.4.1 + +### Dependency updates + +- @gradio/statustracker@0.8.1 +- @gradio/column@0.2.0 + +## 0.3.23-beta.5 + +### Dependency updates + +- @gradio/statustracker@0.8.0-beta.5 +- @gradio/atoms@0.9.0-beta.5 +- @gradio/column@0.2.0-beta.2 + +## 0.3.23-beta.4 + +### Dependency updates + +- @gradio/statustracker@0.8.0-beta.4 +- @gradio/atoms@0.9.0-beta.4 +- @gradio/column@0.2.0-beta.2 + +## 0.3.23-beta.3 + +### Dependency updates + +- @gradio/statustracker@0.8.0-beta.3 +- @gradio/column@0.2.0-beta.1 +- @gradio/atoms@0.9.0-beta.3 + +## 0.3.23-beta.2 + +### Dependency updates + +- @gradio/statustracker@0.8.0-beta.2 + +## 0.3.23-beta.2 + +### Dependency updates + +- @gradio/atoms@0.9.0-beta.2 +- @gradio/statustracker@0.8.0-beta.2 +- @gradio/utils@0.7.0-beta.2 +- @gradio/column@0.2.0-beta.0 + +## 0.3.23-beta.1 + +### Dependency updates + +- @gradio/atoms@0.8.1-beta.1 +- @gradio/statustracker@0.8.0-beta.1 +- @gradio/utils@0.7.0-beta.1 +- @gradio/column@0.2.0-beta.0 + +## 0.3.23-beta.0 + +## 0.3.23 + +### Fixes + +- [#9163](https://github.com/gradio-app/gradio/pull/9163) [`2b6cbf2`](https://github.com/gradio-app/gradio/commit/2b6cbf25908e42cf027324e54ef2cc0baad11a91) - fix exports and generate types. Thanks @pngwn! + +### Dependency updates + +- @gradio/utils@0.7.0-beta.0 +- @gradio/statustracker@0.8.0-beta.0 +- @gradio/atoms@0.8.1-beta.0 +- @gradio/column@0.2.0-beta.0 + +## 0.3.22 + +### Features + +- [#9118](https://github.com/gradio-app/gradio/pull/9118) [`e1c404d`](https://github.com/gradio-app/gradio/commit/e1c404da1143fb52b659d03e028bdba1badf443d) - setup npm-previews of all packages. Thanks @pngwn! + +### Dependency updates + +- @gradio/utils@0.6.0 +- @gradio/atoms@0.8.0 +- @gradio/statustracker@0.7.5 +- @gradio/column@0.1.3 + +## 0.3.21 + +### Dependency updates + +- @gradio/atoms@0.7.9 +- @gradio/statustracker@0.7.4 +- @gradio/column@0.1.2 + +## 0.3.20 + +### Dependency updates + +- @gradio/atoms@0.7.8 +- @gradio/utils@0.5.2 +- @gradio/statustracker@0.7.3 +- @gradio/column@0.1.2 + +## 0.3.19 + +### Dependency updates + +- @gradio/statustracker@0.7.2 +- @gradio/atoms@0.7.7 +- @gradio/column@0.1.2 + +## 0.3.18 + +### Dependency updates + +- @gradio/atoms@0.7.6 +- @gradio/utils@0.5.1 +- @gradio/statustracker@0.7.1 +- @gradio/column@0.1.2 + +## 0.3.17 + +### Dependency updates + +- @gradio/atoms@0.7.5 +- @gradio/utils@0.5.0 +- @gradio/statustracker@0.7.0 +- @gradio/column@0.1.2 + +## 0.3.16 + +### Dependency updates + +- @gradio/statustracker@0.6.0 +- @gradio/column@0.1.2 + +## 0.3.16 + +### Fixes + +- [#8284](https://github.com/gradio-app/gradio/pull/8284) [`2d705bc`](https://github.com/gradio-app/gradio/commit/2d705bcf7475eb46822358fed21dc081a800a73d) - Add body color to `gr.Accordion`. Thanks @hannahblair! + +### Dependency updates + +- @gradio/column@0.1.2 +- @gradio/statustracker@0.6.0 + +## 0.3.15 + +### Dependency updates + +- @gradio/utils@0.4.2 +- @gradio/atoms@0.7.4 +- @gradio/statustracker@0.5.5 + +## 0.3.14 + +### Dependency updates + +- @gradio/statustracker@0.5.4 + +## 0.3.13 + +### Dependency updates + +- @gradio/statustracker@0.5.3 + +## 0.3.12 + +### Dependency updates + +- @gradio/atoms@0.7.3 +- @gradio/statustracker@0.5.2 + +## 0.3.11 + +### Dependency updates + +- @gradio/atoms@0.7.2 +- @gradio/utils@0.4.1 +- @gradio/statustracker@0.5.1 +- @gradio/column@0.1.1 + +## 0.3.10 + +### Fixes + +- [#8066](https://github.com/gradio-app/gradio/pull/8066) [`624f9b9`](https://github.com/gradio-app/gradio/commit/624f9b9477f74a581a6c14119234f9efdfcda398) - make gradio dev tools a local dependency rather than bundling. Thanks @pngwn! + +### Dependency updates + +- @gradio/atoms@0.7.1 +- @gradio/column@0.1.1 +- @gradio/statustracker@0.5.0 +- @gradio/utils@0.4.0 + +## 0.3.9 + +### Dependency updates + +- @gradio/utils@0.3.2 +- @gradio/statustracker@0.4.12 +- @gradio/atoms@0.7.0 + +## 0.3.8 + +### Dependency updates + +- @gradio/utils@0.3.1 +- @gradio/atoms@0.6.2 +- @gradio/statustracker@0.4.11 + +## 0.3.7 + +### Dependency updates + +- @gradio/atoms@0.6.1 +- @gradio/statustracker@0.4.10 + +## 0.3.6 + +### Dependency updates + +- @gradio/statustracker@0.4.9 +- @gradio/atoms@0.6.0 + +## 0.3.5 + +### Fixes + +- [#7564](https://github.com/gradio-app/gradio/pull/7564) [`5d1e8da`](https://github.com/gradio-app/gradio/commit/5d1e8dae5ac23f605c3b5f41dbe18751dff380a0) - batch UI updates on a per frame basis. Thanks @pngwn! + +## 0.3.4 + +### Patch Changes + +- Updated dependencies [[`98a2719`](https://github.com/gradio-app/gradio/commit/98a2719bfb9c64338caf9009891b6c6b0b33ea89)]: + - @gradio/statustracker@0.4.8 + +## 0.3.3 + +### Patch Changes + +- Updated dependencies []: + - @gradio/atoms@0.5.3 + - @gradio/statustracker@0.4.7 + +## 0.3.2 + +### Patch Changes + +- Updated dependencies [[`065c5b1`](https://github.com/gradio-app/gradio/commit/065c5b163c4badb9d9cbd06d627fb4ba086003e7)]: + - @gradio/utils@0.3.0 + - @gradio/atoms@0.5.2 + - @gradio/statustracker@0.4.6 + +## 0.3.1 + +### Fixes + +- [#7375](https://github.com/gradio-app/gradio/pull/7375) [`4dc9ffb`](https://github.com/gradio-app/gradio/commit/4dc9ffbf70e2233c5b18ed3f722c1189a310a036) - Store `gr.Accordion`'s `open` value. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 0.3.0 + +### Features + +- [#7208](https://github.com/gradio-app/gradio/pull/7208) [`efacc7d`](https://github.com/gradio-app/gradio/commit/efacc7d5cb0d1c6448cbec82abfc00ad6da05b3f) - Ensure `open` reactivity in Accordion. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 0.2.7 + +### Patch Changes + +- Updated dependencies [[`5727b92`](https://github.com/gradio-app/gradio/commit/5727b92abc8a00a675bfc0a921b38de771af947b), [`c60ad4d`](https://github.com/gradio-app/gradio/commit/c60ad4d34ab5b56a89bf6796822977e51e7a4a32)]: + - @gradio/utils@0.2.1 + - @gradio/atoms@0.5.0 + - @gradio/statustracker@0.4.4 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`828fb9e`](https://github.com/gradio-app/gradio/commit/828fb9e6ce15b6ea08318675a2361117596a1b5d), [`73268ee`](https://github.com/gradio-app/gradio/commit/73268ee2e39f23ebdd1e927cb49b8d79c4b9a144)]: + - @gradio/statustracker@0.4.3 + - @gradio/atoms@0.4.1 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies [[`4d1cbbc`](https://github.com/gradio-app/gradio/commit/4d1cbbcf30833ef1de2d2d2710c7492a379a9a00)]: + - @gradio/atoms@0.4.0 + - @gradio/statustracker@0.4.2 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies []: + - @gradio/atoms@0.3.1 + - @gradio/statustracker@0.4.1 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [[`9caddc17b`](https://github.com/gradio-app/gradio/commit/9caddc17b1dea8da1af8ba724c6a5eab04ce0ed8)]: + - @gradio/atoms@0.3.0 + - @gradio/statustracker@0.4.0 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies [[`f816136a0`](https://github.com/gradio-app/gradio/commit/f816136a039fa6011be9c4fb14f573e4050a681a)]: + - @gradio/atoms@0.2.2 + - @gradio/statustracker@0.3.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`3cdeabc68`](https://github.com/gradio-app/gradio/commit/3cdeabc6843000310e1a9e1d17190ecbf3bbc780), [`fad92c29d`](https://github.com/gradio-app/gradio/commit/fad92c29dc1f5cd84341aae417c495b33e01245f)]: + - @gradio/atoms@0.2.1 + - @gradio/statustracker@0.3.1 + +## 0.2.0 + +### Features + +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Custom components. Thanks [@pngwn](https://github.com/pngwn)! +- [#6171](https://github.com/gradio-app/gradio/pull/6171) [`28322422c`](https://github.com/gradio-app/gradio/commit/28322422cb9d8d3e471e439ad602959662e79312) - strip dangling svelte imports. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.2.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`667802a6c`](https://github.com/gradio-app/gradio/commit/667802a6cdbfb2ce454a3be5a78e0990b194548a), [`c476bd5a5`](https://github.com/gradio-app/gradio/commit/c476bd5a5b70836163b9c69bf4bfe068b17fbe13)]: + - @gradio/atoms@0.2.0-beta.6 + - @gradio/statustracker@0.3.0-beta.8 + +## 0.2.0-beta.1 + +### Features + +- [#6016](https://github.com/gradio-app/gradio/pull/6016) [`83e947676`](https://github.com/gradio-app/gradio/commit/83e947676d327ca2ab6ae2a2d710c78961c771a0) - Format js in v4 branch. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.2.0-beta.0 + +### Features + +- [#5960](https://github.com/gradio-app/gradio/pull/5960) [`319c30f3f`](https://github.com/gradio-app/gradio/commit/319c30f3fccf23bfe1da6c9b132a6a99d59652f7) - rererefactor frontend files. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [[`e70805d54`](https://github.com/gradio-app/gradio/commit/e70805d54cc792452545f5d8eccc1aa0212a4695)]: + - @gradio/atoms@0.2.0 + - @gradio/statustracker@0.2.3 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies []: + - @gradio/atoms@0.1.4 + - @gradio/statustracker@0.2.2 + +## 0.1.1 + +### Patch Changes + +- Updated dependencies []: + - @gradio/atoms@0.1.3 + - @gradio/statustracker@0.2.1 + +## 0.1.0 + +### Features + +- [#5554](https://github.com/gradio-app/gradio/pull/5554) [`75ddeb390`](https://github.com/gradio-app/gradio/commit/75ddeb390d665d4484667390a97442081b49a423) - Accessibility Improvements. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 0.0.4 + +### Patch Changes + +- Updated dependencies [[`afac0006`](https://github.com/gradio-app/gradio/commit/afac0006337ce2840cf497cd65691f2f60ee5912)]: + - @gradio/statustracker@0.2.0 + - @gradio/atoms@0.1.2 + +## 0.0.3 + +### Patch Changes + +- Updated dependencies []: + - @gradio/atoms@0.1.1 + - @gradio/statustracker@0.1.1 + +## 0.0.2 + +### Features + +- [#5215](https://github.com/gradio-app/gradio/pull/5215) [`fbdad78a`](https://github.com/gradio-app/gradio/commit/fbdad78af4c47454cbb570f88cc14bf4479bbceb) - Lazy load interactive or static variants of a component individually, rather than loading both variants regardless. This change will improve performance for many applications. Thanks [@pngwn](https://github.com/pngwn)! \ No newline at end of file diff --git a/js/accordion/Index.svelte b/js/accordion/Index.svelte new file mode 100644 index 0000000..463b310 --- /dev/null +++ b/js/accordion/Index.svelte @@ -0,0 +1,62 @@ + + + + {#if gradio.shared.loading_status} + + {/if} + + { + gradio.dispatch("expand"); + gradio.dispatch("gradio_expand"); + }} + oncollapse={() => gradio.dispatch("collapse")} + > + + + + + diff --git a/js/accordion/README.md b/js/accordion/README.md new file mode 100644 index 0000000..e5b427f --- /dev/null +++ b/js/accordion/README.md @@ -0,0 +1,11 @@ +# `@gradio/button` + +```html + + + +``` diff --git a/js/accordion/package.json b/js/accordion/package.json new file mode 100644 index 0000000..88a7952 --- /dev/null +++ b/js/accordion/package.json @@ -0,0 +1,34 @@ +{ + "name": "@gradio/accordion", + "version": "0.5.39", + "description": "Gradio UI packages", + "type": "module", + "author": "", + "license": "ISC", + "main_changeset": true, + "dependencies": { + "@gradio/atoms": "workspace:^", + "@gradio/column": "workspace:^", + "@gradio/statustracker": "workspace:^", + "@gradio/utils": "workspace:^" + }, + "peerDependencies": { + "svelte": "^5.48.0" + }, + "devDependencies": { + "@gradio/preview": "workspace:^" + }, + "exports": { + ".": { + "gradio": "./Index.svelte", + "svelte": "./dist/Index.svelte", + "types": "./dist/Index.svelte.d.ts" + }, + "./package.json": "./package.json" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/gradio-app/gradio.git", + "directory": "js/accordion" + } +} diff --git a/js/accordion/shared/Accordion.svelte b/js/accordion/shared/Accordion.svelte new file mode 100644 index 0000000..b5f03ed --- /dev/null +++ b/js/accordion/shared/Accordion.svelte @@ -0,0 +1,55 @@ + + + +
+ +
+ + diff --git a/js/accordion/types.ts b/js/accordion/types.ts new file mode 100644 index 0000000..55ce738 --- /dev/null +++ b/js/accordion/types.ts @@ -0,0 +1,9 @@ +export interface AccordionProps { + open: boolean; +} + +export interface AccordionEvents { + expand: never; + collapse: never; + gradio_expand: never; +} diff --git a/js/annotatedimage/AnnotatedImage.test.ts b/js/annotatedimage/AnnotatedImage.test.ts new file mode 100644 index 0000000..3eab113 --- /dev/null +++ b/js/annotatedimage/AnnotatedImage.test.ts @@ -0,0 +1,613 @@ +import { test, describe, afterEach, expect } from "vitest"; +import { cleanup, render, fireEvent, waitFor } from "@self/tootils/render"; +import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests"; + +import AnnotatedImage from "./Index.svelte"; + +const fake_image = { + path: "test.png", + url: "https://example.com/test.png", + orig_name: "test.png", + size: 1024, + mime_type: "image/png", + is_stream: false +}; + +const fake_mask_1 = { + path: "mask1.png", + url: "https://example.com/mask1.png", + orig_name: "mask1.png", + size: 512, + mime_type: "image/png", + is_stream: false +}; + +const fake_mask_2 = { + path: "mask2.png", + url: "https://example.com/mask2.png", + orig_name: "mask2.png", + size: 512, + mime_type: "image/png", + is_stream: false +}; + +const fake_value = { + image: fake_image, + annotations: [ + { image: fake_mask_1, label: "cat" }, + { image: fake_mask_2, label: "dog" } + ] +}; + +const single_annotation_value = { + image: fake_image, + annotations: [{ image: fake_mask_1, label: "cat" }] +}; + +const default_props = { + value: null as any, + label: "Annotated Image", + show_label: true, + show_legend: true, + color_map: {} as Record, + buttons: ["fullscreen"] as ( + | string + | { value: string; id: number; icon: null } + )[] +}; + +// BlockLabel in AnnotatedImage doesn't use data-testid='block-info', +// so the shared show_label: false test fails. Disable has_label and +// write custom label tests below. +run_shared_prop_tests({ + component: AnnotatedImage, + name: "AnnotatedImage", + base_props: { + ...default_props + }, + has_label: false, + has_validation_error: false +}); + +describe("Label", () => { + afterEach(() => cleanup()); + + test("label text is rendered", async () => { + const { getByText } = await render(AnnotatedImage, { + ...default_props, + label: "My Image", + show_label: true + }); + + expect(getByText("My Image")).toBeTruthy(); + expect(getByText("My Image")).toBeVisible(); + }); + + test("show_label=false hides the label visually", async () => { + const { getByText } = await render(AnnotatedImage, { + ...default_props, + label: "My Image", + show_label: false + }); + + // BlockLabel hides via CSS when show_label is false + expect(getByText("My Image")).not.toBeVisible(); + }); +}); + +describe("AnnotatedImage", () => { + afterEach(() => cleanup()); + + test("renders empty state when value is null", async () => { + const { queryByAltText } = await render(AnnotatedImage, { + ...default_props, + value: null + }); + + expect(queryByAltText("the base file that is annotated")).toBeNull(); + }); + + test("renders base image when value is set", async () => { + const { getByAltText } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + const img = getByAltText("the base file that is annotated"); + expect(img).toBeTruthy(); + expect(img.getAttribute("src")).toBe("https://example.com/test.png"); + }); + + test("renders annotation masks when value has annotations", async () => { + const { container } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + const masks = container.querySelectorAll("img.mask"); + expect(masks.length).toBe(2); + expect(masks[0].getAttribute("src")).toBe("https://example.com/mask1.png"); + expect(masks[1].getAttribute("src")).toBe("https://example.com/mask2.png"); + }); +}); + +describe("Props: show_legend", () => { + afterEach(() => cleanup()); + + test("show_legend=true renders legend buttons for each annotation", async () => { + const { getByRole } = await render(AnnotatedImage, { + ...default_props, + value: fake_value, + show_legend: true + }); + + expect(getByRole("button", { name: "cat" })).toBeTruthy(); + expect(getByRole("button", { name: "dog" })).toBeTruthy(); + expect(getByRole("button", { name: "cat" })).toBeVisible(); + expect(getByRole("button", { name: "dog" })).toBeVisible(); + }); + + test("show_legend=false hides legend", async () => { + const { queryByRole } = await render(AnnotatedImage, { + ...default_props, + value: fake_value, + show_legend: false + }); + + expect(queryByRole("button", { name: "cat" })).toBeNull(); + expect(queryByRole("button", { name: "dog" })).toBeNull(); + }); + + test("legend is not rendered when value is null", async () => { + const { queryByRole } = await render(AnnotatedImage, { + ...default_props, + value: null, + show_legend: true + }); + + expect(queryByRole("button", { name: "cat" })).toBeNull(); + }); +}); + +describe("Props: color_map", () => { + afterEach(() => cleanup()); + + test("color_map applies custom background colors to legend items", async () => { + const { getByRole } = await render(AnnotatedImage, { + ...default_props, + value: fake_value, + color_map: { cat: "#ff0000", dog: "#00ff00" } + }); + + const catBtn = getByRole("button", { name: "cat" }); + const dogBtn = getByRole("button", { name: "dog" }); + + // color_map colors get '88' (0x88/0xff ≈ 0.533) appended for semi-transparency + // The browser normalizes to rgba + expect(catBtn.style.backgroundColor).toContain("rgba(255, 0, 0"); + expect(dogBtn.style.backgroundColor).toContain("rgba(0, 255, 0"); + }); + + test("without color_map, masks have hue-rotate filter style", async () => { + const { container } = await render(AnnotatedImage, { + ...default_props, + value: fake_value, + color_map: {} + }); + + const masks = container.querySelectorAll("img.mask"); + // First mask: hue-rotate(0deg), second: hue-rotate(180deg) + expect(masks[0].getAttribute("style")).toContain("hue-rotate(0deg)"); + expect(masks[1].getAttribute("style")).toContain("hue-rotate(180deg)"); + }); + + test("with color_map, masks have no hue-rotate filter", async () => { + const { container } = await render(AnnotatedImage, { + ...default_props, + value: fake_value, + color_map: { cat: "#ff0000", dog: "#00ff00" } + }); + + const masks = container.querySelectorAll("img.mask"); + for (const mask of masks) { + const style = mask.getAttribute("style"); + // Should be null or not contain hue-rotate + if (style) { + expect(style).not.toContain("hue-rotate"); + } + } + }); +}); + +describe("Props: buttons", () => { + afterEach(() => cleanup()); + + test("fullscreen button renders when 'fullscreen' is in buttons", async () => { + const { getByLabelText } = await render(AnnotatedImage, { + ...default_props, + value: fake_value, + buttons: ["fullscreen"] + }); + + expect(getByLabelText("Fullscreen")).toBeTruthy(); + expect(getByLabelText("Fullscreen")).toBeVisible(); + }); + + test("clicking the fullscreen button toggles fullscreen mode", async () => { + const { getByLabelText } = await render(AnnotatedImage, { + ...default_props, + value: fake_value, + buttons: ["fullscreen"] + }); + + await fireEvent.click(getByLabelText("Fullscreen")); + await waitFor(() => { + expect(getByLabelText("Exit fullscreen mode")).toBeVisible(); + }); + + await fireEvent.click(getByLabelText("Exit fullscreen mode")); + await waitFor(() => { + expect(getByLabelText("Fullscreen")).toBeVisible(); + }); + }); + + test("empty buttons array shows no fullscreen button", async () => { + const { queryByLabelText } = await render(AnnotatedImage, { + ...default_props, + value: fake_value, + buttons: [] + }); + + expect(queryByLabelText("Fullscreen")).toBeNull(); + }); + + test("custom button renders and dispatches custom_button_click", async () => { + const { listen, getByLabelText } = await render(AnnotatedImage, { + ...default_props, + value: fake_value, + buttons: [{ value: "Analyze", id: 5, icon: null }] + }); + + const custom = listen("custom_button_click"); + const btn = getByLabelText("Analyze"); + + await fireEvent.click(btn); + + expect(custom).toHaveBeenCalledTimes(1); + expect(custom).toHaveBeenCalledWith({ id: 5 }); + }); + + test("buttons not rendered when value is null (empty state)", async () => { + const { queryByLabelText } = await render(AnnotatedImage, { + ...default_props, + value: null, + buttons: ["fullscreen"] + }); + + expect(queryByLabelText("Fullscreen")).toBeNull(); + }); +}); + +describe("Events: change", () => { + afterEach(() => cleanup()); + + test("setting value triggers change event", async () => { + const { listen, set_data } = await render(AnnotatedImage, { + ...default_props, + value: null + }); + + const change = listen("change"); + + await set_data({ value: fake_value }); + + expect(change).toHaveBeenCalledTimes(1); + }); + + test("change event is not triggered on mount with null value", async () => { + const { listen } = await render(AnnotatedImage, { + ...default_props, + value: null + }); + + const change = listen("change", { retrospective: true }); + + expect(change).not.toHaveBeenCalled(); + }); + + test("change event does not fire once on mount when initial value is set", async () => { + // The component's $effect fires change when old_value != value on mount + const { listen } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + const change = listen("change", { retrospective: true }); + + expect(change).not.toHaveBeenCalled(); + }); + + test("changing value multiple times triggers change each time", async () => { + const { listen, set_data } = await render(AnnotatedImage, { + ...default_props, + value: null + }); + + const change = listen("change"); + + await set_data({ value: fake_value }); + expect(change).toHaveBeenCalledTimes(1); + await set_data({ value: single_annotation_value }); + + expect(change).toHaveBeenCalledTimes(2); + }); + + test("setting value to null after a value triggers change", async () => { + const { listen, set_data } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + const change = listen("change"); + + await set_data({ value: null }); + + expect(change).toHaveBeenCalledTimes(1); + }); +}); + +describe("Events: select", () => { + afterEach(() => cleanup()); + + test("clicking legend item dispatches select with value and index", async () => { + const { listen, getByRole } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + const select = listen("select"); + + await fireEvent.click(getByRole("button", { name: "cat" })); + + expect(select).toHaveBeenCalledTimes(1); + expect(select).toHaveBeenCalledWith({ value: "cat", index: 0 }); + }); + + test("clicking second legend item dispatches correct index", async () => { + const { listen, getByRole } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + const select = listen("select"); + + await fireEvent.click(getByRole("button", { name: "dog" })); + + expect(select).toHaveBeenCalledTimes(1); + expect(select).toHaveBeenCalledWith({ value: "dog", index: 1 }); + }); + + test("clicking multiple legend items dispatches select for each", async () => { + const { listen, getByRole } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + const select = listen("select"); + + await fireEvent.click(getByRole("button", { name: "cat" })); + await fireEvent.click(getByRole("button", { name: "dog" })); + + expect(select).toHaveBeenCalledTimes(2); + expect(select).toHaveBeenNthCalledWith(1, { value: "cat", index: 0 }); + expect(select).toHaveBeenNthCalledWith(2, { value: "dog", index: 1 }); + }); +}); + +describe("Legend hover interactions", () => { + afterEach(() => cleanup()); + + test("hovering over legend item sets active class on corresponding mask", async () => { + const { container, getByRole } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + await fireEvent.mouseOver(getByRole("button", { name: "cat" })); + + const masks = container.querySelectorAll("img.mask"); + expect(masks[0].classList.contains("active")).toBe(true); + }); + + test("hovering over legend item sets inactive class on other masks", async () => { + const { container, getByRole } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + await fireEvent.mouseOver(getByRole("button", { name: "cat" })); + + const masks = container.querySelectorAll("img.mask"); + expect(masks[0].classList.contains("active")).toBe(true); + expect(masks[1].classList.contains("inactive")).toBe(true); + }); + + test("mouseout resets all masks to default state", async () => { + const { container, getByRole } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + await fireEvent.mouseOver(getByRole("button", { name: "cat" })); + await fireEvent.mouseOut(getByRole("button", { name: "cat" })); + + const masks = container.querySelectorAll("img.mask"); + expect(masks[0].classList.contains("active")).toBe(false); + expect(masks[0].classList.contains("inactive")).toBe(false); + expect(masks[1].classList.contains("active")).toBe(false); + expect(masks[1].classList.contains("inactive")).toBe(false); + }); + + test("focus on legend item activates mask (keyboard accessibility)", async () => { + const { container, getByRole } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + await fireEvent.focus(getByRole("button", { name: "dog" })); + + const masks = container.querySelectorAll("img.mask"); + expect(masks[1].classList.contains("active")).toBe(true); + expect(masks[0].classList.contains("inactive")).toBe(true); + }); + + test("blur on legend item resets masks (keyboard accessibility)", async () => { + const { container, getByRole } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + await fireEvent.focus(getByRole("button", { name: "dog" })); + await fireEvent.blur(getByRole("button", { name: "dog" })); + + const masks = container.querySelectorAll("img.mask"); + expect(masks[0].classList.contains("active")).toBe(false); + expect(masks[0].classList.contains("inactive")).toBe(false); + expect(masks[1].classList.contains("active")).toBe(false); + expect(masks[1].classList.contains("inactive")).toBe(false); + }); +}); + +describe("get_data / set_data", () => { + afterEach(() => cleanup()); + + test("get_data returns null when no value", async () => { + const { get_data } = await render(AnnotatedImage, { + ...default_props, + value: null + }); + + const data = await get_data(); + expect(data.value).toBeNull(); + }); + + test("set_data updates the displayed image", async () => { + const { set_data, getByAltText } = await render(AnnotatedImage, { + ...default_props, + value: null + }); + + await set_data({ value: fake_value }); + + const img = getByAltText("the base file that is annotated"); + expect(img).toBeTruthy(); + expect(img.getAttribute("src")).toBe("https://example.com/test.png"); + }); + + test("get_data reflects set_data value (round-trip)", async () => { + const { get_data, set_data } = await render(AnnotatedImage, { + ...default_props, + value: null + }); + + await set_data({ value: fake_value }); + + const data = await get_data(); + expect(data.value).toEqual(fake_value); + }); + + test("set_data to null shows empty state", async () => { + const { set_data, queryByAltText } = await render(AnnotatedImage, { + ...default_props, + value: fake_value + }); + + await set_data({ value: null }); + + expect(queryByAltText("the base file that is annotated")).toBeNull(); + }); + + test("set_data updates legend items", async () => { + const { set_data, getByRole, queryByRole } = await render(AnnotatedImage, { + ...default_props, + value: null + }); + + await set_data({ value: fake_value }); + + expect(getByRole("button", { name: "cat" })).toBeTruthy(); + expect(getByRole("button", { name: "dog" })).toBeTruthy(); + + await set_data({ value: single_annotation_value }); + + expect(getByRole("button", { name: "cat" })).toBeTruthy(); + expect(queryByRole("button", { name: "dog" })).toBeNull(); + }); +}); + +describe("Edge cases", () => { + afterEach(() => cleanup()); + + test("single annotation renders without errors in hue calculation", async () => { + const { container } = await render(AnnotatedImage, { + ...default_props, + value: single_annotation_value, + color_map: {} + }); + + const masks = container.querySelectorAll("img.mask"); + expect(masks.length).toBe(1); + // With 1 annotation: hue-rotate(0 * 360 / 1) = hue-rotate(0deg) + expect(masks[0].getAttribute("style")).toContain("hue-rotate(0deg)"); + }); + + test("value with empty annotations array renders base image but no masks or legend", async () => { + const { container, getByAltText, queryByRole } = await render( + AnnotatedImage, + { + ...default_props, + value: { + image: fake_image, + annotations: [] + } + } + ); + + expect(getByAltText("the base file that is annotated")).toBeTruthy(); + + const masks = container.querySelectorAll("img.mask"); + expect(masks.length).toBe(0); + + // No legend buttons since there are no annotations + expect(queryByRole("button", { name: "cat" })).toBeNull(); + }); + + test("color_map with only some labels falls back to hue-rotate for unmapped labels", async () => { + const { container } = await render(AnnotatedImage, { + ...default_props, + value: fake_value, + color_map: { cat: "#ff0000" } // only cat is mapped, dog is not + }); + + const masks = container.querySelectorAll("img.mask"); + // cat mask: has color_map entry, no hue-rotate + const catStyle = masks[0].getAttribute("style"); + if (catStyle) { + expect(catStyle).not.toContain("hue-rotate"); + } + // dog mask: no color_map entry, should have hue-rotate + expect(masks[1].getAttribute("style")).toContain("hue-rotate"); + }); +}); + +test.todo( + "VISUAL: mask opacity transitions on hover — masks should fade to 0.3 opacity on container hover, active mask at 1.0, inactive at 0 — needs Playwright visual regression screenshot comparison" +); + +test.todo( + "VISUAL: height/width props control component dimensions — needs Playwright visual regression screenshot comparison" +); + +test.todo( + "VISUAL: default hue-rotate coloring distributes colors evenly across the color wheel — needs Playwright visual regression screenshot comparison" +); diff --git a/js/annotatedimage/CHANGELOG.md b/js/annotatedimage/CHANGELOG.md new file mode 100644 index 0000000..929be65 --- /dev/null +++ b/js/annotatedimage/CHANGELOG.md @@ -0,0 +1,1295 @@ +# @gradio/annotatedimage + +## 0.12.1 + +### Dependency updates + +- @gradio/statustracker@0.15.1 +- @gradio/icons@0.16.0 +- @gradio/atoms@0.26.0 +- @gradio/client@2.3.1 +- @gradio/upload@0.18.1 + +## 0.12.0 + +### Features + +- [#13526](https://github.com/gradio-app/gradio/pull/13526) [`53cb4ca`](https://github.com/gradio-app/gradio/commit/53cb4cae1ec3521e9170d12867253516413ba37a) - Run `pnpm lint` and `pnpm ts:check` on CI. Thanks @abidlabs! + +### Fixes + +- [#13533](https://github.com/gradio-app/gradio/pull/13533) [`e5ec1ca`](https://github.com/gradio-app/gradio/commit/e5ec1ca66c45e7e3a057b75ac2b8b86660b2827b) - Fix fullscreen button not working in ImageSlider, interactive Image, native plots, and AnnotatedImage. Thanks @hysts! + +### Dependency updates + +- @gradio/atoms@0.25.0 +- @gradio/statustracker@0.15.0 +- @gradio/utils@0.13.0 +- @gradio/client@2.3.0 +- @gradio/upload@0.18.0 + +## 0.11.8 + +### Dependency updates + +- @gradio/client@2.2.2 + +## 0.11.8 + +### Dependency updates + +- @gradio/client@2.2.1 +- @gradio/upload@0.17.10 + +## 0.11.8 + +### Dependency updates + +- @gradio/atoms@0.24.0 +- @gradio/statustracker@0.14.1 +- @gradio/upload@0.17.9 + +## 0.11.7 + +### Features + +- [#13185](https://github.com/gradio-app/gradio/pull/13185) [`ffc00ff`](https://github.com/gradio-app/gradio/commit/ffc00ff4cf23641e90f0963cec6ed52f85ed511c) - Annotated image unit tests. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/atoms@0.23.1 +- @gradio/statustracker@0.14.0 +- @gradio/client@2.2.0 + +## 0.11.6 + +### Dependency updates + +- @gradio/utils@0.12.2 +- @gradio/atoms@0.23.0 +- @gradio/statustracker@0.13.1 +- @gradio/upload@0.17.8 + +## 0.11.5 + +### Dependency updates + +- @gradio/utils@0.12.1 +- @gradio/statustracker@0.13.0 + +## 0.11.4 + +### Dependency updates + +- @gradio/statustracker@0.12.5 +- @gradio/utils@0.12.0 +- @gradio/atoms@0.22.2 +- @gradio/upload@0.17.7 + +## 0.11.3 + +### Dependency updates + +- @gradio/client@2.1.0 + +## 0.11.3 + +### Dependency updates + +- @gradio/utils@0.11.3 +- @gradio/atoms@0.22.0 +- @gradio/statustracker@0.12.4 +- @gradio/upload@0.17.6 + +## 0.11.2 + +### Dependency updates + +- @gradio/atoms@0.21.0 +- @gradio/client@2.0.4 +- @gradio/statustracker@0.12.3 +- @gradio/upload@0.17.5 + +## 0.11.1 + +### Fixes + +- [#12800](https://github.com/gradio-app/gradio/pull/12800) [`7a1c321`](https://github.com/gradio-app/gradio/commit/7a1c321b6546ba05a353488f5133e8262c4a8a39) - Bump svelte/kit for security reasons. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/statustracker@0.12.2 +- @gradio/atoms@0.20.1 +- @gradio/utils@0.11.2 +- @gradio/icons@0.15.1 +- @gradio/upload@0.17.4 +- @gradio/client@2.0.3 + +## 0.11.0 + +### Dependency updates + +- @gradio/utils@0.11.1 +- @gradio/client@2.0.2 + +## 0.11.0 + +### Features + +- [#12539](https://github.com/gradio-app/gradio/pull/12539) [`f1d83fa`](https://github.com/gradio-app/gradio/commit/f1d83fac3d6e4bad60cf896a026fa2d572f26073) - Add ability to add custom buttons to components. Thanks @abidlabs! + +### Dependency updates + +- @gradio/atoms@0.20.0 +- @gradio/utils@0.11.0 +- @gradio/client@2.0.1 +- @gradio/statustracker@0.12.1 +- @gradio/upload@0.17.3 + +## 0.10.1 + +### Dependency updates + +- @gradio/utils@0.10.4 + +## 0.10.1 + +### Features + +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Add back default values for labels +- [#12438](https://github.com/gradio-app/gradio/pull/12438) [`25ffc03`](https://github.com/gradio-app/gradio/commit/25ffc0398f8feb43d817c02b2ab970c16de6d797) - Svelte5 migration and bugfix + +### Dependencies + +- @gradio/atoms@0.19.0 +- @gradio/icons@0.15.0 +- @gradio/statustracker@0.12.0 +- @gradio/upload@0.17.2 +- @gradio/utils@0.10.3 +- @gradio/client@2.0.0 + +## 0.10.1-dev.2 + +### Dependency updates + +- @gradio/atoms@0.19.0-dev.1 +- @gradio/client@2.0.0-dev.2 +- @gradio/statustracker@0.12.0-dev.1 +- @gradio/upload@0.17.2-dev.2 + +## 0.10.1-dev.1 + +### Dependency updates + +- @gradio/atoms@0.18.2-dev.0 +- @gradio/upload@0.17.2-dev.1 +- @gradio/utils@0.10.3-dev.0 +- @gradio/statustracker@0.12.0-dev.0 +- @gradio/icons@0.15.0-dev.0 + +## 0.10.1-dev.0 + +### Dependency updates + +- @gradio/client@2.0.0-dev.1 + +## 0.10.1-dev.0 + +### Dependency updates + +- @gradio/upload@0.17.2-dev.0 +- @gradio/client@2.0.0-dev.0 + +## 0.10.0 + +### Dependency updates + +- @gradio/client@1.19.1 + +## 0.10.0 + +### Dependency updates + +- @gradio/upload@0.17.1 +- @gradio/atoms@0.18.1 + +## 0.10.0 + +### Features + +- [#11858](https://github.com/gradio-app/gradio/pull/11858) [`3f8ea13`](https://github.com/gradio-app/gradio/commit/3f8ea13a8ca92abf0ad34392e403a449fda3c6c2) - remove lite. Thanks @pngwn! + +### Fixes + +- [#11784](https://github.com/gradio-app/gradio/pull/11784) [`d9dd3f5`](https://github.com/gradio-app/gradio/commit/d9dd3f54b7fb34cf7118e549d39fc63937ca3489) - Add "hidden" option to component's `visible` kwarg to render but visually hide the component. Thanks @pngwn! + +### Dependency updates + +- @gradio/statustracker@0.11.1 +- @gradio/atoms@0.18.0 +- @gradio/client@1.19.0 +- @gradio/upload@0.17.0 + +## 0.9.30 + +### Dependency updates + +- @gradio/client@1.18.0 +- @gradio/icons@0.14.0 +- @gradio/atoms@0.17.0 +- @gradio/statustracker@0.11.0 +- @gradio/upload@0.16.17 + +## 0.9.29 + +### Dependency updates + +- @gradio/statustracker@0.10.18 + +## 0.9.29 + +### Dependency updates + +- @gradio/icons@0.13.1 +- @gradio/upload@0.16.16 + +## 0.9.29 + +### Dependency updates + +- @gradio/statustracker@0.10.17 +- @gradio/atoms@0.16.5 +- @gradio/client@1.17.1 +- @gradio/icons@0.13.0 +- @gradio/upload@0.16.15 + +## 0.9.28 + +### Dependency updates + +- @gradio/statustracker@0.10.16 +- @gradio/atoms@0.16.4 +- @gradio/client@1.17.0 +- @gradio/upload@0.16.14 + +## 0.9.27 + +### Dependency updates + +- @gradio/upload@0.16.13 +- @gradio/client@1.16.0 + +## 0.9.26 + +### Dependency updates + +- @gradio/upload@0.16.12 +- @gradio/client@1.15.7 + +## 0.9.25 + +### Dependency updates + +- @gradio/client@1.15.6 +- @gradio/statustracker@0.10.15 +- @gradio/upload@0.16.11 + +## 0.9.24 + +### Dependency updates + +- @gradio/atoms@0.16.3 +- @gradio/statustracker@0.10.14 +- @gradio/upload@0.16.10 +- @gradio/client@1.15.5 + +## 0.9.23 + +### Dependency updates + +- @gradio/upload@0.16.9 +- @gradio/client@1.15.4 + +## 0.9.22 + +### Dependency updates + +- @gradio/statustracker@0.10.13 +- @gradio/atoms@0.16.2 +- @gradio/client@1.15.3 +- @gradio/upload@0.16.8 + +## 0.9.21 + +### Dependency updates + +- @gradio/statustracker@0.10.12 + +## 0.9.21 + +### Dependency updates + +- @gradio/upload@0.16.7 +- @gradio/client@1.15.2 + +## 0.9.20 + +### Dependency updates + +- @gradio/statustracker@0.10.12 + +## 0.9.20 + +### Dependency updates + +- @gradio/statustracker@0.10.12 +- @gradio/client@1.15.1 +- @gradio/upload@0.16.6 + +## 0.9.19 + +### Features + +- [#11177](https://github.com/gradio-app/gradio/pull/11177) [`3068196`](https://github.com/gradio-app/gradio/commit/3068196d47234fd1c1634f33b9ddfc089f5cbbe0) - Improved, smoother fullscreen mode for components. Thanks @aliabid94! + +### Dependency updates + +- @gradio/statustracker@0.10.11 +- @gradio/atoms@0.16.1 +- @gradio/client@1.15.0 +- @gradio/upload@0.16.5 + +## 0.9.18 + +### Dependency updates + +- @gradio/upload@0.16.4 + +## 0.9.17 + +### Dependency updates + +- @gradio/statustracker@0.10.10 +- @gradio/upload@0.16.3 +- @gradio/atoms@0.16.0 + +## 0.9.16 + +### Dependency updates + +- @gradio/statustracker@0.10.9 +- @gradio/atoms@0.15.2 +- @gradio/client@1.14.2 +- @gradio/utils@0.10.2 +- @gradio/upload@0.16.2 + +## 0.9.15 + +### Dependency updates + +- @gradio/upload@0.16.1 +- @gradio/atoms@0.15.1 +- @gradio/statustracker@0.10.8 +- @gradio/icons@0.12.0 + +## 0.9.14 + +### Dependency updates + +- @gradio/statustracker@0.10.7 +- @gradio/atoms@0.15.0 +- @gradio/icons@0.11.0 +- @gradio/upload@0.16.0 + +## 0.9.13 + +### Dependency updates + +- @gradio/wasm@0.18.1 +- @gradio/client@1.14.1 +- @gradio/upload@0.15.7 + +## 0.9.12 + +### Dependency updates + +- @gradio/atoms@0.14.1 +- @gradio/statustracker@0.10.6 +- @gradio/client@1.14.0 +- @gradio/wasm@0.18.0 +- @gradio/upload@0.15.6 + +## 0.9.11 + +### Dependency updates + +- @gradio/upload@0.15.5 +- @gradio/statustracker@0.10.5 +- @gradio/atoms@0.14.0 + +## 0.9.10 + +### Dependency updates + +- @gradio/client@1.13.1 +- @gradio/wasm@0.17.4 +- @gradio/upload@0.15.4 + +## 0.9.9 + +### Dependency updates + +- @gradio/upload@0.15.3 +- @gradio/client@1.13.0 + +## 0.9.8 + +### Dependency updates + +- @gradio/upload@0.15.2 +- @gradio/statustracker@0.10.4 +- @gradio/atoms@0.13.3 + +## 0.9.7 + +### Dependency updates + +- @gradio/statustracker@0.10.3 +- @gradio/atoms@0.13.2 +- @gradio/utils@0.10.1 +- @gradio/client@1.12.0 +- @gradio/upload@0.15.1 +- @gradio/wasm@0.17.3 + +## 0.9.6 + +### Dependency updates + +- @gradio/client@1.11.0 +- @gradio/upload@0.15.0 + +## 0.9.5 + +### Dependency updates + +- @gradio/upload@0.14.8 +- @gradio/wasm@0.17.2 + +## 0.9.4 + +### Dependency updates + +- @gradio/upload@0.14.7 +- @gradio/wasm@0.17.1 + +## 0.9.3 + +### Dependency updates + +- @gradio/upload@0.14.6 +- @gradio/wasm@0.17.0 +- @gradio/statustracker@0.10.2 + +## 0.9.2 + +### Dependency updates + +- @gradio/atoms@0.13.1 +- @gradio/statustracker@0.10.1 +- @gradio/client@1.10.0 +- @gradio/icons@0.10.0 +- @gradio/upload@0.14.5 + +## 0.9.1 + +### Dependency updates + +- @gradio/atoms@0.13.0 +- @gradio/utils@0.10.0 +- @gradio/upload@0.14.4 +- @gradio/client@1.9.0 +- @gradio/icons@0.9.0 +- @gradio/statustracker@0.10.0 +- @gradio/wasm@0.16.0 + +## 0.9.0 + +### Features + +- [#10098](https://github.com/gradio-app/gradio/pull/10098) [`9a6ce6f`](https://github.com/gradio-app/gradio/commit/9a6ce6f6b089d94c06da0b8620f28967f39f8383) - Refactor full screen logic to be reusable. Thanks @hannahblair! + +### Dependency updates + +- @gradio/statustracker@0.9.7 +- @gradio/upload@0.14.3 +- @gradio/atoms@0.12.0 + +## 0.8.9 + +### Dependency updates + +- @gradio/atoms@0.11.2 +- @gradio/utils@0.9.0 +- @gradio/statustracker@0.9.6 +- @gradio/upload@0.14.2 + +## 0.8.8 + +### Dependency updates + +- @gradio/atoms@0.11.1 +- @gradio/client@1.8.0 +- @gradio/utils@0.8.0 +- @gradio/upload@0.14.1 +- @gradio/statustracker@0.9.5 + +## 0.8.7 + +### Dependency updates + +- @gradio/statustracker@0.9.4 +- @gradio/atoms@0.11.0 +- @gradio/upload@0.14.0 +- @gradio/wasm@0.15.0 + +## 0.8.6 + +### Dependency updates + +- @gradio/statustracker@0.9.3 +- @gradio/atoms@0.10.1 +- @gradio/client@1.7.1 +- @gradio/upload@0.13.5 + +## 0.8.5 + +### Dependency updates + +- @gradio/statustracker@0.9.2 +- @gradio/atoms@0.10.0 +- @gradio/icons@0.8.1 +- @gradio/upload@0.13.4 + +## 0.8.4 + +### Dependency updates + +- @gradio/statustracker@0.9.1 +- @gradio/upload@0.13.3 +- @gradio/atoms@0.9.2 + +## 0.8.3 + +### Dependency updates + +- @gradio/atoms@0.9.1 +- @gradio/statustracker@0.9.0 +- @gradio/client@1.7.0 +- @gradio/upload@0.13.2 +- @gradio/wasm@0.14.2 + +## 0.8.2 + +### Dependency updates + +- @gradio/upload@0.13.1 +- @gradio/wasm@0.14.1 + +## 0.8.1 + +### Dependency updates + +- @gradio/statustracker@0.8.1 + +## 0.8.0 + +### Features + +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Improve Icon Button consistency + +### Dependencies + +- @gradio/atoms@0.9.0 +- @gradio/icons@0.8.0 +- @gradio/statustracker@0.8.0 +- @gradio/upload@0.13.0 +- @gradio/utils@0.7.0 +- @gradio/client@1.6.0 +- @gradio/wasm@0.14.0 + +## 0.8.0-beta.7 + +### Dependency updates + +- @gradio/upload@0.13.0-beta.7 +- @gradio/statustracker@0.8.0-beta.5 +- @gradio/icons@0.8.0-beta.4 +- @gradio/atoms@0.9.0-beta.5 + +## 0.8.0-beta.6 + +### Dependency updates + +- @gradio/statustracker@0.8.0-beta.4 +- @gradio/atoms@0.9.0-beta.4 +- @gradio/client@1.6.0-beta.4 +- @gradio/upload@0.13.0-beta.6 + +## 0.8.0-beta.5 + +### Dependency updates + +- @gradio/upload@0.13.0-beta.5 +- @gradio/statustracker@0.8.0-beta.3 +- @gradio/icons@0.8.0-beta.3 +- @gradio/atoms@0.9.0-beta.3 + +## 0.8.0-beta.4 + +### Dependency updates + +- @gradio/statustracker@0.8.0-beta.2 +- @gradio/upload@0.13.0-beta.4 +- @gradio/wasm@0.14.0-beta.3 + +## 0.8.0-beta.3 + +### Dependency updates + +- @gradio/upload@0.13.0-beta.3 +- @gradio/client@1.6.0-beta.3 + +## 0.8.0-beta.2 + +### Features + +- [#9250](https://github.com/gradio-app/gradio/pull/9250) [`350b0a5`](https://github.com/gradio-app/gradio/commit/350b0a5cafb9176f914f62e7c90de51d4352cc77) - Improve Icon Button consistency. Thanks @hannahblair! + +### Dependency updates + +- @gradio/atoms@0.9.0-beta.2 +- @gradio/upload@0.13.0-beta.2 +- @gradio/wasm@0.14.0-beta.2 +- @gradio/client@1.6.0-beta.2 +- @gradio/icons@0.8.0-beta.2 +- @gradio/statustracker@0.8.0-beta.2 +- @gradio/utils@0.7.0-beta.2 + +## 0.7.2-beta.1 + +### Features + +- [#9187](https://github.com/gradio-app/gradio/pull/9187) [`5bf00b7`](https://github.com/gradio-app/gradio/commit/5bf00b7524ebf399b48719120a49d15bb21bd65c) - make all component SSR compatible. Thanks @pngwn! + +### Dependency updates + +- @gradio/atoms@0.8.1-beta.1 +- @gradio/icons@0.8.0-beta.1 +- @gradio/statustracker@0.8.0-beta.1 +- @gradio/utils@0.7.0-beta.1 +- @gradio/client@1.6.0-beta.1 +- @gradio/upload@0.12.4-beta.1 +- @gradio/wasm@0.13.1-beta.1 + +## 0.7.2-beta.0 + +### Fixes + +- [#9163](https://github.com/gradio-app/gradio/pull/9163) [`2b6cbf2`](https://github.com/gradio-app/gradio/commit/2b6cbf25908e42cf027324e54ef2cc0baad11a91) - fix exports and generate types. Thanks @pngwn! + +### Dependency updates + +- @gradio/utils@0.7.0-beta.0 +- @gradio/statustracker@0.8.0-beta.0 +- @gradio/atoms@0.8.1-beta.0 +- @gradio/client@1.6.0-beta.0 +- @gradio/icons@0.8.0-beta.0 +- @gradio/upload@0.12.4-beta.0 +- @gradio/wasm@0.13.1-beta.0 + +## 0.7.1 + +### Features + +- [#9118](https://github.com/gradio-app/gradio/pull/9118) [`e1c404d`](https://github.com/gradio-app/gradio/commit/e1c404da1143fb52b659d03e028bdba1badf443d) - setup npm-previews of all packages. Thanks @pngwn! + +### Dependency updates + +- @gradio/utils@0.6.0 +- @gradio/upload@0.12.3 +- @gradio/atoms@0.8.0 +- @gradio/client@1.5.1 +- @gradio/statustracker@0.7.5 +- @gradio/wasm@0.13.0 +- @gradio/icons@0.7.1 + +## 0.7.0 + +### Features + +- [#8964](https://github.com/gradio-app/gradio/pull/8964) [`bf6bbd9`](https://github.com/gradio-app/gradio/commit/bf6bbd971acddbf78f03bea431ed7d1e0003ccf9) - Add min/max-imize button to gr.Image and gr.Gallery. Thanks @hannahblair! + +### Dependency updates + +- @gradio/atoms@0.7.9 +- @gradio/statustracker@0.7.4 +- @gradio/client@1.5.0 +- @gradio/icons@0.7.0 +- @gradio/upload@0.12.2 + +## 0.6.15 + +### Dependency updates + +- @gradio/atoms@0.7.8 +- @gradio/icons@0.6.1 +- @gradio/utils@0.5.2 +- @gradio/statustracker@0.7.3 +- @gradio/upload@0.12.1 + +## 0.6.14 + +### Dependency updates + +- @gradio/wasm@0.12.0 +- @gradio/client@1.4.0 +- @gradio/statustracker@0.7.2 +- @gradio/upload@0.12.0 +- @gradio/atoms@0.7.7 + +## 0.6.13 + +### Dependency updates + +- @gradio/atoms@0.7.6 +- @gradio/utils@0.5.1 +- @gradio/statustracker@0.7.1 +- @gradio/client@1.3.0 +- @gradio/upload@0.11.5 +- @gradio/icons@0.6.0 + +## 0.6.12 + +### Dependency updates + +- @gradio/upload@0.11.4 +- @gradio/client@1.2.1 + +## 0.6.11 + +### Dependency updates + +- @gradio/atoms@0.7.5 +- @gradio/utils@0.5.0 +- @gradio/icons@0.5.0 +- @gradio/wasm@0.11.0 +- @gradio/client@1.2.0 +- @gradio/statustracker@0.7.0 +- @gradio/upload@0.11.3 + +## 0.6.10 + +### Dependency updates + +- @gradio/client@1.1.1 +- @gradio/upload@0.11.2 + +## 0.6.9 + +### Dependency updates + +- @gradio/upload@0.11.1 +- @gradio/client@1.1.0 + +## 0.6.8 + +### Dependency updates + +- @gradio/statustracker@0.6.0 +- @gradio/client@1.0.0 +- @gradio/upload@0.11.0 + +## 0.6.7 + +### Dependency updates + +- @gradio/upload@0.10.7 +- @gradio/client@0.20.1 + +## 0.6.6 + +### Dependency updates + +- @gradio/client@0.20.0 +- @gradio/statustracker@0.6.0 +- @gradio/upload@0.10.6 + +## 0.6.5 + +### Dependency updates + +- @gradio/utils@0.4.2 +- @gradio/atoms@0.7.4 +- @gradio/statustracker@0.5.5 +- @gradio/upload@0.10.5 +- @gradio/client@0.19.4 + +## 0.6.4 + +### Dependency updates + +- @gradio/client@0.19.3 +- @gradio/statustracker@0.5.4 +- @gradio/upload@0.10.4 + +## 0.6.3 + +### Dependency updates + +- @gradio/upload@0.10.3 +- @gradio/client@0.19.2 + +## 0.6.2 + +### Dependency updates + +- @gradio/statustracker@0.5.3 +- @gradio/client@0.19.1 +- @gradio/upload@0.10.2 + +## 0.6.1 + +### Dependency updates + +- @gradio/atoms@0.7.3 +- @gradio/statustracker@0.5.2 +- @gradio/client@0.19.0 +- @gradio/icons@0.4.1 +- @gradio/upload@0.10.1 + +## 0.6.0 + +### Features + +- [#8121](https://github.com/gradio-app/gradio/pull/8121) [`f5b710c`](https://github.com/gradio-app/gradio/commit/f5b710c919b0ce604ea955f0d5f4faa91095ca4a) - chore(deps): update dependency eslint to v9. Thanks @renovate! + +### Dependency updates + +- @gradio/atoms@0.7.2 +- @gradio/client@0.18.0 +- @gradio/upload@0.10.0 +- @gradio/utils@0.4.1 +- @gradio/wasm@0.10.1 +- @gradio/statustracker@0.5.1 + +## 0.5.13 + +### Fixes + +- [#8066](https://github.com/gradio-app/gradio/pull/8066) [`624f9b9`](https://github.com/gradio-app/gradio/commit/624f9b9477f74a581a6c14119234f9efdfcda398) - make gradio dev tools a local dependency rather than bundling. Thanks @pngwn! + +### Dependency updates + +- @gradio/atoms@0.7.1 +- @gradio/client@0.17.0 +- @gradio/statustracker@0.5.0 +- @gradio/upload@0.9.0 +- @gradio/utils@0.4.0 + +## 0.5.12 + +### Dependency updates + +- @gradio/utils@0.3.2 +- @gradio/statustracker@0.4.12 +- @gradio/client@0.16.0 +- @gradio/upload@0.8.5 +- @gradio/atoms@0.7.0 +- @gradio/icons@0.4.0 + +## 0.5.11 + +### Dependency updates + +- @gradio/utils@0.3.1 +- @gradio/atoms@0.6.2 +- @gradio/statustracker@0.4.11 +- @gradio/upload@0.8.4 +- @gradio/client@0.15.1 + +## 0.5.10 + +### Dependency updates + +- @gradio/upload@0.8.3 +- @gradio/client@0.15.0 + +## 0.5.9 + +### Dependency updates + +- @gradio/atoms@0.6.1 +- @gradio/statustracker@0.4.10 +- @gradio/icons@0.3.4 +- @gradio/upload@0.8.2 + +## 0.5.8 + +### Dependency updates + +- @gradio/upload@0.8.1 +- @gradio/statustracker@0.4.9 +- @gradio/wasm@0.10.0 +- @gradio/atoms@0.6.0 + +## 0.5.7 + +### Dependency updates + +- @gradio/client@0.14.0 +- @gradio/upload@0.8.0 +- @gradio/wasm@0.9.0 + +## 0.5.6 + +### Dependency updates + +- @gradio/upload@0.7.7 +- @gradio/client@0.13.0 +- @gradio/wasm@0.8.0 + +## 0.5.5 + +### Patch Changes + +- Updated dependencies [[`8181695`](https://github.com/gradio-app/gradio/commit/8181695e70187e8bc2bf7518697098c8d1b9843d)]: + - @gradio/upload@0.7.6 + +## 0.5.4 + +### Patch Changes + +- Updated dependencies [[`26356a6`](https://github.com/gradio-app/gradio/commit/26356a623c4196f48ca236d973a597831743cdb8), [`9c6de6d`](https://github.com/gradio-app/gradio/commit/9c6de6d85092c1c9378d7f81e5ec734221536812), [`eda33b3`](https://github.com/gradio-app/gradio/commit/eda33b3763897a542acf298e523fa493dc655aee), [`4b0d589`](https://github.com/gradio-app/gradio/commit/4b0d58933057432758a54169a360eb352903d6b4), [`561579d`](https://github.com/gradio-app/gradio/commit/561579d9b7b860c5cb3f8131e0dced0c8114463f)]: + - @gradio/upload@0.7.5 + - @gradio/wasm@0.7.0 + - @gradio/client@0.12.2 + +## 0.5.3 + +### Patch Changes + +- Updated dependencies [[`98a2719`](https://github.com/gradio-app/gradio/commit/98a2719bfb9c64338caf9009891b6c6b0b33ea89)]: + - @gradio/statustracker@0.4.8 + +## 0.5.2 + +### Patch Changes + +- Updated dependencies [[`f191786`](https://github.com/gradio-app/gradio/commit/f1917867916647d383b8d7ce15e0c17f2abbdec1)]: + - @gradio/icons@0.3.3 + - @gradio/atoms@0.5.3 + - @gradio/statustracker@0.4.7 + - @gradio/upload@0.7.4 + +## 0.5.1 + +### Patch Changes + +- Updated dependencies [[`065c5b1`](https://github.com/gradio-app/gradio/commit/065c5b163c4badb9d9cbd06d627fb4ba086003e7), [`32b317f`](https://github.com/gradio-app/gradio/commit/32b317f24e3d43f26684bb9f3964f31efd0ea556)]: + - @gradio/utils@0.3.0 + - @gradio/client@0.12.1 + - @gradio/atoms@0.5.2 + - @gradio/statustracker@0.4.6 + - @gradio/upload@0.7.3 + +## 0.5.0 + +### Features + +- [#7183](https://github.com/gradio-app/gradio/pull/7183) [`49d9c48`](https://github.com/gradio-app/gradio/commit/49d9c48537aa706bf72628e3640389470138bdc6) - [WIP] Refactor file normalization to be in the backend and remove it from the frontend of each component. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 0.4.4 + +### Patch Changes + +- Updated dependencies [[`572e360`](https://github.com/gradio-app/gradio/commit/572e360fff4a03c335b86e1a7517a44cb6af2bcd), [`68a54a7`](https://github.com/gradio-app/gradio/commit/68a54a7a310d8d7072fdae930bf1cfdf12c45a7f), [`fdd1521`](https://github.com/gradio-app/gradio/commit/fdd15213c24b9cbc58bbc1b6beb4af7c18f48557), [`c3e61e4`](https://github.com/gradio-app/gradio/commit/c3e61e4f70696a71aede67b65d28447eb67daf16), [`05d8a3c`](https://github.com/gradio-app/gradio/commit/05d8a3c8030b733bd47250f5db6f89f230f9a707)]: + - @gradio/upload@0.7.1 + - @gradio/client@0.11.0 + - @gradio/utils@0.2.2 + - @gradio/wasm@0.6.0 + - @gradio/atoms@0.5.1 + - @gradio/statustracker@0.4.5 + +## 0.4.3 + +### Patch Changes + +- Updated dependencies [[`5727b92`](https://github.com/gradio-app/gradio/commit/5727b92abc8a00a675bfc0a921b38de771af947b), [`bc2cdc1`](https://github.com/gradio-app/gradio/commit/bc2cdc1df95b38025486cf76df4a494b66d98585), [`c60ad4d`](https://github.com/gradio-app/gradio/commit/c60ad4d34ab5b56a89bf6796822977e51e7a4a32), [`be56c76`](https://github.com/gradio-app/gradio/commit/be56c76c7b5d2814ea8239c7dbeddc4b1d3701c4), [`8c355a4`](https://github.com/gradio-app/gradio/commit/8c355a47844296e3aab250fe61e2ecc706122e78)]: + - @gradio/utils@0.2.1 + - @gradio/upload@0.7.0 + - @gradio/atoms@0.5.0 + - @gradio/wasm@0.5.1 + - @gradio/statustracker@0.4.4 + +## 0.4.2 + +### Patch Changes + +- Updated dependencies [[`3c3cf86`](https://github.com/gradio-app/gradio/commit/3c3cf8618a8cad1ef66a7f96664923d2c9f5e0e2), [`3f139c7`](https://github.com/gradio-app/gradio/commit/3f139c7c995f749562bb007d2a567bb167669de9)]: + - @gradio/client@0.10.1 + - @gradio/upload@0.6.1 + +## 0.4.1 + +### Patch Changes + +- Updated dependencies [[`793bf8f`](https://github.com/gradio-app/gradio/commit/793bf8f7b1943f265c5d016c1a0c682ee549232a), [`5d00dd3`](https://github.com/gradio-app/gradio/commit/5d00dd37ca14bbfef2ceac550b29dbe05ba8cab0)]: + - @gradio/upload@0.6.0 + - @gradio/wasm@0.5.0 + +## 0.4.0 + +### Features + +- [#6133](https://github.com/gradio-app/gradio/pull/6133) [`f742d0e`](https://github.com/gradio-app/gradio/commit/f742d0e861c8e25c5d77d9102c9d50f94b0d3383) - Lite: Support AnnotatedImage on Wasm. Thanks [@whitphx](https://github.com/whitphx)! + +## 0.3.14 + +### Patch Changes + +- Updated dependencies [[`d406855`](https://github.com/gradio-app/gradio/commit/d4068557953746662235d595ec435c42ceb24414)]: + - @gradio/client@0.9.4 + - @gradio/upload@0.5.7 + +## 0.3.13 + +### Patch Changes + +- Updated dependencies [[`828fb9e`](https://github.com/gradio-app/gradio/commit/828fb9e6ce15b6ea08318675a2361117596a1b5d), [`73268ee`](https://github.com/gradio-app/gradio/commit/73268ee2e39f23ebdd1e927cb49b8d79c4b9a144)]: + - @gradio/client@0.9.3 + - @gradio/statustracker@0.4.3 + - @gradio/atoms@0.4.1 + - @gradio/upload@0.5.6 + +## 0.3.12 + +### Patch Changes + +- Updated dependencies [[`245d58e`](https://github.com/gradio-app/gradio/commit/245d58eff788e8d44a59d37a2d9b26d0f08a62b4)]: + - @gradio/client@0.9.2 + - @gradio/upload@0.5.5 + +## 0.3.11 + +### Patch Changes + +- Updated dependencies [[`5d51fbc`](https://github.com/gradio-app/gradio/commit/5d51fbce7826da840a2fd4940feb5d9ad6f1bc5a), [`34f9431`](https://github.com/gradio-app/gradio/commit/34f943101bf7dd6b8a8974a6131c1ed7c4a0dac0)]: + - @gradio/upload@0.5.4 + - @gradio/client@0.9.1 + +## 0.3.10 + +### Patch Changes + +- Updated dependencies [[`6a9151d`](https://github.com/gradio-app/gradio/commit/6a9151d5c9432c724098da7d88a539aaaf5ffe88), [`d76bcaa`](https://github.com/gradio-app/gradio/commit/d76bcaaaf0734aaf49a680f94ea9d4d22a602e70), [`67ddd40`](https://github.com/gradio-app/gradio/commit/67ddd40b4b70d3a37cb1637c33620f8d197dbee0), [`053bec9`](https://github.com/gradio-app/gradio/commit/053bec98be1127e083414024e02cf0bebb0b5142), [`4d1cbbc`](https://github.com/gradio-app/gradio/commit/4d1cbbcf30833ef1de2d2d2710c7492a379a9a00)]: + - @gradio/upload@0.5.3 + - @gradio/client@0.9.0 + - @gradio/icons@0.3.2 + - @gradio/atoms@0.4.0 + - @gradio/statustracker@0.4.2 + +## 0.3.9 + +### Patch Changes + +- Updated dependencies [[`206af31`](https://github.com/gradio-app/gradio/commit/206af31d7c1a31013364a44e9b40cf8df304ba50)]: + - @gradio/icons@0.3.1 + - @gradio/atoms@0.3.1 + - @gradio/statustracker@0.4.1 + - @gradio/upload@0.5.2 + +## 0.3.8 + +### Patch Changes + +- Updated dependencies [[`71f1a1f99`](https://github.com/gradio-app/gradio/commit/71f1a1f9931489d465c2c1302a5c8d768a3cd23a)]: + - @gradio/client@0.8.2 + - @gradio/upload@0.5.1 + +## 0.3.7 + +### Patch Changes + +- Updated dependencies [[`9caddc17b`](https://github.com/gradio-app/gradio/commit/9caddc17b1dea8da1af8ba724c6a5eab04ce0ed8)]: + - @gradio/atoms@0.3.0 + - @gradio/icons@0.3.0 + - @gradio/statustracker@0.4.0 + - @gradio/upload@0.5.0 + +## 0.3.6 + +### Patch Changes + +- Updated dependencies [[`2f805a7dd`](https://github.com/gradio-app/gradio/commit/2f805a7dd3d2b64b098f659dadd5d01258290521), [`f816136a0`](https://github.com/gradio-app/gradio/commit/f816136a039fa6011be9c4fb14f573e4050a681a)]: + - @gradio/upload@0.4.2 + - @gradio/atoms@0.2.2 + - @gradio/icons@0.2.1 + - @gradio/statustracker@0.3.2 + +## 0.3.5 + +### Patch Changes + +- Updated dependencies [[`324867f63`](https://github.com/gradio-app/gradio/commit/324867f63c920113d89a565892aa596cf8b1e486)]: + - @gradio/client@0.8.1 + - @gradio/upload@0.4.1 + +## 0.3.4 + +### Patch Changes + +- Updated dependencies [[`854b482f5`](https://github.com/gradio-app/gradio/commit/854b482f598e0dc47673846631643c079576da9c), [`f1409f95e`](https://github.com/gradio-app/gradio/commit/f1409f95ed39c5565bed6a601e41f94e30196a57)]: + - @gradio/upload@0.4.0 + - @gradio/client@0.8.0 + +## 0.3.3 + +### Patch Changes + +- Updated dependencies [[`bca6c2c80`](https://github.com/gradio-app/gradio/commit/bca6c2c80f7e5062427019de45c282238388af95), [`3cdeabc68`](https://github.com/gradio-app/gradio/commit/3cdeabc6843000310e1a9e1d17190ecbf3bbc780), [`fad92c29d`](https://github.com/gradio-app/gradio/commit/fad92c29dc1f5cd84341aae417c495b33e01245f)]: + - @gradio/client@0.7.2 + - @gradio/atoms@0.2.1 + - @gradio/upload@0.3.3 + - @gradio/statustracker@0.3.1 + +## 0.3.2 + +### Patch Changes + +- Updated dependencies [[`aaa55ce85`](https://github.com/gradio-app/gradio/commit/aaa55ce85e12f95aba9299445e9c5e59824da18e)]: + - @gradio/upload@0.3.2 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [[`2ba14b284`](https://github.com/gradio-app/gradio/commit/2ba14b284f908aa13859f4337167a157075a68eb)]: + - @gradio/client@0.7.1 + - @gradio/upload@0.3.1 + +## 0.3.0 + +### Features + +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - fix circular dependency with client + upload. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Clean root url. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Image v4. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Custom components. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.3.0-beta.2 + +### Features + +- [#6143](https://github.com/gradio-app/gradio/pull/6143) [`e4f7b4b40`](https://github.com/gradio-app/gradio/commit/e4f7b4b409323b01aa01b39e15ce6139e29aa073) - fix circular dependency with client + upload. Thanks [@pngwn](https://github.com/pngwn)! +- [#6094](https://github.com/gradio-app/gradio/pull/6094) [`c476bd5a5`](https://github.com/gradio-app/gradio/commit/c476bd5a5b70836163b9c69bf4bfe068b17fbe13) - Image v4. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.3.0-beta.1 + +### Features + +- [#6016](https://github.com/gradio-app/gradio/pull/6016) [`83e947676`](https://github.com/gradio-app/gradio/commit/83e947676d327ca2ab6ae2a2d710c78961c771a0) - Format js in v4 branch. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.3.0-beta.0 + +### Features + +- [#5960](https://github.com/gradio-app/gradio/pull/5960) [`319c30f3f`](https://github.com/gradio-app/gradio/commit/319c30f3fccf23bfe1da6c9b132a6a99d59652f7) - rererefactor frontend files. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [[`e70805d54`](https://github.com/gradio-app/gradio/commit/e70805d54cc792452545f5d8eccc1aa0212a4695)]: + - @gradio/atoms@0.2.0 + - @gradio/statustracker@0.2.3 + - @gradio/upload@0.3.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies []: + - @gradio/utils@0.1.2 + - @gradio/atoms@0.1.4 + - @gradio/statustracker@0.2.2 + - @gradio/upload@0.3.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`8f0fed857`](https://github.com/gradio-app/gradio/commit/8f0fed857d156830626eb48b469d54d211a582d2)]: + - @gradio/icons@0.2.0 + - @gradio/atoms@0.1.3 + - @gradio/statustracker@0.2.1 + - @gradio/upload@0.3.1 + +## 0.2.0 + +### Features + +- [#5554](https://github.com/gradio-app/gradio/pull/5554) [`75ddeb390`](https://github.com/gradio-app/gradio/commit/75ddeb390d665d4484667390a97442081b49a423) - Accessibility Improvements. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 0.1.2 + +### Patch Changes + +- Updated dependencies [[`afac0006`](https://github.com/gradio-app/gradio/commit/afac0006337ce2840cf497cd65691f2f60ee5912)]: + - @gradio/statustracker@0.2.0 + - @gradio/utils@0.1.1 + - @gradio/atoms@0.1.2 + - @gradio/upload@0.2.1 + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [[`abf1c57d`](https://github.com/gradio-app/gradio/commit/abf1c57d7d85de0df233ee3b38aeb38b638477db), [`79d8f9d8`](https://github.com/gradio-app/gradio/commit/79d8f9d891901683c5a1b7486efb44eab2478c96)]: + - @gradio/icons@0.1.0 + - @gradio/utils@0.1.0 + - @gradio/upload@0.2.0 + - @gradio/atoms@0.1.1 + - @gradio/statustracker@0.1.1 + +## 0.1.0 + +### Highlights + +#### Improve startup performance and markdown support ([#5279](https://github.com/gradio-app/gradio/pull/5279) [`fe057300`](https://github.com/gradio-app/gradio/commit/fe057300f0672c62dab9d9b4501054ac5d45a4ec)) + +##### Improved markdown support + +We now have better support for markdown in `gr.Markdown` and `gr.Dataframe`. Including syntax highlighting and Github Flavoured Markdown. We also have more consistent markdown behaviour and styling. + +##### Various performance improvements + +These improvements will be particularly beneficial to large applications. + +- Rather than attaching events manually, they are now delegated, leading to a significant performance improvement and addressing a performance regression introduced in a recent version of Gradio. App startup for large applications is now around twice as fast. +- Optimised the mounting of individual components, leading to a modest performance improvement during startup (~30%). +- Corrected an issue that was causing markdown to re-render infinitely. +- Ensured that the `gr.3DModel` does re-render prematurely. + +Thanks [@pngwn](https://github.com/pngwn)! + +### Features + +- [#5215](https://github.com/gradio-app/gradio/pull/5215) [`fbdad78a`](https://github.com/gradio-app/gradio/commit/fbdad78af4c47454cbb570f88cc14bf4479bbceb) - Lazy load interactive or static variants of a component individually, rather than loading both variants regardless. This change will improve performance for many applications. Thanks [@pngwn](https://github.com/pngwn)! +- [#5216](https://github.com/gradio-app/gradio/pull/5216) [`4b58ea6d`](https://github.com/gradio-app/gradio/commit/4b58ea6d98e7a43b3f30d8a4cb6f379bc2eca6a8) - Update i18n tokens and locale files. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 0.0.2 + +### Patch Changes + +- Updated dependencies [[`667875b2`](https://github.com/gradio-app/gradio/commit/667875b2441753e74d25bd9d3c8adedd8ede11cd)]: + - @gradio/upload@0.0.3 \ No newline at end of file diff --git a/js/annotatedimage/Index.svelte b/js/annotatedimage/Index.svelte new file mode 100644 index 0000000..1a85305 --- /dev/null +++ b/js/annotatedimage/Index.svelte @@ -0,0 +1,193 @@ + + + + + + +
+ {#if gradio.props.value == null} + + {:else} +
+ { + gradio.dispatch("custom_button_click", { id }); + }} + > + {#if (gradio.props.buttons || []).some((btn) => typeof btn === "string" && btn === "fullscreen")} + (fullscreen = fs)} + /> + {/if} + + + the base file that is annotated + {#each gradio.props.value ? gradio.props.value.annotations : [] as ann, i} + segmentation mask identifying {gradio.shared
+							.label} within the uploaded file + {/each} +
+ {#if gradio.props.show_legend && gradio.props.value} +
+ {#each gradio.props.value.annotations as ann, i} + + {/each} +
+ {/if} + {/if} +
+
+ + diff --git a/js/annotatedimage/package.json b/js/annotatedimage/package.json new file mode 100644 index 0000000..fef341b --- /dev/null +++ b/js/annotatedimage/package.json @@ -0,0 +1,37 @@ +{ + "name": "@gradio/annotatedimage", + "version": "0.12.1", + "description": "Gradio UI packages", + "type": "module", + "author": "", + "license": "ISC", + "private": false, + "main_changeset": true, + "exports": { + ".": { + "gradio": "./Index.svelte", + "svelte": "./dist/Index.svelte", + "types": "./dist/Index.svelte.d.ts" + }, + "./package.json": "./package.json" + }, + "devDependencies": { + "@gradio/preview": "workspace:^" + }, + "peerDependencies": { + "svelte": "^5.48.0" + }, + "dependencies": { + "@gradio/atoms": "workspace:^", + "@gradio/icons": "workspace:^", + "@gradio/statustracker": "workspace:^", + "@gradio/upload": "workspace:^", + "@gradio/utils": "workspace:^", + "@gradio/client": "workspace:^" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/gradio-app/gradio.git", + "directory": "js/annotatedimage" + } +} diff --git a/js/annotatedimage/types.ts b/js/annotatedimage/types.ts new file mode 100644 index 0000000..a67663c --- /dev/null +++ b/js/annotatedimage/types.ts @@ -0,0 +1,27 @@ +import type { FileData } from "@gradio/client"; +import type { CustomButton } from "@gradio/utils"; + +export interface Annotation { + image: FileData; + label: string; +} + +export interface AnnotatedImageValue { + image: FileData; + annotations: Annotation[]; +} + +export interface AnnotatedImageProps { + value: AnnotatedImageValue | null; + show_legend: boolean; + height: number | undefined; + width: number | undefined; + color_map: Record; + buttons: (string | CustomButton)[]; +} + +export interface AnnotatedImageEvents { + change: never; + select: { index: number; value: string }; + custom_button_click: { id: number }; +} diff --git a/js/app/.gitignore b/js/app/.gitignore new file mode 100644 index 0000000..79518f7 --- /dev/null +++ b/js/app/.gitignore @@ -0,0 +1,21 @@ +node_modules + +# Output +.output +.vercel +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/js/app/CHANGELOG.md b/js/app/CHANGELOG.md new file mode 100644 index 0000000..ba5f7ce --- /dev/null +++ b/js/app/CHANGELOG.md @@ -0,0 +1,2944 @@ +# @self/app + +## 2.2.2 + +### Dependency updates + +- @gradio/client@2.3.1 +- @gradio/core@1.9.0 + +## 2.2.2 + +### Dependency updates + +- @gradio/client@2.3.0 +- @gradio/theme@0.6.2 +- @gradio/core@1.8.0 + +## 2.2.2 + +### Dependency updates + +- @gradio/client@2.2.2 + +## 2.2.2 + +### Fixes + +- [#13450](https://github.com/gradio-app/gradio/pull/13450) [`dba4a16`](https://github.com/gradio-app/gradio/commit/dba4a168ce00c15497fd2b99a3364ebbb4842b72) - preserve head script execution order. Thanks @hysts! + +## 2.2.1 + +### Fixes + +- [#13463](https://github.com/gradio-app/gradio/pull/13463) [`58088ad`](https://github.com/gradio-app/gradio/commit/58088ad1d75eee0f93ba1c02f427ee04f60847b4) - Self-host frontend assets so that Gradio works offline!. Thanks @abidlabs! + +### Dependency updates + +- @gradio/core@1.6.0 + +## 2.2.0 + +### Features + +- [#13366](https://github.com/gradio-app/gradio/pull/13366) [`10f43e0`](https://github.com/gradio-app/gradio/commit/10f43e0fe187aac9594f3a9765f8addc81b63ad1) - Offload traffic to static workers and use node as the proxy. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/client@2.2.1 +- @gradio/core@1.5.1 + +## 2.1.1 + +### Dependency updates + +- @gradio/core@1.5.0 + +## 2.1.1 + +### Fixes + +- [#13239](https://github.com/gradio-app/gradio/pull/13239) [`9efdcb5`](https://github.com/gradio-app/gradio/commit/9efdcb57b46fc808dc55051a113154489fcc237d) - Reduce `gradio` package size by restoring frontend settings. Thanks @abidlabs! +- [#13210](https://github.com/gradio-app/gradio/pull/13210) [`4005b93`](https://github.com/gradio-app/gradio/commit/4005b93dd59d7e0d144619986f01e900659b3d0b) - Fix ZeroGPU handling for `gr.Server`. Thanks @abidlabs! + +### Dependency updates + +- @gradio/client@2.2.0 +- @gradio/core@1.4.2 + +## 2.1.0 + +### Dependency updates + +- @gradio/core@1.4.2 + +## 2.1.0 + +### Dependency updates + +- @gradio/core@1.4.1 + +## 2.1.0 + +### Features + +- [#12879](https://github.com/gradio-app/gradio/pull/12879) [`c498688`](https://github.com/gradio-app/gradio/commit/c4986883b267570d76b442899c6fc09d14e3e222) - Ensure svelte version mismatches do not break custom components. Thanks @pngwn! + +### Dependency updates + +- @gradio/core@1.4.0 +- @self/build@0.6.0 + +## 2.0.6 + +### Dependency updates + +- @gradio/client@2.1.0 + +## 2.0.6 + +### Fixes + +- [#12797](https://github.com/gradio-app/gradio/pull/12797) [`6a0c6ea`](https://github.com/gradio-app/gradio/commit/6a0c6eae53931ec137c0b8379428acc8a7ea27c9) - Refactor translation logic. Thanks @hannahblair! + +### Dependency updates + +- @gradio/core@1.2.0 + +## 2.0.5 + +### Fixes + +- [#12828](https://github.com/gradio-app/gradio/pull/12828) [`151cbd1`](https://github.com/gradio-app/gradio/commit/151cbd1aac0da3aeb5f0b7b33585223d2bc47138) - Fix private spaces. Thanks @freddyaboulton! +- [#12835](https://github.com/gradio-app/gradio/pull/12835) [`5ecf6d2`](https://github.com/gradio-app/gradio/commit/5ecf6d27c50a20e2329c1aca0634924479ceb6cd) - Fix CSS root in spaces. Thanks @freddyaboulton! +- [#12817](https://github.com/gradio-app/gradio/pull/12817) [`05acc66`](https://github.com/gradio-app/gradio/commit/05acc6627de866db6a742e0540c2733041d76a86) - Fix Login. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/client@2.0.4 +- @gradio/core@1.1.3 + +## 2.0.4 + +### Fixes + +- [#12800](https://github.com/gradio-app/gradio/pull/12800) [`7a1c321`](https://github.com/gradio-app/gradio/commit/7a1c321b6546ba05a353488f5133e8262c4a8a39) - Bump svelte/kit for security reasons. Thanks @freddyaboulton! +- [#12779](https://github.com/gradio-app/gradio/pull/12779) [`ea2d3e9`](https://github.com/gradio-app/gradio/commit/ea2d3e985a8b42d188e551f517c5825c00790628) - Migrate Audio + Upload + Atoms to Svelte 5. Thanks @dawoodkhan82! +- [#12472](https://github.com/gradio-app/gradio/pull/12472) [`9a2bc0d`](https://github.com/gradio-app/gradio/commit/9a2bc0dacdd2b3f670fae815093c61ad08eee7e3) - Re-enable SSR mode. Thanks @pngwn! + +### Dependency updates + +- @gradio/client@2.0.3 +- @gradio/theme@0.6.1 +- @gradio/core@1.1.2 +- @self/build@0.5.2 + +## 2.0.3 + +### Fixes + +- [#12679](https://github.com/gradio-app/gradio/pull/12679) [`45cb6be`](https://github.com/gradio-app/gradio/commit/45cb6be9bedfbfcdad9b3ac4df26342f29143c73) - Fix SSR mode styling by enabling PostCSS custom media preprocessing. Thanks @abidlabs! + +### Dependency updates + +- @gradio/client@2.0.2 +- @gradio/theme@0.6.0 +- @gradio/core@1.1.1 +- @self/build@0.5.1 + +## 2.0.2 + +### Dependency updates + +- @gradio/client@2.0.1 +- @gradio/core@1.1.0 + +## 2.0.2 + +### Fixes + +- [#12508](https://github.com/gradio-app/gradio/pull/12508) [`0715279`](https://github.com/gradio-app/gradio/commit/0715279005cbd6fea3c8081996aa5afddbd27e34) - Fix custom `js` param. Thanks @dawoodkhan82! + +### Dependency updates + +- @gradio/core@1.0.2 + +## 2.0.1 + +### Fixes + +- [#12416](https://github.com/gradio-app/gradio/pull/12416) [`7e867fd`](https://github.com/gradio-app/gradio/commit/7e867fde9070849fb474a08544c72f2522a9cbe9) - Fix custom components for gradio 6. Thanks @pngwn! +- [#12461](https://github.com/gradio-app/gradio/pull/12461) [`9a86e80`](https://github.com/gradio-app/gradio/commit/9a86e8064787029bc20b086c6f3191879f786e0f) - Fix Login Gradio 6. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/core@1.0.1 + +## 2.0.0 + +### Features + +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Fix browser component tests +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Fix full width toast issue +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Rename show_api +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Fix Reload Mode in 6.0 +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Reduce files in build +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - fix custom components +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - gr.HTML "custom components" +- [#12438](https://github.com/gradio-app/gradio/pull/12438) [`25ffc03`](https://github.com/gradio-app/gradio/commit/25ffc0398f8feb43d817c02b2ab970c16de6d797) - Svelte5 migration and bugfix + +### Dependencies + +- @gradio/client@2.0.0 +- @gradio/core@1.0.0 +- @gradio/theme@0.5.0 +- @self/build@0.5.0 + +## 2.0.0-dev.3 + +### Features + +- [#12383](https://github.com/gradio-app/gradio/pull/12383) [`6505763`](https://github.com/gradio-app/gradio/commit/650576399bad10f03aaa03dd9437f03b47ba378b) - Fix Reload Mode in 6.0. Thanks @freddyaboulton! +- [#12381](https://github.com/gradio-app/gradio/pull/12381) [`36d5657`](https://github.com/gradio-app/gradio/commit/36d5657128a81739a78d6b11b3f5a632a9027ab1) - Fix full width toast issue. Thanks @freddyaboulton! +- [#12391](https://github.com/gradio-app/gradio/pull/12391) [`5ba7e1e`](https://github.com/gradio-app/gradio/commit/5ba7e1e7d502017551c5899db175bcc9c6fb5eaa) - Reduce files in build. Thanks @freddyaboulton! +- [#12397](https://github.com/gradio-app/gradio/pull/12397) [`f2a47fe`](https://github.com/gradio-app/gradio/commit/f2a47fe4f155a1ed93ad3e8f18504e9427ba61d2) - fix custom components. Thanks @pngwn! + +### Dependency updates + +- @gradio/core@1.0.0-dev.3 +- @gradio/client@2.0.0-dev.2 +- @self/build@0.5.0-dev.1 + +## 2.0.0-dev.2 + +### Features + +- [#12098](https://github.com/gradio-app/gradio/pull/12098) [`3965abe`](https://github.com/gradio-app/gradio/commit/3965abe584fef88e23501ffb67145fd47e517633) - gr.HTML "custom components". Thanks @aliabid94! + +### Dependency updates + +- @gradio/theme@0.5.0-dev.0 +- @gradio/core@1.0.0-dev.1 +- @self/build@0.4.1-dev.0 + +## 2.0.0-dev.1 + +### Features + +- [#12069](https://github.com/gradio-app/gradio/pull/12069) [`9de88ca`](https://github.com/gradio-app/gradio/commit/9de88ca470ce529366d259f0deaa955f658000b9) - Rename show_api. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/client@2.0.0-dev.1 +- @gradio/core@1.0.0-dev.0 + +## 1.52.2-dev.0 + +### Dependency updates + +- @gradio/client@2.0.0-dev.0 +- @gradio/core@0.29.2 + +## 1.52.1 + +### Dependency updates + +- @gradio/client@1.19.1 + +## 1.52.1 + +### Fixes + +- [#11972](https://github.com/gradio-app/gradio/pull/11972) [`cc27cd7`](https://github.com/gradio-app/gradio/commit/cc27cd765559a047efffe1073063cad0e071de0c) - Fix postMessage warning when running outside hugging face spaces. Thanks @freddyaboulton! + +## 1.52.0 + +### Features + +- [#11858](https://github.com/gradio-app/gradio/pull/11858) [`3f8ea13`](https://github.com/gradio-app/gradio/commit/3f8ea13a8ca92abf0ad34392e403a449fda3c6c2) - remove lite. Thanks @pngwn! + +### Dependency updates + +- @gradio/client@1.19.0 +- @gradio/core@0.29.0 +- @self/build@0.4.0 + +## 1.51.0 + +### Features + +- [#11833](https://github.com/gradio-app/gradio/pull/11833) [`a446fcb`](https://github.com/gradio-app/gradio/commit/a446fcba6f3fe59c32194beb7f27fb6f80b61347) - Add gr.Navbar component for multipage apps. Thanks @abidlabs! + +### Fixes + +- [#11815](https://github.com/gradio-app/gradio/pull/11815) [`1a477c5`](https://github.com/gradio-app/gradio/commit/1a477c5202c13097d1089fb70a32a08db22d7660) - Fix i18n string visible during load and i18n not respecting HTML. Thanks @freddyaboulton! +- [#11749](https://github.com/gradio-app/gradio/pull/11749) [`70f4532`](https://github.com/gradio-app/gradio/commit/70f4532a4dc7576dbdbe1d0a43a05644a0dfcf43) - fix various iFrame related UI issues when deploying to spaces. Thanks @pngwn! + +### Dependency updates + +- @gradio/core@0.28.0 +- @gradio/client@1.18.0 +- @self/build@0.3.0 + +## 1.50.9 + +### Dependency updates + +- @gradio/client@1.17.1 +- @gradio/core@0.27.0 + +## 1.50.8 + +### Dependency updates + +- @gradio/client@1.17.0 +- @gradio/core@0.26.0 + +## 1.50.7 + +### Dependency updates + +- @gradio/client@1.16.0 +- @gradio/core@0.25.0 + +## 1.50.6 + +### Dependency updates + +- @gradio/client@1.15.7 +- @gradio/core@0.24.0 + +## 1.50.5 + +### Dependency updates + +- @gradio/core@0.23.2 + +## 1.50.4 + +### Features + +- [#11615](https://github.com/gradio-app/gradio/pull/11615) [`e2b66d7`](https://github.com/gradio-app/gradio/commit/e2b66d718f3a8f57b6ee224502849ee737b1b120) - fix change events for hidden components. Thanks @pngwn! + +### Dependency updates + +- @gradio/core@0.23.1 + +## 1.50.3 + +### Features + +- [#11543](https://github.com/gradio-app/gradio/pull/11543) [`ac95ac0`](https://github.com/gradio-app/gradio/commit/ac95ac0d8c2e65d1632376e632fb7d16131334b6) - Connection handling messaging. Thanks @aliabid94! +- [#11584](https://github.com/gradio-app/gradio/pull/11584) [`78428cb`](https://github.com/gradio-app/gradio/commit/78428cb29bf6dc66d583b7cf93dd404aef737e75) - Fix reload mode. Thanks @aliabid94! + +### Dependency updates + +- @gradio/core@0.23.0 +- @gradio/client@1.15.6 + +## 1.50.2 + +### Fixes + +- [#11504](https://github.com/gradio-app/gradio/pull/11504) [`1bb635d`](https://github.com/gradio-app/gradio/commit/1bb635d7721232dd715478a1c52620771dca1a20) - Fix fonts in SSR mode. Thanks @dawoodkhan82! + +### Dependency updates + +- @gradio/core@0.22.0 + +## 1.50.1 + +### Features + +- [#11539](https://github.com/gradio-app/gradio/pull/11539) [`6b6d95b`](https://github.com/gradio-app/gradio/commit/6b6d95b6409f9136233ba1d4f55a1cab9e040070) - Update @huggingface/space-header to 1.0.4. Thanks @aayush4vedi! + +## 1.50.0 + +### Features + +- [#11427](https://github.com/gradio-app/gradio/pull/11427) [`6b2bcd0`](https://github.com/gradio-app/gradio/commit/6b2bcd097ae5ef999a7fb273ecf7c7e4c0eab305) - Improve load times of the Gradio front-end. Thanks @pngwn! +- [#11511](https://github.com/gradio-app/gradio/pull/11511) [`1a99336`](https://github.com/gradio-app/gradio/commit/1a99336043076e7b0a4c4b07f97082f0361882f4) - Fix SSR. Thanks @pngwn! + +### Fixes + +- [#11435](https://github.com/gradio-app/gradio/pull/11435) [`50d43a8`](https://github.com/gradio-app/gradio/commit/50d43a8e8bc2a4862668a5d46a5e6482b4f75fe3) - SSR Auth Fix. Thanks @dawoodkhan82! + +### Dependency updates + +- @gradio/client@1.15.5 +- @gradio/core@0.21.0 + +## 1.49.16 + +### Fixes + +- [#11421](https://github.com/gradio-app/gradio/pull/11421) [`c2acf6e`](https://github.com/gradio-app/gradio/commit/c2acf6e33025fe7bbfe0660c182006651cc95090) - Preserve value in reload mode. Thanks @aliabid94! + +### Dependency updates + +- @gradio/client@1.15.4 +- @gradio/core@0.20.0 + +## 1.49.15 + +### Fixes + +- [#11414](https://github.com/gradio-app/gradio/pull/11414) [`b2b40b6`](https://github.com/gradio-app/gradio/commit/b2b40b633d3aae5e2538198f116abd57aab6eea4) - Fixes white background appears while a Gradio app with SSR is loading. Thanks @dawoodkhan82! + +### Dependency updates + +- @gradio/core@0.19.3 + +## 1.49.14 + +### Fixes + +- [#11387](https://github.com/gradio-app/gradio/pull/11387) [`8245afc`](https://github.com/gradio-app/gradio/commit/8245afc669501e1e5f0d619f452455f68a3b7667) - Define root URL in frontend. Thanks @aliabid94! + +### Dependency updates + +- @gradio/client@1.15.3 +- @gradio/core@0.19.3 + +## 1.49.13 + +### Dependency updates + +- @gradio/core@0.19.2 + +## 1.49.12 + +### Dependency updates + +- @gradio/client@1.15.2 +- @gradio/core@0.19.1 + +## 1.49.11 + +### Dependency updates + +- @gradio/core@0.19.0 + +## 1.49.10 + +### Dependency updates + +- @gradio/client@1.15.1 +- @gradio/core@0.18.1 + +## 1.49.9 + +### Dependency updates + +- @gradio/core@0.18.0 + +## 1.49.8 + +### Dependency updates + +- @gradio/client@1.15.0 +- @gradio/core@0.17.0 + +## 1.49.7 + +### Dependency updates + +- @gradio/core@0.16.1 + +## 1.49.6 + +### Dependency updates + +- @gradio/core@0.16.0 + +## 1.49.5 + +### Dependency updates + +- @gradio/core@0.15.1 + +## 1.49.4 + +### Dependency updates + +- @gradio/core@0.15.0 + +## 1.49.3 + +### Dependency updates + +- @gradio/core@0.14.1 + +## 1.49.2 + +### Dependency updates + +- @gradio/client@1.14.2 +- @gradio/core@0.14.0 + +## 1.49.1 + +### Fixes + +- [#10968](https://github.com/gradio-app/gradio/pull/10968) [`238702a`](https://github.com/gradio-app/gradio/commit/238702a709c23f9c1b6302eb143c7cb71a3a066b) - Fix Default meta social tags + Add ability to override existing meta tags. Thanks @dawoodkhan82! + +### Dependency updates + +- @gradio/core@0.14.0 + +## 1.49.0 + +### Features + +- [#10635](https://github.com/gradio-app/gradio/pull/10635) [`2f68c9d`](https://github.com/gradio-app/gradio/commit/2f68c9d988dcbc53a0b8e53bdb1de49c9c8c65d8) - Refactor and redesign `ImageEditor` component. Thanks @pngwn! + +### Dependency updates + +- @gradio/core@0.13.2 + +## 1.48.1 + +### Dependency updates + +- @gradio/wasm@0.18.1 +- @gradio/client@1.14.1 +- @gradio/core@0.13.2 + +## 1.48.0 + +### Features + +- [#10834](https://github.com/gradio-app/gradio/pull/10834) [`c05610c`](https://github.com/gradio-app/gradio/commit/c05610c87dd7f9e9fe5d0aed2fe93e40fdd32648) - Add Deep Links. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/client@1.14.0 +- @gradio/core@0.13.1 +- @gradio/wasm@0.18.0 + +## 1.47.7 + +### Dependency updates + +- @gradio/core@0.13.0 + +## 1.47.6 + +### Dependency updates + +- @gradio/client@1.13.1 +- @gradio/wasm@0.17.4 +- @gradio/core@0.12.1 + +## 1.47.5 + +### Dependency updates + +- @gradio/client@1.13.0 +- @gradio/core@0.12.0 + +## 1.47.4 + +### Dependency updates + +- @gradio/core@0.11.1 + +## 1.47.3 + +### Dependency updates + +- @gradio/core@0.11.0 + +## 1.47.2 + +### Dependency updates + +- @gradio/core@0.10.1 + +## 1.47.1 + +### Fixes + +- [#10533](https://github.com/gradio-app/gradio/pull/10533) [`59c892d`](https://github.com/gradio-app/gradio/commit/59c892d5620951ca404bd72cae6765cf25f15bc5) - support URL params for themes in SSR mode. Thanks @aliabid94! + +### Dependency updates + +- @gradio/theme@0.4.0 +- @gradio/client@1.12.0 +- @gradio/core@0.10.0 +- @gradio/wasm@0.17.3 +- @self/build@0.2.1 + +## 1.47.0 + +### Features + +- [#10433](https://github.com/gradio-app/gradio/pull/10433) [`2e8dc74`](https://github.com/gradio-app/gradio/commit/2e8dc74f751be02f7217f78d241806b42fcdca04) - Allow building multipage Gradio apps. Thanks @aliabid94! + +### Dependency updates + +- @gradio/client@1.11.0 +- @gradio/core@0.9.0 + +## 1.46.3 + +### Dependency updates + +- @gradio/wasm@0.17.2 +- @gradio/core@0.8.0 + +## 1.46.2 + +### Dependency updates + +- @gradio/wasm@0.17.1 +- @gradio/core@0.7.0 + +## 1.46.1 + +### Dependency updates + +- @gradio/wasm@0.17.0 +- @gradio/core@0.7.0 + +## 1.46.0 + +### Features + +- [#10270](https://github.com/gradio-app/gradio/pull/10270) [`bb11a2a`](https://github.com/gradio-app/gradio/commit/bb11a2a702ca04fde245e7d54d155cbcbde7791e) - [ZeroGPU] Handshake-based postMessage. Thanks @cbensimon! + +### Dependency updates + +- @gradio/client@1.10.0 +- @gradio/core@0.6.1 + +## 1.45.0 + +### Features + +- [#10187](https://github.com/gradio-app/gradio/pull/10187) [`64d1864`](https://github.com/gradio-app/gradio/commit/64d1864f8fb6f2d0b274fcf7bd47bdb1f6a77980) - `manifest json` for PWA. Thanks @whitphx! + +### Dependency updates + +- @gradio/client@1.9.0 +- @gradio/wasm@0.16.0 +- @gradio/core@0.6.0 + +## 1.44.2 + +### Dependency updates + +- @gradio/core@0.5.0 + +## 1.44.1 + +### Dependency updates + +- @gradio/core@0.4.1 + +## 1.44.0 + +### Features + +- [#9950](https://github.com/gradio-app/gradio/pull/9950) [`fc06fe4`](https://github.com/gradio-app/gradio/commit/fc06fe41f015678a0545f4e5c99f6ae2704f0031) - Add ability to read and write from LocalStorage. Thanks @abidlabs! + +### Dependency updates + +- @gradio/client@1.8.0 +- @gradio/core@0.4.0 + +## 1.43.1 + +### Dependency updates + +- @gradio/wasm@0.15.0 +- @gradio/core@0.3.0 + +## 1.43.0 + +### Features + +- [#9794](https://github.com/gradio-app/gradio/pull/9794) [`70bda3b`](https://github.com/gradio-app/gradio/commit/70bda3bb187a7219f4707344cf47edd0a021da19) - fix storybook build. Thanks @pngwn! + +### Dependency updates + +- @gradio/client@1.7.1 +- @gradio/core@0.2.1 + +## 1.42.1 + +### Fixes + +- [#9653](https://github.com/gradio-app/gradio/pull/9653) [`61cd768`](https://github.com/gradio-app/gradio/commit/61cd768490a12f5d63101d5434092bcd1cfc43a8) - Ensures tabs with visible set to false are not visible. Thanks @hannahblair! + +### Dependency updates + +- @self/build@0.2.0 +- @gradio/core@0.2.1 + +## 1.42.0 + +### Features + +- [#9681](https://github.com/gradio-app/gradio/pull/9681) [`2ed2361`](https://github.com/gradio-app/gradio/commit/2ed236187a9aab18e17fc4a8079eddef7dd195a5) - Allow setting title in gr.Info/Warning/Error. Thanks @ABucket! + +### Dependency updates + +- @gradio/client@1.7.0 +- @gradio/wasm@0.14.2 +- @self/build@0.1.1 +- @gradio/core@0.2.0 + +## 1.41.2 + +### Fixes + +- [#9669](https://github.com/gradio-app/gradio/pull/9669) [`70998ea`](https://github.com/gradio-app/gradio/commit/70998ea75276e677595447ac36b67e1152e589a2) - fix height in iframe. Thanks @pngwn! + +### Dependency updates + +- @gradio/wasm@0.14.1 +- @gradio/core@0.1.1 + +## 1.41.1 + +### Features + +- [#9617](https://github.com/gradio-app/gradio/pull/9617) [`c163182`](https://github.com/gradio-app/gradio/commit/c163182d1b752ef91629f9caa13bf3cce0fb0869) - Fix dark mode detection and container height. Thanks @pngwn! +- [#9616](https://github.com/gradio-app/gradio/pull/9616) [`ca7cd79`](https://github.com/gradio-app/gradio/commit/ca7cd79e7bdbc50570372c19391083a0f6501576) - fix devmode. Thanks @pngwn! + +### Fixes + +- [#9630](https://github.com/gradio-app/gradio/pull/9630) [`2eaa066`](https://github.com/gradio-app/gradio/commit/2eaa0667e1d1a0edd1089bf8c3ffa3f563b9bca2) - Fix duplicate attribute error. Thanks @pngwn! + +### Dependency updates + +- @gradio/core@0.1.1 + +## 1.41.0 + +### Features + +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - SSR e2e + fixes +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Fix custom component CLI on main/5.0 +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - fix css +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Fix reload mode +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - fix SSR apps on spaces +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Fix favicon in ssr mode +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Ssr part 2 +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Ensure media queries work for SSR mode + +### Dependencies + +- @gradio/client@1.6.0 +- @gradio/core@0.1.0 +- @gradio/theme@0.3.0 +- @gradio/wasm@0.14.0 +- @self/build@0.1.0 + +## 1.41.0-beta.7 + +### Features + +- [#9590](https://github.com/gradio-app/gradio/pull/9590) [`e853c41`](https://github.com/gradio-app/gradio/commit/e853c413583d91186aef3aceb0849d0ec0494834) - SSR e2e + fixes. Thanks @pngwn! +- [#9482](https://github.com/gradio-app/gradio/pull/9482) [`bd6c5f2`](https://github.com/gradio-app/gradio/commit/bd6c5f237b0631d86273c7684c3bf2b1011992a3) - Fix custom component CLI on main/5.0. Thanks @freddyaboulton! +- [#9576](https://github.com/gradio-app/gradio/pull/9576) [`430a26a`](https://github.com/gradio-app/gradio/commit/430a26a4fbcbabb5e9ddb6173bf658a00960e88e) - Fix reload mode. Thanks @freddyaboulton! +- [#9592](https://github.com/gradio-app/gradio/pull/9592) [`24fe222`](https://github.com/gradio-app/gradio/commit/24fe222fd17583d04dd31aebf60b649224e8382f) - Fix favicon in ssr mode. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/core@0.1.0-beta.6 +- @self/build@0.1.0-beta.3 + +## 1.41.0-beta.6 + +### Dependency updates + +- @gradio/client@1.6.0-beta.4 +- @gradio/core@0.1.0-beta.5 + +## 1.41.0-beta.5 + +### Features + +- [#9428](https://github.com/gradio-app/gradio/pull/9428) [`a17c7b6`](https://github.com/gradio-app/gradio/commit/a17c7b6e01bcee88b57a5231d0ff876f79adb8b1) - Ensure media queries work for SSR mode. Thanks @pngwn! + +### Dependency updates + +- @gradio/core@0.1.0-beta.4 + +## 1.41.0-beta.4 + +### Dependency updates + +- @gradio/wasm@0.14.0-beta.3 +- @gradio/core@0.1.0-beta.4 + +## 1.41.0-beta.3 + +### Features + +- [#9427](https://github.com/gradio-app/gradio/pull/9427) [`b672deb`](https://github.com/gradio-app/gradio/commit/b672deb240e1934f3cde6d257dbe93e3fa8b1857) - fix css. Thanks @pngwn! + +### Dependency updates + +- @gradio/core@0.1.0-beta.3 + +## 1.41.0-beta.2 + +### Features + +- [#9412](https://github.com/gradio-app/gradio/pull/9412) [`c2c2fd9`](https://github.com/gradio-app/gradio/commit/c2c2fd989348f826566773c07c0e0bda200199ff) - fix SSR apps on spaces. Thanks @pngwn! + +### Dependency updates + +- @gradio/client@1.6.0-beta.3 +- @gradio/core@0.1.0-beta.3 + +## 1.41.0-beta.1 + +### Features + +- [#9339](https://github.com/gradio-app/gradio/pull/9339) [`4c8c6f2`](https://github.com/gradio-app/gradio/commit/4c8c6f2fe603081941c5fdc43f48a0632b9f31ad) - Ssr part 2. Thanks @pngwn! + +### Dependency updates + +- @gradio/wasm@0.14.0-beta.2 +- @gradio/client@1.6.0-beta.2 +- @gradio/core@0.1.0-beta.2 +- @self/build@0.1.0-beta.2 + +## 1.40.1-beta.0 + +### Fixes + +- [#9163](https://github.com/gradio-app/gradio/pull/9163) [`2b6cbf2`](https://github.com/gradio-app/gradio/commit/2b6cbf25908e42cf027324e54ef2cc0baad11a91) - fix exports and generate types. Thanks @pngwn! + +## 1.40.0 + +### Features + +- [#8935](https://github.com/gradio-app/gradio/pull/8935) [`f6b2b97`](https://github.com/gradio-app/gradio/commit/f6b2b97d473dd81571410cccc6e1ddfcf9fa00bc) - Initialize the client with the fake host for Lite server. Thanks @whitphx! +- [#9102](https://github.com/gradio-app/gradio/pull/9102) [`efdc323`](https://github.com/gradio-app/gradio/commit/efdc3231a7bde38cfe45d10086d0d36a24c1b9b4) - Initial SSR refactor. Thanks @pngwn! +- [#9110](https://github.com/gradio-app/gradio/pull/9110) [`e1e7ad3`](https://github.com/gradio-app/gradio/commit/e1e7ad3add2679cafab5c05856aba6e28c0f2b76) - fix version + pkg name. Thanks @pngwn! + +## 1.39.2 + +### Dependency updates + +- @gradio/code@0.8.2 +- @gradio/paramviewer@0.4.20 +- @gradio/atoms@0.7.9 +- @gradio/statustracker@0.7.4 +- @gradio/column@0.1.2 +- @gradio/client@1.5.0 +- @gradio/gallery@0.12.0 +- @gradio/icons@0.7.0 +- @gradio/upload@0.12.2 +- @gradio/markdown@0.9.2 +- @gradio/plot@0.6.3 +- @gradio/image@0.14.0 +- @gradio/file@0.9.2 +- @gradio/chatbot@0.12.4 +- @gradio/annotatedimage@0.7.0 +- @gradio/imageeditor@0.9.0 +- @gradio/nativeplot@0.3.0 +- @gradio/model3d@0.12.0 +- @gradio/audio@0.13.2 +- @gradio/button@0.2.49 +- @gradio/dataframe@0.9.2 +- @gradio/dataset@0.2.3 +- @gradio/downloadbutton@0.1.26 +- @gradio/fileexplorer@0.4.17 +- @gradio/multimodaltextbox@0.5.5 +- @gradio/simpleimage@0.6.5 +- @gradio/uploadbutton@0.6.17 +- @gradio/video@0.10.2 +- @gradio/datetime@0.1.2 +- @gradio/dropdown@0.7.11 +- @gradio/form@0.1.23 +- @gradio/highlightedtext@0.7.5 +- @gradio/html@0.3.4 +- @gradio/json@0.3.1 +- @gradio/label@0.3.11 +- @gradio/simpledropdown@0.2.11 +- @gradio/simpletextbox@0.2.11 +- @gradio/textbox@0.6.10 +- @gradio/accordion@0.3.21 +- @gradio/box@0.1.23 +- @gradio/checkbox@0.3.11 +- @gradio/checkboxgroup@0.5.11 +- @gradio/colorpicker@0.3.11 +- @gradio/fallback@0.3.11 +- @gradio/number@0.4.11 +- @gradio/radio@0.5.11 +- @gradio/slider@0.4.11 +- @gradio/row@0.1.3 + +## 1.39.1 + +### Dependency updates + +- @gradio/atoms@0.7.8 +- @gradio/icons@0.6.1 +- @gradio/utils@0.5.2 +- @gradio/statustracker@0.7.3 +- @gradio/upload@0.12.1 +- @gradio/image@0.13.1 +- @gradio/video@0.10.1 +- @gradio/code@0.8.1 +- @gradio/paramviewer@0.4.19 +- @gradio/json@0.3.0 +- @gradio/multimodaltextbox@0.5.4 +- @gradio/nativeplot@0.2.0 +- @gradio/annotatedimage@0.6.15 +- @gradio/audio@0.13.1 +- @gradio/chatbot@0.12.3 +- @gradio/datetime@0.1.1 +- @gradio/dropdown@0.7.10 +- @gradio/file@0.9.1 +- @gradio/fileexplorer@0.4.16 +- @gradio/form@0.1.22 +- @gradio/gallery@0.11.4 +- @gradio/highlightedtext@0.7.4 +- @gradio/imageeditor@0.8.1 +- @gradio/label@0.3.10 +- @gradio/markdown@0.9.1 +- @gradio/model3d@0.11.2 +- @gradio/plot@0.6.2 +- @gradio/simpledropdown@0.2.10 +- @gradio/simpleimage@0.6.4 +- @gradio/simpletextbox@0.2.10 +- @gradio/textbox@0.6.9 +- @gradio/button@0.2.48 +- @gradio/dataframe@0.9.1 +- @gradio/dataset@0.2.2 +- @gradio/uploadbutton@0.6.16 +- @gradio/accordion@0.3.20 +- @gradio/checkbox@0.3.10 +- @gradio/checkboxgroup@0.5.10 +- @gradio/colorpicker@0.3.10 +- @gradio/column@0.1.2 +- @gradio/downloadbutton@0.1.25 +- @gradio/fallback@0.3.10 +- @gradio/html@0.3.3 +- @gradio/number@0.4.10 +- @gradio/radio@0.5.10 +- @gradio/row@0.1.3 +- @gradio/slider@0.4.10 +- @gradio/tabitem@0.2.13 +- @gradio/tabs@0.2.12 +- @gradio/timer@0.3.1 +- @gradio/group@0.1.1 +- @gradio/box@0.1.22 + +## 1.39.0 + +### Features + +- [#8832](https://github.com/gradio-app/gradio/pull/8832) [`e75f2ca`](https://github.com/gradio-app/gradio/commit/e75f2ca2da4f41f25459b98bedaa940c887e6a93) - Fix build for pre-release. Thanks @pngwn! +- [#8846](https://github.com/gradio-app/gradio/pull/8846) [`76c1759`](https://github.com/gradio-app/gradio/commit/76c175935019833baef709a5cf401d2263ca72ee) - add space header. Thanks @pngwn! +- [#8804](https://github.com/gradio-app/gradio/pull/8804) [`1d09925`](https://github.com/gradio-app/gradio/commit/1d09925469a5f96e8d3a972a28841903fa1c7265) - Fix Lite's . Thanks @whitphx! +- [#8807](https://github.com/gradio-app/gradio/pull/8807) [`a238af4`](https://github.com/gradio-app/gradio/commit/a238af4d688c4e030e37c2ef01d5c80d6d940912) - Refactor plots to drop `altair` and use `vega.js` directly. Thanks @aliabid94! + +### Fixes + +- [#8818](https://github.com/gradio-app/gradio/pull/8818) [`2de9a97`](https://github.com/gradio-app/gradio/commit/2de9a97ae953cc5c58c0d33d8966e638e76f950f) - Refactoring component making the code simpler and fixing a Playground mode bug. Thanks @whitphx! + +### Dependency updates + +- @gradio/code@0.8.0 +- @gradio/paramviewer@0.4.18 +- @gradio/audio@0.13.0 +- @gradio/video@0.10.0 +- @gradio/wasm@0.12.0 +- @gradio/markdown@0.9.0 +- @gradio/chatbot@0.12.2 +- @gradio/client@1.4.0 +- @gradio/image@0.13.0 +- @gradio/dataframe@0.9.0 +- @gradio/statustracker@0.7.2 +- @gradio/imageeditor@0.8.0 +- @gradio/file@0.9.0 +- @gradio/upload@0.12.0 +- @gradio/datetime@0.1.0 +- @gradio/nativeplot@0.1.0 +- @gradio/atoms@0.7.7 +- @gradio/simpleimage@0.6.3 +- @gradio/annotatedimage@0.6.14 +- @gradio/model3d@0.11.1 +- @gradio/button@0.2.47 +- @gradio/dataset@0.2.1 +- @gradio/downloadbutton@0.1.24 +- @gradio/fileexplorer@0.4.15 +- @gradio/gallery@0.11.3 +- @gradio/multimodaltextbox@0.5.3 +- @gradio/uploadbutton@0.6.15 +- @gradio/accordion@0.3.19 +- @gradio/checkbox@0.3.9 +- @gradio/checkboxgroup@0.5.9 +- @gradio/colorpicker@0.3.9 +- @gradio/column@0.1.2 +- @gradio/dropdown@0.7.9 +- @gradio/fallback@0.3.9 +- @gradio/highlightedtext@0.7.3 +- @gradio/html@0.3.2 +- @gradio/json@0.2.9 +- @gradio/label@0.3.9 +- @gradio/number@0.4.9 +- @gradio/plot@0.6.1 +- @gradio/radio@0.5.9 +- @gradio/row@0.1.3 +- @gradio/simpledropdown@0.2.9 +- @gradio/simpletextbox@0.2.9 +- @gradio/slider@0.4.9 +- @gradio/textbox@0.6.8 +- @gradio/box@0.1.21 +- @gradio/form@0.1.21 + +## 1.38.1 + +### Dependency updates + +- @gradio/chatbot@0.12.1 + +## 1.38.0 + +### Features + +- [#8713](https://github.com/gradio-app/gradio/pull/8713) [`e3c7079`](https://github.com/gradio-app/gradio/commit/e3c7079e380880d5759d98d180eaf688122f1c69) - Time range component. Thanks @aliabid94! + +### Fixes + +- [#8505](https://github.com/gradio-app/gradio/pull/8505) [`2943d6d`](https://github.com/gradio-app/gradio/commit/2943d6d68847314885dc6c5c0247083116017ca0) - Add Timer component. Thanks @aliabid94! +- [#8720](https://github.com/gradio-app/gradio/pull/8720) [`936c713`](https://github.com/gradio-app/gradio/commit/936c7137a99ef59efdf75bae5dd27eea2ac1f577) - Documents auth in the guides, in the view API page, and also types the Blocks.config object. Thanks @abidlabs! + +### Dependency updates + +- @gradio/atoms@0.7.6 +- @gradio/utils@0.5.1 +- @gradio/statustracker@0.7.1 +- @gradio/client@1.3.0 +- @gradio/markdown@0.8.1 +- @gradio/upload@0.11.5 +- @gradio/button@0.2.46 +- @gradio/paramviewer@0.4.17 +- @gradio/dataframe@0.8.13 +- @gradio/timer@0.3.0 +- @gradio/code@0.7.0 +- @gradio/chatbot@0.12.0 +- @gradio/image@0.12.2 +- @gradio/icons@0.6.0 +- @gradio/plot@0.6.0 +- @gradio/dataset@0.2.0 +- @gradio/model3d@0.11.0 +- @gradio/annotatedimage@0.6.13 +- @gradio/audio@0.12.2 +- @gradio/downloadbutton@0.1.23 +- @gradio/file@0.8.5 +- @gradio/fileexplorer@0.4.14 +- @gradio/gallery@0.11.2 +- @gradio/imageeditor@0.7.13 +- @gradio/multimodaltextbox@0.5.2 +- @gradio/simpleimage@0.6.2 +- @gradio/uploadbutton@0.6.14 +- @gradio/video@0.9.2 +- @gradio/accordion@0.3.18 +- @gradio/checkbox@0.3.8 +- @gradio/checkboxgroup@0.5.8 +- @gradio/colorpicker@0.3.8 +- @gradio/column@0.1.2 +- @gradio/datetime@0.0.2 +- @gradio/dropdown@0.7.8 +- @gradio/fallback@0.3.8 +- @gradio/form@0.1.20 +- @gradio/highlightedtext@0.7.2 +- @gradio/html@0.3.1 +- @gradio/json@0.2.8 +- @gradio/label@0.3.8 +- @gradio/number@0.4.8 +- @gradio/radio@0.5.8 +- @gradio/row@0.1.3 +- @gradio/simpledropdown@0.2.8 +- @gradio/simpletextbox@0.2.8 +- @gradio/slider@0.4.8 +- @gradio/tabitem@0.2.12 +- @gradio/tabs@0.2.11 +- @gradio/textbox@0.6.7 +- @gradio/group@0.1.1 +- @gradio/box@0.1.20 + +## 1.37.1 + +### Dependency updates + +- @gradio/code@0.6.13 +- @gradio/upload@0.11.4 +- @gradio/client@1.2.1 +- @gradio/gallery@0.11.1 +- @gradio/image@0.12.1 +- @gradio/chatbot@0.11.1 +- @gradio/file@0.8.4 +- @gradio/multimodaltextbox@0.5.1 +- @gradio/fileexplorer@0.4.13 +- @gradio/annotatedimage@0.6.12 +- @gradio/audio@0.12.1 +- @gradio/button@0.2.45 +- @gradio/dataframe@0.8.12 +- @gradio/dataset@0.1.45 +- @gradio/imageeditor@0.7.12 +- @gradio/model3d@0.10.12 +- @gradio/simpleimage@0.6.1 +- @gradio/uploadbutton@0.6.13 +- @gradio/video@0.9.1 +- @gradio/downloadbutton@0.1.22 + +## 1.37.0 + +### Features + +- [#8131](https://github.com/gradio-app/gradio/pull/8131) [`bb504b4`](https://github.com/gradio-app/gradio/commit/bb504b494947a287d6386e0e7ead3860c0f15223) - Gradio components in `gr.Chatbot()`. Thanks @dawoodkhan82! +- [#8489](https://github.com/gradio-app/gradio/pull/8489) [`c2a0d05`](https://github.com/gradio-app/gradio/commit/c2a0d056d679d90631d9ccd944dadd67e7e03b7f) - Control Display of Error, Info, Warning. Thanks @freddyaboulton! +- [#8571](https://github.com/gradio-app/gradio/pull/8571) [`a77877f`](https://github.com/gradio-app/gradio/commit/a77877f62df7c610fcfac7b3b00e186a087c8ec6) - First time loading performance optimization. Thanks @baojianting! + +### Fixes + +- [#8599](https://github.com/gradio-app/gradio/pull/8599) [`ca125b7`](https://github.com/gradio-app/gradio/commit/ca125b728a0803a5b5ed71a5189e6b68175edcad) - Fix reload mode for jupyter notebook and stateful demos. Thanks @freddyaboulton! +- [#8521](https://github.com/gradio-app/gradio/pull/8521) [`900cf25`](https://github.com/gradio-app/gradio/commit/900cf25256a5b0563860097d69aac28b6afbfd8b) - Ensure frontend functions work when they don't return a value. Thanks @pngwn! + +### Dependency updates + +- @gradio/code@0.6.12 +- @gradio/atoms@0.7.5 +- @gradio/audio@0.12.0 +- @gradio/chatbot@0.11.0 +- @gradio/gallery@0.11.0 +- @gradio/image@0.12.0 +- @gradio/multimodaltextbox@0.5.0 +- @gradio/plot@0.5.0 +- @gradio/simpleimage@0.6.0 +- @gradio/utils@0.5.0 +- @gradio/video@0.9.0 +- @gradio/icons@0.5.0 +- @gradio/wasm@0.11.0 +- @gradio/client@1.2.0 +- @gradio/statustracker@0.7.0 +- @gradio/html@0.3.0 +- @gradio/dataset@0.1.44 +- @gradio/markdown@0.8.0 +- @gradio/imageeditor@0.7.11 +- @gradio/accordion@0.3.17 +- @gradio/annotatedimage@0.6.11 +- @gradio/button@0.2.44 +- @gradio/checkbox@0.3.7 +- @gradio/checkboxgroup@0.5.7 +- @gradio/colorpicker@0.3.7 +- @gradio/column@0.1.2 +- @gradio/dataframe@0.8.11 +- @gradio/downloadbutton@0.1.21 +- @gradio/dropdown@0.7.7 +- @gradio/fallback@0.3.7 +- @gradio/file@0.8.3 +- @gradio/fileexplorer@0.4.12 +- @gradio/form@0.1.19 +- @gradio/highlightedtext@0.7.1 +- @gradio/json@0.2.7 +- @gradio/label@0.3.7 +- @gradio/model3d@0.10.11 +- @gradio/number@0.4.7 +- @gradio/paramviewer@0.4.16 +- @gradio/radio@0.5.7 +- @gradio/row@0.1.3 +- @gradio/simpledropdown@0.2.7 +- @gradio/simpletextbox@0.2.7 +- @gradio/slider@0.4.7 +- @gradio/tabitem@0.2.11 +- @gradio/tabs@0.2.10 +- @gradio/textbox@0.6.6 +- @gradio/upload@0.11.3 +- @gradio/uploadbutton@0.6.12 +- @gradio/group@0.1.1 +- @gradio/box@0.1.19 + +## 1.36.2 + +### Features + +- [#8499](https://github.com/gradio-app/gradio/pull/8499) [`c5f6e77`](https://github.com/gradio-app/gradio/commit/c5f6e7722a197d4706419ade14276ddecf3196f8) - Cache break themes on change. Thanks @aliabid94! + +### Dependency updates + +- @gradio/code@0.6.11 +- @gradio/client@1.1.1 +- @gradio/upload@0.11.2 +- @gradio/annotatedimage@0.6.10 +- @gradio/audio@0.11.10 +- @gradio/button@0.2.43 +- @gradio/chatbot@0.10.11 +- @gradio/dataframe@0.8.10 +- @gradio/dataset@0.1.43 +- @gradio/file@0.8.2 +- @gradio/fileexplorer@0.4.11 +- @gradio/gallery@0.10.10 +- @gradio/image@0.11.10 +- @gradio/imageeditor@0.7.10 +- @gradio/model3d@0.10.10 +- @gradio/multimodaltextbox@0.4.11 +- @gradio/simpleimage@0.5.10 +- @gradio/uploadbutton@0.6.11 +- @gradio/video@0.8.10 +- @gradio/downloadbutton@0.1.20 + +## 1.36.1 + +### Dependency updates + +- @gradio/upload@0.11.1 +- @gradio/code@0.6.10 +- @gradio/client@1.1.0 +- @gradio/annotatedimage@0.6.9 +- @gradio/audio@0.11.9 +- @gradio/button@0.2.42 +- @gradio/chatbot@0.10.10 +- @gradio/dataframe@0.8.9 +- @gradio/dataset@0.1.42 +- @gradio/downloadbutton@0.1.19 +- @gradio/file@0.8.1 +- @gradio/fileexplorer@0.4.10 +- @gradio/gallery@0.10.9 +- @gradio/image@0.11.9 +- @gradio/imageeditor@0.7.9 +- @gradio/model3d@0.10.9 +- @gradio/multimodaltextbox@0.4.10 +- @gradio/simpleimage@0.5.9 +- @gradio/uploadbutton@0.6.10 +- @gradio/video@0.8.9 + +## 1.36.0 + +### Features + +- [#8370](https://github.com/gradio-app/gradio/pull/8370) [`48eeea4`](https://github.com/gradio-app/gradio/commit/48eeea4eaab7e24168688e3c3fbafb30e4e78d51) - Refactor Cancelling Logic To Use /cancel. Thanks @freddyaboulton! +- [#8460](https://github.com/gradio-app/gradio/pull/8460) [`8628899`](https://github.com/gradio-app/gradio/commit/86288993d9589ceb7bcc3e4d10f0adb6419d4ac5) - Support Bash in Api Recorder. Thanks @aliabd! +- [#8444](https://github.com/gradio-app/gradio/pull/8444) [`2cd02ff`](https://github.com/gradio-app/gradio/commit/2cd02ff3b7c57cd69635d111ff25643eba30b9b0) - Remove deprecated parameters from Python Client. Thanks @abidlabs! +- [#8473](https://github.com/gradio-app/gradio/pull/8473) [`8ca93d4`](https://github.com/gradio-app/gradio/commit/8ca93d45dd9f8948cfe87fe16ef5943139e756a7) - Improve design of api recorder. Thanks @aliabd! +- [#8445](https://github.com/gradio-app/gradio/pull/8445) [`5c8915b`](https://github.com/gradio-app/gradio/commit/5c8915b11308756c3b7279864d240ea85f5a0b4a) - Add cURL to view API Page and add a dedicated Guide. Thanks @abidlabs! + +### Fixes + +- [#8451](https://github.com/gradio-app/gradio/pull/8451) [`9d2d605`](https://github.com/gradio-app/gradio/commit/9d2d6051caed5c8749a26a6fa7480a5ae6e6c4f3) - Change client submit API to be an AsyncIterable and support more platforms. Thanks @pngwn! +- [#8439](https://github.com/gradio-app/gradio/pull/8439) [`63d36fb`](https://github.com/gradio-app/gradio/commit/63d36fbbf4bf6dc909be9a0ffc7b6bf6621d83e8) - Handle gradio apps using `state` in the JS Client. Thanks @hannahblair! + +### Dependency updates + +- @gradio/code@0.6.9 +- @gradio/statustracker@0.6.0 +- @gradio/client@1.0.0 +- @gradio/file@0.8.0 +- @gradio/upload@0.11.0 +- @gradio/annotatedimage@0.6.8 +- @gradio/audio@0.11.8 +- @gradio/button@0.2.41 +- @gradio/chatbot@0.10.9 +- @gradio/dataframe@0.8.8 +- @gradio/dataset@0.1.41 +- @gradio/downloadbutton@0.1.18 +- @gradio/fileexplorer@0.4.9 +- @gradio/gallery@0.10.8 +- @gradio/image@0.11.8 +- @gradio/imageeditor@0.7.8 +- @gradio/model3d@0.10.8 +- @gradio/multimodaltextbox@0.4.9 +- @gradio/simpleimage@0.5.8 +- @gradio/uploadbutton@0.6.9 +- @gradio/video@0.8.8 +- @gradio/accordion@0.3.16 +- @gradio/checkbox@0.3.6 +- @gradio/checkboxgroup@0.5.6 +- @gradio/colorpicker@0.3.6 +- @gradio/column@0.1.2 +- @gradio/dropdown@0.7.6 +- @gradio/fallback@0.3.6 +- @gradio/form@0.1.18 +- @gradio/group@0.1.1 +- @gradio/highlightedtext@0.7.0 +- @gradio/html@0.2.6 +- @gradio/json@0.2.6 +- @gradio/label@0.3.6 +- @gradio/markdown@0.7.6 +- @gradio/number@0.4.6 +- @gradio/paramviewer@0.4.15 +- @gradio/plot@0.4.6 +- @gradio/radio@0.5.6 +- @gradio/row@0.1.3 +- @gradio/simpledropdown@0.2.6 +- @gradio/simpletextbox@0.2.6 +- @gradio/slider@0.4.6 +- @gradio/tabitem@0.2.10 +- @gradio/tabs@0.2.9 +- @gradio/textbox@0.6.5 + +## 1.35.9 + +### Dependency updates + +- @gradio/chatbot@0.10.8 +- @gradio/multimodaltextbox@0.4.8 + +## 1.35.8 + +### Fixes + +- [#8431](https://github.com/gradio-app/gradio/pull/8431) [`9909b28`](https://github.com/gradio-app/gradio/commit/9909b28364b82b5e8bfd7d47a858a204f68b963a) - fix scrolling on spaces. Thanks @pngwn! + +## 1.35.7 + +### Dependency updates + +- @gradio/upload@0.10.7 +- @gradio/code@0.6.8 +- @gradio/client@0.20.1 +- @gradio/annotatedimage@0.6.7 +- @gradio/audio@0.11.7 +- @gradio/button@0.2.40 +- @gradio/chatbot@0.10.7 +- @gradio/dataframe@0.8.7 +- @gradio/dataset@0.1.40 +- @gradio/downloadbutton@0.1.17 +- @gradio/file@0.7.7 +- @gradio/fileexplorer@0.4.8 +- @gradio/gallery@0.10.7 +- @gradio/image@0.11.7 +- @gradio/imageeditor@0.7.7 +- @gradio/model3d@0.10.7 +- @gradio/multimodaltextbox@0.4.7 +- @gradio/simpleimage@0.5.7 +- @gradio/uploadbutton@0.6.8 +- @gradio/video@0.8.7 + +## 1.35.6 + +### Features + +- [#8243](https://github.com/gradio-app/gradio/pull/8243) [`55f664f`](https://github.com/gradio-app/gradio/commit/55f664f2979a49acc29a73cde16c6ebdfcc91db2) - Add event listener support to render blocks. Thanks @aliabid94! +- [#8398](https://github.com/gradio-app/gradio/pull/8398) [`945ac83`](https://github.com/gradio-app/gradio/commit/945ac837e779b120790814ea6f6f81bd2712f5f8) - Improve rendering. Thanks @aliabid94! + +### Dependency updates + +- @gradio/code@0.6.7 +- @gradio/client@0.20.0 +- @gradio/model3d@0.10.6 +- @gradio/column@0.1.2 +- @gradio/row@0.1.3 +- @gradio/statustracker@0.6.0 +- @gradio/dataset@0.1.39 +- @gradio/accordion@0.3.16 +- @gradio/highlightedtext@0.7.0 +- @gradio/annotatedimage@0.6.6 +- @gradio/audio@0.11.6 +- @gradio/button@0.2.39 +- @gradio/chatbot@0.10.6 +- @gradio/dataframe@0.8.6 +- @gradio/downloadbutton@0.1.16 +- @gradio/file@0.7.6 +- @gradio/fileexplorer@0.4.7 +- @gradio/gallery@0.10.6 +- @gradio/image@0.11.6 +- @gradio/imageeditor@0.7.6 +- @gradio/multimodaltextbox@0.4.6 +- @gradio/simpleimage@0.5.6 +- @gradio/upload@0.10.6 +- @gradio/uploadbutton@0.6.7 +- @gradio/video@0.8.6 +- @gradio/tabitem@0.2.10 +- @gradio/checkbox@0.3.6 +- @gradio/checkboxgroup@0.5.6 +- @gradio/colorpicker@0.3.6 +- @gradio/dropdown@0.7.6 +- @gradio/fallback@0.3.6 +- @gradio/html@0.2.6 +- @gradio/json@0.2.6 +- @gradio/label@0.3.6 +- @gradio/markdown@0.7.6 +- @gradio/number@0.4.6 +- @gradio/paramviewer@0.4.15 +- @gradio/plot@0.4.6 +- @gradio/radio@0.5.6 +- @gradio/simpledropdown@0.2.6 +- @gradio/simpletextbox@0.2.6 +- @gradio/slider@0.4.6 +- @gradio/textbox@0.6.5 + +## 1.35.5 + +### Dependency updates + +- @gradio/utils@0.4.2 +- @gradio/atoms@0.7.4 +- @gradio/statustracker@0.5.5 +- @gradio/upload@0.10.5 +- @gradio/tabs@0.2.9 +- @gradio/code@0.6.6 +- @gradio/markdown@0.7.5 +- @gradio/theme@0.2.3 +- @gradio/client@0.19.4 +- @gradio/audio@0.11.5 +- @gradio/image@0.11.5 +- @gradio/video@0.8.5 +- @gradio/chatbot@0.10.5 +- @gradio/file@0.7.5 +- @gradio/dataframe@0.8.5 +- @gradio/highlightedtext@0.6.4 +- @gradio/plot@0.4.5 +- @gradio/fileexplorer@0.4.6 +- @gradio/gallery@0.10.5 +- @gradio/annotatedimage@0.6.5 +- @gradio/button@0.2.38 +- @gradio/dataset@0.1.38 +- @gradio/downloadbutton@0.1.15 +- @gradio/imageeditor@0.7.5 +- @gradio/model3d@0.10.5 +- @gradio/multimodaltextbox@0.4.5 +- @gradio/simpleimage@0.5.5 +- @gradio/uploadbutton@0.6.6 +- @gradio/accordion@0.3.15 +- @gradio/checkbox@0.3.5 +- @gradio/checkboxgroup@0.5.5 +- @gradio/colorpicker@0.3.5 +- @gradio/dropdown@0.7.5 +- @gradio/fallback@0.3.5 +- @gradio/form@0.1.18 +- @gradio/html@0.2.5 +- @gradio/json@0.2.5 +- @gradio/label@0.3.5 +- @gradio/number@0.4.5 +- @gradio/paramviewer@0.4.14 +- @gradio/radio@0.5.5 +- @gradio/simpledropdown@0.2.5 +- @gradio/simpletextbox@0.2.5 +- @gradio/slider@0.4.5 +- @gradio/tabitem@0.2.9 +- @gradio/textbox@0.6.4 +- @gradio/box@0.1.18 + +## 1.35.4 + +### Fixes + +- [#8247](https://github.com/gradio-app/gradio/pull/8247) [`8f46556`](https://github.com/gradio-app/gradio/commit/8f46556b38e35cffbadac74ff80445dceea3bcf5) - Fix api recorder. Thanks @abidlabs! + +## 1.35.3 + +### Features + +- [#8279](https://github.com/gradio-app/gradio/pull/8279) [`4350215`](https://github.com/gradio-app/gradio/commit/4350215348981aba6dea473884b047f096dcdf0f) - Link to troubleshooting guide in the custom component loading status. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/code@0.6.5 +- @gradio/client@0.19.3 +- @gradio/statustracker@0.5.4 +- @gradio/annotatedimage@0.6.4 +- @gradio/audio@0.11.4 +- @gradio/button@0.2.37 +- @gradio/chatbot@0.10.4 +- @gradio/dataframe@0.8.4 +- @gradio/dataset@0.1.37 +- @gradio/downloadbutton@0.1.14 +- @gradio/file@0.7.4 +- @gradio/fileexplorer@0.4.5 +- @gradio/gallery@0.10.4 +- @gradio/image@0.11.4 +- @gradio/imageeditor@0.7.4 +- @gradio/model3d@0.10.4 +- @gradio/multimodaltextbox@0.4.4 +- @gradio/simpleimage@0.5.4 +- @gradio/upload@0.10.4 +- @gradio/uploadbutton@0.6.5 +- @gradio/video@0.8.4 +- @gradio/accordion@0.3.14 +- @gradio/checkbox@0.3.4 +- @gradio/checkboxgroup@0.5.4 +- @gradio/colorpicker@0.3.4 +- @gradio/dropdown@0.7.4 +- @gradio/fallback@0.3.4 +- @gradio/highlightedtext@0.6.3 +- @gradio/html@0.2.4 +- @gradio/json@0.2.4 +- @gradio/label@0.3.4 +- @gradio/markdown@0.7.4 +- @gradio/number@0.4.4 +- @gradio/paramviewer@0.4.13 +- @gradio/plot@0.4.4 +- @gradio/radio@0.5.4 +- @gradio/simpledropdown@0.2.4 +- @gradio/simpletextbox@0.2.4 +- @gradio/slider@0.4.4 +- @gradio/textbox@0.6.3 + +## 1.35.2 + +### Dependency updates + +- @gradio/upload@0.10.3 +- @gradio/code@0.6.4 +- @gradio/client@0.19.2 +- @gradio/annotatedimage@0.6.3 +- @gradio/audio@0.11.3 +- @gradio/button@0.2.36 +- @gradio/chatbot@0.10.3 +- @gradio/dataframe@0.8.3 +- @gradio/dataset@0.1.36 +- @gradio/downloadbutton@0.1.13 +- @gradio/file@0.7.3 +- @gradio/fileexplorer@0.4.4 +- @gradio/gallery@0.10.3 +- @gradio/image@0.11.3 +- @gradio/imageeditor@0.7.3 +- @gradio/model3d@0.10.3 +- @gradio/multimodaltextbox@0.4.3 +- @gradio/simpleimage@0.5.3 +- @gradio/uploadbutton@0.6.4 +- @gradio/video@0.8.3 + +## 1.35.1 + +### Features + +- [#8263](https://github.com/gradio-app/gradio/pull/8263) [`de52f0e`](https://github.com/gradio-app/gradio/commit/de52f0e5af75688713e4e96a195208576a4d64ad) - Reduce the analytics that are collected in Gradio. Thanks @abidlabs! + +### Fixes + +- [#8272](https://github.com/gradio-app/gradio/pull/8272) [`fbf4edd`](https://github.com/gradio-app/gradio/commit/fbf4edde7c896cdf4c903463e44c31ed96111b3c) - ensure client works for private spaces. Thanks @pngwn! + +### Dependency updates + +- @gradio/code@0.6.3 +- @gradio/statustracker@0.5.3 +- @gradio/client@0.19.1 +- @gradio/accordion@0.3.13 +- @gradio/annotatedimage@0.6.2 +- @gradio/audio@0.11.2 +- @gradio/chatbot@0.10.2 +- @gradio/checkbox@0.3.3 +- @gradio/checkboxgroup@0.5.3 +- @gradio/colorpicker@0.3.3 +- @gradio/dataframe@0.8.2 +- @gradio/dropdown@0.7.3 +- @gradio/fallback@0.3.3 +- @gradio/file@0.7.2 +- @gradio/fileexplorer@0.4.3 +- @gradio/gallery@0.10.2 +- @gradio/highlightedtext@0.6.2 +- @gradio/html@0.2.3 +- @gradio/image@0.11.2 +- @gradio/imageeditor@0.7.2 +- @gradio/json@0.2.3 +- @gradio/label@0.3.3 +- @gradio/markdown@0.7.3 +- @gradio/model3d@0.10.2 +- @gradio/multimodaltextbox@0.4.2 +- @gradio/number@0.4.3 +- @gradio/paramviewer@0.4.12 +- @gradio/plot@0.4.3 +- @gradio/radio@0.5.3 +- @gradio/simpledropdown@0.2.3 +- @gradio/simpleimage@0.5.2 +- @gradio/simpletextbox@0.2.3 +- @gradio/slider@0.4.3 +- @gradio/textbox@0.6.2 +- @gradio/video@0.8.2 +- @gradio/button@0.2.35 +- @gradio/dataset@0.1.35 +- @gradio/downloadbutton@0.1.12 +- @gradio/upload@0.10.2 +- @gradio/uploadbutton@0.6.3 + +## 1.35.0 + +### Features + +- [#8219](https://github.com/gradio-app/gradio/pull/8219) [`32d915a`](https://github.com/gradio-app/gradio/commit/32d915aad5c6d9b2f5fdcafef39e246fc1b2d852) - Apply clean_indent() to the file contents specified with tags. Thanks @whitphx! +- [#8110](https://github.com/gradio-app/gradio/pull/8110) [`5436031`](https://github.com/gradio-app/gradio/commit/5436031f92c1596282eb64e1e74d555f279e9697) - Render decorator 2. Thanks @aliabid94! +- [#8197](https://github.com/gradio-app/gradio/pull/8197) [`e09b4e8`](https://github.com/gradio-app/gradio/commit/e09b4e8216b970bc1b142a0f08e7d190b954eb35) - Add support for passing keyword args to `data` in JS client. Thanks @hannahblair! + +### Fixes + +- [#8252](https://github.com/gradio-app/gradio/pull/8252) [`22df61a`](https://github.com/gradio-app/gradio/commit/22df61a26adf8023f6dd49c051979990e8d3879a) - Client node fix. Thanks @pngwn! + +### Dependency updates + +- @gradio/atoms@0.7.3 +- @gradio/statustracker@0.5.2 +- @gradio/code@0.6.2 +- @gradio/markdown@0.7.2 +- @gradio/client@0.19.0 +- @gradio/icons@0.4.1 +- @gradio/audio@0.11.1 +- @gradio/image@0.11.1 +- @gradio/upload@0.10.1 +- @gradio/video@0.8.1 +- @gradio/chatbot@0.10.1 +- @gradio/multimodaltextbox@0.4.1 +- @gradio/dataframe@0.8.1 +- @gradio/file@0.7.1 +- @gradio/gallery@0.10.1 +- @gradio/imageeditor@0.7.1 +- @gradio/model3d@0.10.1 +- @gradio/simpleimage@0.5.1 +- @gradio/annotatedimage@0.6.1 +- @gradio/button@0.2.34 +- @gradio/dataset@0.1.34 +- @gradio/downloadbutton@0.1.11 +- @gradio/fileexplorer@0.4.2 +- @gradio/uploadbutton@0.6.2 +- @gradio/dropdown@0.7.2 +- @gradio/form@0.1.17 +- @gradio/highlightedtext@0.6.1 +- @gradio/json@0.2.2 +- @gradio/label@0.3.2 +- @gradio/plot@0.4.2 +- @gradio/simpledropdown@0.2.2 +- @gradio/simpletextbox@0.2.2 +- @gradio/textbox@0.6.1 +- @gradio/accordion@0.3.12 +- @gradio/box@0.1.17 +- @gradio/checkbox@0.3.2 +- @gradio/checkboxgroup@0.5.2 +- @gradio/colorpicker@0.3.2 +- @gradio/fallback@0.3.2 +- @gradio/html@0.2.2 +- @gradio/number@0.4.2 +- @gradio/paramviewer@0.4.11 +- @gradio/radio@0.5.2 +- @gradio/slider@0.4.2 + +## 1.34.0 + +### Features + +- [#8121](https://github.com/gradio-app/gradio/pull/8121) [`f5b710c`](https://github.com/gradio-app/gradio/commit/f5b710c919b0ce604ea955f0d5f4faa91095ca4a) - chore(deps): update dependency eslint to v9. Thanks @renovate! +- [#8174](https://github.com/gradio-app/gradio/pull/8174) [`a81e369`](https://github.com/gradio-app/gradio/commit/a81e36967c0973012e90ec7cf03b99cf3fea88ec) - Remove hatch installation in js/app/package.json which is no longer needed. Thanks @whitphx! +- [#8209](https://github.com/gradio-app/gradio/pull/8209) [`b9afe93`](https://github.com/gradio-app/gradio/commit/b9afe93915401df5bd6737c89395c2477acfa585) - Rename `eventSource_Factory` and `fetch_implementation`. Thanks @hannahblair! +- [#8109](https://github.com/gradio-app/gradio/pull/8109) [`bed2f82`](https://github.com/gradio-app/gradio/commit/bed2f82e2297b50f7b59423a3de05af0b9910724) - Implement JS Client tests. Thanks @hannahblair! +- [#8106](https://github.com/gradio-app/gradio/pull/8106) [`d0a759f`](https://github.com/gradio-app/gradio/commit/d0a759f3df8b564e2f21421d448b24fecf287306) - Pass Error status in /dev/reload stream. Thanks @freddyaboulton! +- [#7855](https://github.com/gradio-app/gradio/pull/7855) [`611c927`](https://github.com/gradio-app/gradio/commit/611c9273a301e925b5aad93a19272dccd53c39fa) - Lite wheel optimization. Thanks @whitphx! + +### Fixes + +- [#8179](https://github.com/gradio-app/gradio/pull/8179) [`6a218b4`](https://github.com/gradio-app/gradio/commit/6a218b4148095aaa0c58d8c20973ba01c8764fc2) - rework upload to be a class method + pass client into each component. Thanks @pngwn! +- [#8181](https://github.com/gradio-app/gradio/pull/8181) [`cf52ca6`](https://github.com/gradio-app/gradio/commit/cf52ca6a51320ece97f009a177792840b5fbc785) - Ensure connectivity to private HF spaces with SSE protocol. Thanks @hannahblair! + +### Dependency updates + +- @gradio/code@0.6.1 +- @gradio/atoms@0.7.2 +- @gradio/client@0.18.0 +- @gradio/upload@0.10.0 +- @gradio/utils@0.4.1 +- @gradio/wasm@0.10.1 +- @gradio/statustracker@0.5.1 +- @gradio/annotatedimage@0.6.0 +- @gradio/audio@0.11.0 +- @gradio/chatbot@0.10.0 +- @gradio/dataframe@0.8.0 +- @gradio/gallery@0.10.0 +- @gradio/highlightedtext@0.6.0 +- @gradio/image@0.11.0 +- @gradio/imageeditor@0.7.0 +- @gradio/multimodaltextbox@0.4.0 +- @gradio/textbox@0.6.0 +- @gradio/video@0.8.0 +- @gradio/file@0.7.0 +- @gradio/model3d@0.10.0 +- @gradio/simpleimage@0.5.0 +- @gradio/uploadbutton@0.6.1 +- @gradio/button@0.2.33 +- @gradio/dataset@0.1.33 +- @gradio/downloadbutton@0.1.10 +- @gradio/fileexplorer@0.4.1 +- @gradio/accordion@0.3.11 +- @gradio/checkbox@0.3.1 +- @gradio/checkboxgroup@0.5.1 +- @gradio/colorpicker@0.3.1 +- @gradio/column@0.1.1 +- @gradio/dropdown@0.7.1 +- @gradio/fallback@0.3.1 +- @gradio/form@0.1.16 +- @gradio/group@0.1.1 +- @gradio/html@0.2.1 +- @gradio/json@0.2.1 +- @gradio/label@0.3.1 +- @gradio/markdown@0.7.1 +- @gradio/number@0.4.1 +- @gradio/paramviewer@0.4.10 +- @gradio/plot@0.4.1 +- @gradio/radio@0.5.1 +- @gradio/row@0.1.2 +- @gradio/simpledropdown@0.2.1 +- @gradio/simpletextbox@0.2.1 +- @gradio/slider@0.4.1 +- @gradio/tabitem@0.2.8 +- @gradio/tabs@0.2.8 +- @gradio/box@0.1.16 + +## 1.33.0 + +### Highlights + +#### Setting File Upload Limits ([#7909](https://github.com/gradio-app/gradio/pull/7909) [`2afca65`](https://github.com/gradio-app/gradio/commit/2afca6541912b37dc84f447c7ad4af21607d7c72)) + +We have added a `max_file_size` size parameter to `launch()` that limits to size of files uploaded to the server. This limit applies to each individual file. This parameter can be specified as a string or an integer (corresponding to the size in bytes). + +The following code snippet sets a max file size of 5 megabytes. + +```python +import gradio as gr + +demo = gr.Interface(lambda x: x, "image", "image") + +demo.launch(max_file_size="5mb") +# or +demo.launch(max_file_size=5 * gr.FileSize.MB) +``` + +![max_file_size_upload](https://github.com/gradio-app/gradio/assets/41651716/7547330c-a082-4901-a291-3f150a197e45) + + +#### Error states can now be cleared + +When a component encounters an error, the error state shown in the UI can now be cleared by clicking on the `x` icon in the top right of the component. This applies to all types of errors, whether it's raised in the UI or the server. + +![error_modal_calculator](https://github.com/gradio-app/gradio/assets/41651716/16cb071c-accd-45a6-9c18-0dea27d4bd98) + + Thanks @freddyaboulton! + +### Features + +- [#8092](https://github.com/gradio-app/gradio/pull/8092) [`659d3c5`](https://github.com/gradio-app/gradio/commit/659d3c51ae8591b8c90879f17b2b10d1d79cb331) - chore(deps): update dependency iframe-resizer to v4.3.11. Thanks @renovate! +- [#8067](https://github.com/gradio-app/gradio/pull/8067) [`0fb058e`](https://github.com/gradio-app/gradio/commit/0fb058ec232bfaceb24f1515d16a41fa432a1ee8) - Fix the Lite custom element parser so it doesn't add the .code option when the entrypoint file is already specified. Thanks @whitphx! +- [#8051](https://github.com/gradio-app/gradio/pull/8051) [`d665f40`](https://github.com/gradio-app/gradio/commit/d665f409704b4938d57bee6476a2d000617643c8) - Fix custom JS function caller to concat the outputs of a dep to the inputs as the arguments. Thanks @whitphx! +- [#8056](https://github.com/gradio-app/gradio/pull/8056) [`2e469a5`](https://github.com/gradio-app/gradio/commit/2e469a5f99e52a5011a010f46e47dde7bb0c7140) - Using keys to preserve values between reloads. Thanks @aliabid94! +- [#7646](https://github.com/gradio-app/gradio/pull/7646) [`450b8cc`](https://github.com/gradio-app/gradio/commit/450b8cc898f130f15caa3742f65c17b9f7a8f398) - Refactor JS Client. Thanks @hannahblair! +- [#8115](https://github.com/gradio-app/gradio/pull/8115) [`595ebf7`](https://github.com/gradio-app/gradio/commit/595ebf74c5e09ad90fca0ca8a9a312f161a981aa) - Cache an error from app.submit() and show it on frontend. Thanks @whitphx! +- [#8084](https://github.com/gradio-app/gradio/pull/8084) [`1c99570`](https://github.com/gradio-app/gradio/commit/1c99570f3cbf28f020d6e92527754dd4cae3bcdb) - Adjust `View Api` container `z-index`. Thanks @hannahblair! + +### Dependency updates + +- @gradio/atoms@0.7.1 +- @gradio/client@0.17.0 +- @gradio/audio@0.10.0 +- @gradio/label@0.3.0 +- @gradio/accordion@0.3.10 +- @gradio/annotatedimage@0.5.13 +- @gradio/button@0.2.32 +- @gradio/chatbot@0.9.0 +- @gradio/checkbox@0.3.0 +- @gradio/checkboxgroup@0.5.0 +- @gradio/code@0.6.0 +- @gradio/colorpicker@0.3.0 +- @gradio/column@0.1.1 +- @gradio/dataframe@0.7.0 +- @gradio/dataset@0.1.32 +- @gradio/downloadbutton@0.1.9 +- @gradio/dropdown@0.7.0 +- @gradio/fallback@0.3.0 +- @gradio/file@0.6.0 +- @gradio/fileexplorer@0.4.0 +- @gradio/form@0.1.15 +- @gradio/gallery@0.9.0 +- @gradio/group@0.1.1 +- @gradio/highlightedtext@0.5.0 +- @gradio/html@0.2.0 +- @gradio/image@0.10.0 +- @gradio/imageeditor@0.6.0 +- @gradio/json@0.2.0 +- @gradio/markdown@0.7.0 +- @gradio/model3d@0.9.0 +- @gradio/multimodaltextbox@0.3.0 +- @gradio/number@0.4.0 +- @gradio/paramviewer@0.4.9 +- @gradio/plot@0.4.0 +- @gradio/radio@0.5.0 +- @gradio/row@0.1.2 +- @gradio/simpledropdown@0.2.0 +- @gradio/simpleimage@0.4.0 +- @gradio/simpletextbox@0.2.0 +- @gradio/slider@0.4.0 +- @gradio/statustracker@0.5.0 +- @gradio/tabitem@0.2.7 +- @gradio/tabs@0.2.7 +- @gradio/textbox@0.5.0 +- @gradio/uploadbutton@0.6.0 +- @gradio/video@0.7.0 +- @gradio/upload@0.9.0 +- @gradio/utils@0.4.0 +- @gradio/box@0.1.15 + +## 1.32.0 + +### Features + +- [#8062](https://github.com/gradio-app/gradio/pull/8062) [`cecd6e4`](https://github.com/gradio-app/gradio/commit/cecd6e4c4accb3ef220284dc54a87510b3503ec9) - Update dependency iframe-resizer to v4.3.10. Thanks @renovate! +- [#7887](https://github.com/gradio-app/gradio/pull/7887) [`5f0248e`](https://github.com/gradio-app/gradio/commit/5f0248e797af087c7fd0ad35ea7f2bd778a7cc41) - When authenticating with HF OAuth, stay in same tab. Thanks @Wauplin! +- [#7975](https://github.com/gradio-app/gradio/pull/7975) [`c9ddd84`](https://github.com/gradio-app/gradio/commit/c9ddd847d6c57d5efc4e887180d219f2a0b5b98d) - Update the Lite custom element parser. Thanks @whitphx! + +### Fixes + +- [#8028](https://github.com/gradio-app/gradio/pull/8028) [`6fafce0`](https://github.com/gradio-app/gradio/commit/6fafce06704ab8f2cd5fe6fbdb58b842e144e44d) - ensure maps are correctly shallow cloned when updating state. Thanks @pngwn! +- [#7981](https://github.com/gradio-app/gradio/pull/7981) [`c1df2f8`](https://github.com/gradio-app/gradio/commit/c1df2f818ce285a8e7871d43b76c4959beb00956) - Fix example loading for custom components. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/utils@0.3.2 +- @gradio/tabs@0.2.6 +- @gradio/statustracker@0.4.12 +- @gradio/code@0.5.12 +- @gradio/theme@0.2.2 +- @gradio/client@0.16.0 +- @gradio/upload@0.8.5 +- @gradio/atoms@0.7.0 +- @gradio/icons@0.4.0 +- @gradio/audio@0.9.12 +- @gradio/image@0.9.12 +- @gradio/video@0.6.12 +- @gradio/markdown@0.6.10 +- @gradio/chatbot@0.8.3 +- @gradio/dataset@0.1.31 +- @gradio/multimodaltextbox@0.2.5 +- @gradio/imageeditor@0.5.0 +- @gradio/highlightedtext@0.4.15 +- @gradio/plot@0.3.7 +- @gradio/annotatedimage@0.5.12 +- @gradio/button@0.2.31 +- @gradio/downloadbutton@0.1.8 +- @gradio/file@0.5.12 +- @gradio/fileexplorer@0.3.32 +- @gradio/gallery@0.8.8 +- @gradio/model3d@0.8.11 +- @gradio/simpleimage@0.3.12 +- @gradio/uploadbutton@0.5.8 +- @gradio/dataframe@0.6.13 +- @gradio/accordion@0.3.9 +- @gradio/box@0.1.14 +- @gradio/checkbox@0.2.15 +- @gradio/checkboxgroup@0.4.9 +- @gradio/colorpicker@0.2.15 +- @gradio/dropdown@0.6.7 +- @gradio/fallback@0.2.15 +- @gradio/form@0.1.14 +- @gradio/html@0.1.15 +- @gradio/json@0.1.15 +- @gradio/label@0.2.15 +- @gradio/number@0.3.15 +- @gradio/paramviewer@0.4.8 +- @gradio/radio@0.4.10 +- @gradio/simpledropdown@0.1.15 +- @gradio/simpletextbox@0.1.15 +- @gradio/slider@0.3.3 +- @gradio/textbox@0.4.16 +- @gradio/tabitem@0.2.6 + +## 1.31.0 + +### Features + +- [#7811](https://github.com/gradio-app/gradio/pull/7811) [`b43055b`](https://github.com/gradio-app/gradio/commit/b43055b297dfe1aa56fda4cd722d878f7297a1b5) - Lite playground design changes. Thanks @aliabd! +- [#7850](https://github.com/gradio-app/gradio/pull/7850) [`2bae1cf`](https://github.com/gradio-app/gradio/commit/2bae1cfbd41ed8ae3eea031a64899611a22a1821) - Adds an "API Recorder" to the view API page, some internal methods have been made async. Thanks @abidlabs! + +### Fixes + +- [#7963](https://github.com/gradio-app/gradio/pull/7963) [`1eb4c20`](https://github.com/gradio-app/gradio/commit/1eb4c2012065c678d722477f3555ec45a9e78c14) - ensure kwargs are always in sync across the whole application. Thanks @pngwn! +- [#7916](https://github.com/gradio-app/gradio/pull/7916) [`7c9a964`](https://github.com/gradio-app/gradio/commit/7c9a964ac6b9c2231c7439758e87b4fac56db99f) - Fix programmatic tab selection. Thanks @aliabid94! + +### Dependency updates + +- @gradio/utils@0.3.1 +- @gradio/atoms@0.6.2 +- @gradio/statustracker@0.4.11 +- @gradio/code@0.5.11 +- @gradio/upload@0.8.4 +- @gradio/tabs@0.2.5 +- @gradio/image@0.9.11 +- @gradio/markdown@0.6.9 +- @gradio/theme@0.2.1 +- @gradio/dropdown@0.6.6 +- @gradio/imageeditor@0.4.11 +- @gradio/checkboxgroup@0.4.8 +- @gradio/client@0.15.1 +- @gradio/chatbot@0.8.2 +- @gradio/gallery@0.8.7 +- @gradio/multimodaltextbox@0.2.4 +- @gradio/video@0.6.11 +- @gradio/dataframe@0.6.12 +- @gradio/highlightedtext@0.4.14 +- @gradio/plot@0.3.6 +- @gradio/annotatedimage@0.5.11 +- @gradio/audio@0.9.11 +- @gradio/button@0.2.30 +- @gradio/dataset@0.1.30 +- @gradio/downloadbutton@0.1.7 +- @gradio/file@0.5.11 +- @gradio/fileexplorer@0.3.31 +- @gradio/model3d@0.8.10 +- @gradio/simpleimage@0.3.11 +- @gradio/uploadbutton@0.5.7 +- @gradio/accordion@0.3.8 +- @gradio/checkbox@0.2.14 +- @gradio/colorpicker@0.2.14 +- @gradio/fallback@0.2.14 +- @gradio/form@0.1.13 +- @gradio/html@0.1.14 +- @gradio/json@0.1.14 +- @gradio/label@0.2.14 +- @gradio/number@0.3.14 +- @gradio/paramviewer@0.4.7 +- @gradio/radio@0.4.9 +- @gradio/simpledropdown@0.1.14 +- @gradio/simpletextbox@0.1.14 +- @gradio/slider@0.3.2 +- @gradio/tabitem@0.2.5 +- @gradio/textbox@0.4.15 +- @gradio/box@0.1.13 + +## 1.30.1 + +### Fixes + +- [#7865](https://github.com/gradio-app/gradio/pull/7865) [`7bbc3b6`](https://github.com/gradio-app/gradio/commit/7bbc3b62bf85af2d2230e5f7539efb7f1f0007a1) - JS functions break entire app if there's no input, fixed. Thanks @aliabid94! + +### Dependency updates + +- @gradio/button@0.2.29 +- @gradio/upload@0.8.3 +- @gradio/code@0.5.10 +- @gradio/client@0.15.0 +- @gradio/multimodaltextbox@0.2.3 +- @gradio/annotatedimage@0.5.10 +- @gradio/audio@0.9.10 +- @gradio/chatbot@0.8.1 +- @gradio/dataset@0.1.29 +- @gradio/downloadbutton@0.1.6 +- @gradio/file@0.5.10 +- @gradio/fileexplorer@0.3.30 +- @gradio/gallery@0.8.6 +- @gradio/image@0.9.10 +- @gradio/imageeditor@0.4.10 +- @gradio/model3d@0.8.9 +- @gradio/simpleimage@0.3.10 +- @gradio/uploadbutton@0.5.6 +- @gradio/video@0.6.10 +- @gradio/dataframe@0.6.11 + +## 1.30.0 + +### Features + +- [#7852](https://github.com/gradio-app/gradio/pull/7852) [`72661e3`](https://github.com/gradio-app/gradio/commit/72661e3391a432ccd77fb3100e69fbdf95093931) - Revert the minify setting in vite.config.js which was mistakingly introduced in #6261. Thanks @whitphx! + +### Dependency updates + +- @gradio/button@0.2.28 +- @gradio/atoms@0.6.1 +- @gradio/statustracker@0.4.10 +- @gradio/code@0.5.9 +- @gradio/icons@0.3.4 +- @gradio/upload@0.8.2 +- @gradio/audio@0.9.9 +- @gradio/image@0.9.9 +- @gradio/video@0.6.9 +- @gradio/markdown@0.6.8 +- @gradio/chatbot@0.8.0 +- @gradio/downloadbutton@0.1.5 +- @gradio/multimodaltextbox@0.2.2 +- @gradio/annotatedimage@0.5.9 +- @gradio/dropdown@0.6.5 +- @gradio/file@0.5.9 +- @gradio/fileexplorer@0.3.29 +- @gradio/form@0.1.12 +- @gradio/gallery@0.8.5 +- @gradio/highlightedtext@0.4.13 +- @gradio/imageeditor@0.4.9 +- @gradio/json@0.1.13 +- @gradio/label@0.2.13 +- @gradio/model3d@0.8.8 +- @gradio/plot@0.3.5 +- @gradio/simpledropdown@0.1.13 +- @gradio/simpleimage@0.3.9 +- @gradio/simpletextbox@0.1.13 +- @gradio/textbox@0.4.14 +- @gradio/dataframe@0.6.10 +- @gradio/dataset@0.1.28 +- @gradio/uploadbutton@0.5.5 +- @gradio/accordion@0.3.7 +- @gradio/box@0.1.12 +- @gradio/checkbox@0.2.13 +- @gradio/checkboxgroup@0.4.7 +- @gradio/colorpicker@0.2.13 +- @gradio/fallback@0.2.13 +- @gradio/html@0.1.13 +- @gradio/number@0.3.13 +- @gradio/paramviewer@0.4.6 +- @gradio/radio@0.4.8 +- @gradio/slider@0.3.1 + +## 1.29.0 + +### Features + +- [#7684](https://github.com/gradio-app/gradio/pull/7684) [`755157f`](https://github.com/gradio-app/gradio/commit/755157f99c2961f2e5caeaa9b76d248b4225ea8f) - Do not reload code inside gr.NO_RELOAD context. Thanks @freddyaboulton! +- [#7732](https://github.com/gradio-app/gradio/pull/7732) [`2efb05e`](https://github.com/gradio-app/gradio/commit/2efb05ed99a8a3575aab0a6c14a8d8b91f4e9ed7) - Adds support for kwargs and default arguments in the python client, and improves how parameter information is displayed in the "view API" page. Thanks @abidlabs! + +### Dependency updates + +- @gradio/code@0.5.8 +- @gradio/upload@0.8.1 +- @gradio/button@0.2.27 +- @gradio/statustracker@0.4.9 +- @gradio/radio@0.4.7 +- @gradio/wasm@0.10.0 +- @gradio/atoms@0.6.0 +- @gradio/slider@0.3.0 +- @gradio/dataset@0.1.27 +- @gradio/annotatedimage@0.5.8 +- @gradio/audio@0.9.8 +- @gradio/file@0.5.8 +- @gradio/image@0.9.8 +- @gradio/imageeditor@0.4.8 +- @gradio/model3d@0.8.7 +- @gradio/simpleimage@0.3.8 +- @gradio/video@0.6.8 +- @gradio/accordion@0.3.6 +- @gradio/box@0.1.11 +- @gradio/chatbot@0.7.8 +- @gradio/checkbox@0.2.12 +- @gradio/checkboxgroup@0.4.6 +- @gradio/colorpicker@0.2.12 +- @gradio/dataframe@0.6.9 +- @gradio/dropdown@0.6.4 +- @gradio/fallback@0.2.12 +- @gradio/fileexplorer@0.3.28 +- @gradio/form@0.1.11 +- @gradio/gallery@0.8.4 +- @gradio/highlightedtext@0.4.12 +- @gradio/html@0.1.12 +- @gradio/json@0.1.12 +- @gradio/label@0.2.12 +- @gradio/markdown@0.6.7 +- @gradio/multimodaltextbox@0.2.1 +- @gradio/number@0.3.12 +- @gradio/paramviewer@0.4.5 +- @gradio/plot@0.3.4 +- @gradio/simpledropdown@0.1.12 +- @gradio/simpletextbox@0.1.12 +- @gradio/textbox@0.4.13 +- @gradio/uploadbutton@0.5.4 +- @gradio/downloadbutton@0.1.4 + +## 1.28.0 + +### Features + +- [#7420](https://github.com/gradio-app/gradio/pull/7420) [`15da39f`](https://github.com/gradio-app/gradio/commit/15da39fca01d09a30cf47e7e72d7efa5052f61f8) - Multimodal Textbox (Chat Input Component). Thanks @dawoodkhan82! +- [#7660](https://github.com/gradio-app/gradio/pull/7660) [`f739bef`](https://github.com/gradio-app/gradio/commit/f739bef6c70a2b012dd896456709eae5ee4de7d5) - Add Playground to Lite Custom Element. Thanks @aliabd! +- [#7710](https://github.com/gradio-app/gradio/pull/7710) [`0a3870d`](https://github.com/gradio-app/gradio/commit/0a3870d52b5efc64b2d1f9a8c7314a3a8b48b90a) - Call handle_darkmode() even if `window.__gradio_mode__ === "website"` but enforce the light theme. Thanks @whitphx! + +### Fixes + +- [#7564](https://github.com/gradio-app/gradio/pull/7564) [`5d1e8da`](https://github.com/gradio-app/gradio/commit/5d1e8dae5ac23f605c3b5f41dbe18751dff380a0) - batch UI updates on a per frame basis. Thanks @pngwn! +- [#7691](https://github.com/gradio-app/gradio/pull/7691) [`84f81fe`](https://github.com/gradio-app/gradio/commit/84f81fec9287b041203a141bbf2852720f7d199c) - Fix race condition between state updates and loading_status updates. Thanks @aliabid94! +- [#7709](https://github.com/gradio-app/gradio/pull/7709) [`f67759d`](https://github.com/gradio-app/gradio/commit/f67759dcee665cfd7c44f102f36ab23128ee2c2c) - Fix wasm_proxied_mount_css to not reuse an existing style element. Thanks @whitphx! +- [#7703](https://github.com/gradio-app/gradio/pull/7703) [`598ad7b`](https://github.com/gradio-app/gradio/commit/598ad7baf722181a25200e9a8ba858bae39c7d82) - fix dev mode. Thanks @pngwn! + +### Dependency updates + +- @gradio/code@0.5.7 +- @gradio/radio@0.4.6 +- @gradio/client@0.14.0 +- @gradio/multimodaltextbox@0.2.0 +- @gradio/upload@0.8.0 +- @gradio/wasm@0.9.0 +- @gradio/accordion@0.3.5 +- @gradio/dataframe@0.6.8 +- @gradio/dataset@0.1.26 +- @gradio/markdown@0.6.6 +- @gradio/annotatedimage@0.5.7 +- @gradio/audio@0.9.7 +- @gradio/button@0.2.26 +- @gradio/chatbot@0.7.7 +- @gradio/downloadbutton@0.1.3 +- @gradio/file@0.5.7 +- @gradio/fileexplorer@0.3.27 +- @gradio/gallery@0.8.3 +- @gradio/image@0.9.7 +- @gradio/imageeditor@0.4.7 +- @gradio/model3d@0.8.6 +- @gradio/simpleimage@0.3.7 +- @gradio/uploadbutton@0.5.3 +- @gradio/video@0.6.7 + +## 1.27.0 + +### Features + +- [#7577](https://github.com/gradio-app/gradio/pull/7577) [`7c66a29`](https://github.com/gradio-app/gradio/commit/7c66a29dea0e4e56106d95d93972225b886a9df3) - Fix the Lite custom element to initialize the app in the connected callback and dispose the app in the disconnected callback. Thanks @whitphx! + +### Fixes + +- [#7643](https://github.com/gradio-app/gradio/pull/7643) [`9482c7a`](https://github.com/gradio-app/gradio/commit/9482c7a3d2d7b6abd81b786100ddc385133095a5) - fix: redundant meta tags that are unwanted. Thanks @qkdxorjs1002! +- [#7628](https://github.com/gradio-app/gradio/pull/7628) [`ba8cc48`](https://github.com/gradio-app/gradio/commit/ba8cc48b136e701717c0c5d15ce410b4f8bf6a21) - feature detect CSSStylesheet. Thanks @pngwn! +- [#7575](https://github.com/gradio-app/gradio/pull/7575) [`d0688b3`](https://github.com/gradio-app/gradio/commit/d0688b3c25feabb4fc7dfa0ab86086b3af7eb337) - Files should now be supplied as `file(...)` in the Client, and some fixes to `gr.load()` as well. Thanks @abidlabs! + +### Dependency updates + +- @gradio/button@0.2.25 +- @gradio/upload@0.7.7 +- @gradio/code@0.5.6 +- @gradio/client@0.13.0 +- @gradio/wasm@0.8.0 +- @gradio/annotatedimage@0.5.6 +- @gradio/audio@0.9.6 +- @gradio/chatbot@0.7.6 +- @gradio/dataset@0.1.25 +- @gradio/downloadbutton@0.1.2 +- @gradio/file@0.5.6 +- @gradio/fileexplorer@0.3.26 +- @gradio/gallery@0.8.2 +- @gradio/image@0.9.6 +- @gradio/imageeditor@0.4.6 +- @gradio/model3d@0.8.5 +- @gradio/simpleimage@0.3.6 +- @gradio/uploadbutton@0.5.2 +- @gradio/video@0.6.6 +- @gradio/dataframe@0.6.7 + +## 1.26.1 + +### Patch Changes + +- Updated dependencies [[`8181695`](https://github.com/gradio-app/gradio/commit/8181695e70187e8bc2bf7518697098c8d1b9843d)]: + - @gradio/upload@0.7.6 + - @gradio/annotatedimage@0.5.5 + - @gradio/audio@0.9.5 + - @gradio/button@0.2.24 + - @gradio/chatbot@0.7.5 + - @gradio/code@0.5.5 + - @gradio/dataframe@0.6.6 + - @gradio/dataset@0.1.24 + - @gradio/file@0.5.5 + - @gradio/fileexplorer@0.3.25 + - @gradio/gallery@0.8.1 + - @gradio/image@0.9.5 + - @gradio/imageeditor@0.4.5 + - @gradio/model3d@0.8.4 + - @gradio/simpleimage@0.3.5 + - @gradio/uploadbutton@0.5.1 + - @gradio/video@0.6.5 + - @gradio/downloadbutton@0.1.1 + +## 1.26.0 + +### Features + +- [#7345](https://github.com/gradio-app/gradio/pull/7345) [`561579d`](https://github.com/gradio-app/gradio/commit/561579d9b7b860c5cb3f8131e0dced0c8114463f) - fix-tests. Thanks [@pngwn](https://github.com/pngwn)! +- [#7518](https://github.com/gradio-app/gradio/pull/7518) [`bd2c695`](https://github.com/gradio-app/gradio/commit/bd2c69532801f9a0626cd1192de158bde6068b9a) - Adds a `gr.DownloadButton` component. Thanks [@abidlabs](https://github.com/abidlabs)! + +### Fixes + +- [#7510](https://github.com/gradio-app/gradio/pull/7510) [`08c2d49`](https://github.com/gradio-app/gradio/commit/08c2d491ecac83268ad20f05769ef7e1335089e2) - when adding custom head html, ensure there are no duplicate meta tags. Thanks [@qkdxorjs1002](https://github.com/qkdxorjs1002)! +- [#7545](https://github.com/gradio-app/gradio/pull/7545) [`1fa2e91`](https://github.com/gradio-app/gradio/commit/1fa2e914ca4663a47743cc34acdf98c7fcc469c8) - Fixes `auth_message` so that it correctly renders HTML. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 1.25.2 + +### Fixes + +- [#7505](https://github.com/gradio-app/gradio/pull/7505) [`b186767`](https://github.com/gradio-app/gradio/commit/b18676774448f44a2ef3a9490224703254cffa7c) - Fix `Gallery` preview overlay and backdrop. Thanks [@MMP0](https://github.com/MMP0)! + +## 1.25.1 + +### Patch Changes + +- Updated dependencies [[`f52cab6`](https://github.com/gradio-app/gradio/commit/f52cab634b94638d7f4625d40bf3d9afbe68040b), [`6b8a7e5`](https://github.com/gradio-app/gradio/commit/6b8a7e5d36887cdfcfbfec1536a915128df0d6b2), [`f191786`](https://github.com/gradio-app/gradio/commit/f1917867916647d383b8d7ce15e0c17f2abbdec1), [`3e4e680`](https://github.com/gradio-app/gradio/commit/3e4e680a52ba5a73c108ef1b328dacd7b6e4b566)]: + - @gradio/fileexplorer@0.3.22 + - @gradio/dropdown@0.6.1 + - @gradio/audio@0.9.2 + - @gradio/icons@0.3.3 + - @gradio/chatbot@0.7.2 + - @gradio/annotatedimage@0.5.2 + - @gradio/atoms@0.5.3 + - @gradio/code@0.5.2 + - @gradio/file@0.5.2 + - @gradio/form@0.1.10 + - @gradio/gallery@0.7.2 + - @gradio/highlightedtext@0.4.10 + - @gradio/image@0.9.2 + - @gradio/imageeditor@0.4.2 + - @gradio/json@0.1.10 + - @gradio/label@0.2.10 + - @gradio/model3d@0.8.1 + - @gradio/plot@0.3.2 + - @gradio/simpledropdown@0.1.10 + - @gradio/simpleimage@0.3.2 + - @gradio/simpletextbox@0.1.10 + - @gradio/statustracker@0.4.7 + - @gradio/textbox@0.4.11 + - @gradio/upload@0.7.4 + - @gradio/video@0.6.2 + - @gradio/accordion@0.3.3 + - @gradio/box@0.1.10 + - @gradio/checkbox@0.2.10 + - @gradio/checkboxgroup@0.4.4 + - @gradio/colorpicker@0.2.10 + - @gradio/dataframe@0.6.3 + - @gradio/dataset@0.1.22 + - @gradio/fallback@0.2.10 + - @gradio/html@0.1.10 + - @gradio/markdown@0.6.4 + - @gradio/number@0.3.10 + - @gradio/paramviewer@0.4.3 + - @gradio/radio@0.4.4 + - @gradio/slider@0.2.10 + - @gradio/button@0.2.22 + - @gradio/uploadbutton@0.4.7 + +## 1.25.0 + +### Fixes + +- [#7410](https://github.com/gradio-app/gradio/pull/7410) [`c2dfc59`](https://github.com/gradio-app/gradio/commit/c2dfc592a4988efd5a96a062eec3fb4906f71748) - remove static while pending behaviour. Thanks [@pngwn](https://github.com/pngwn)! +- [#7404](https://github.com/gradio-app/gradio/pull/7404) [`065c5b1`](https://github.com/gradio-app/gradio/commit/065c5b163c4badb9d9cbd06d627fb4ba086003e7) - Add `.key_up` event listener to `gr.Dropdown()`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#7395](https://github.com/gradio-app/gradio/pull/7395) [`46b4568`](https://github.com/gradio-app/gradio/commit/46b45683e1ea9eb40013121a8de5bee7aa98bf0b) - Allow applying `@media`, `@keyframes` and `@import` in custom CSS. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 1.24.0 + +### Features + +- [#7183](https://github.com/gradio-app/gradio/pull/7183) [`49d9c48`](https://github.com/gradio-app/gradio/commit/49d9c48537aa706bf72628e3640389470138bdc6) - [WIP] Refactor file normalization to be in the backend and remove it from the frontend of each component. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#6890](https://github.com/gradio-app/gradio/pull/6890) [`cccab27`](https://github.com/gradio-app/gradio/commit/cccab27fe8b6ae6860b3fff68694fa33060e18a7) - E2E tests for Lite. Thanks [@whitphx](https://github.com/whitphx)! + +### Fixes + +- [#7354](https://github.com/gradio-app/gradio/pull/7354) [`a7fa47a`](https://github.com/gradio-app/gradio/commit/a7fa47a175fbcf0fd6573ca19334a3a55b55bb24) - ensure Dataframes in background tabs are visible when the tab is selected. Thanks [@pngwn](https://github.com/pngwn)! +- [#7355](https://github.com/gradio-app/gradio/pull/7355) [`2244059`](https://github.com/gradio-app/gradio/commit/2244059cdbacb713530a3b760205c5464c05491c) - Ensure CSS `.dark` rule selectors are applied. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 1.23.0 + +### Features + +- [#7129](https://github.com/gradio-app/gradio/pull/7129) [`ccdaec4`](https://github.com/gradio-app/gradio/commit/ccdaec45002d0a9d6016e8e2078b843a1ff9172b) - Add a `simpleimage` template for custom components. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#7313](https://github.com/gradio-app/gradio/pull/7313) [`edfd05d`](https://github.com/gradio-app/gradio/commit/edfd05d18d20542d350d883d5e7f84d2774ad99e) - Expand chatinterface to full window height. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#7292](https://github.com/gradio-app/gradio/pull/7292) [`aa97a5e`](https://github.com/gradio-app/gradio/commit/aa97a5e33a04ef8a0309b6ec3b2df4caaa26173e) - Improvements to API Docs. Thanks [@abidlabs](https://github.com/abidlabs)! + +### Fixes + +- [#7117](https://github.com/gradio-app/gradio/pull/7117) [`24157a3`](https://github.com/gradio-app/gradio/commit/24157a36028b3f606194bd9977634318650b2d46) - add background color based on the OS mode. Thanks [@aileenvl](https://github.com/aileenvl)! + +## 1.22.0 + +### Features + +- [#7084](https://github.com/gradio-app/gradio/pull/7084) [`94aa271`](https://github.com/gradio-app/gradio/commit/94aa271ab11fc3426a7e143ebaa757eb30c9911d) - Improve rapid generation performance via UI throttling. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#7148](https://github.com/gradio-app/gradio/pull/7148) [`c60ad4d`](https://github.com/gradio-app/gradio/commit/c60ad4d34ab5b56a89bf6796822977e51e7a4a32) - Use Gallery as input component. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Fixes + +- [#7125](https://github.com/gradio-app/gradio/pull/7125) [`45f725f`](https://github.com/gradio-app/gradio/commit/45f725f8d0dc7813b3d2e768ca9582d6ad878d6f) - un-disable output components after exception is raised. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 1.21.0 + +### Highlights + +#### Custom component documentation generator ([#7030](https://github.com/gradio-app/gradio/pull/7030) [`3a944ed`](https://github.com/gradio-app/gradio/commit/3a944ed9f162a224d26959a9c556346a9d205311)) + +If your custom component has type hints and docstrings for both parameters and return values, you can now automatically generate a documentation page and README.md with no additional effort. Simply run the following command: + +```sh +gradio cc docs +``` + +This will generate a Gradio app that you can upload to spaces providing rich documentation for potential users. The documentation page includes: + +- Installation instructions. +- A live embedded demo and working code snippet, pulled from your demo app. +- An API reference for initialising the component, with types, default values and descriptions. +- An explanation of how the component affects the user's predict function inputs and outputs. +- Any additional interfaces or classes that are necessary to understand the API reference. +- Optional links to GitHub, PyPi, and Hugging Face Spaces. + +A README will also be generated detailing the same information but in a format that is optimised for viewing on GitHub or PyPi! + +Thanks [@pngwn](https://github.com/pngwn)! + +## 1.20.0 + +### Features + +- [#6965](https://github.com/gradio-app/gradio/pull/6965) [`5d00dd3`](https://github.com/gradio-app/gradio/commit/5d00dd37ca14bbfef2ceac550b29dbe05ba8cab0) - Make Wasm-compatible. Thanks [@whitphx](https://github.com/whitphx)! + +### Fixes + +- [#6967](https://github.com/gradio-app/gradio/pull/6967) [`5e00162`](https://github.com/gradio-app/gradio/commit/5e0016267f1d683e2daab82ee4a33d2f09513a34) - Make Wasm-compatible. Thanks [@whitphx](https://github.com/whitphx)! +- [#6983](https://github.com/gradio-app/gradio/pull/6983) [`6e285be`](https://github.com/gradio-app/gradio/commit/6e285be8edeacf8730bac10b7ecd3fd5e309a950) - Fix the reloader. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 1.19.0 + +### Features + +- [#6778](https://github.com/gradio-app/gradio/pull/6778) [`8a093e2`](https://github.com/gradio-app/gradio/commit/8a093e23d7993a044e5e0ff73f93a74cb75dad56) - Add a dev instruction for lite in SharedWorker mode. Thanks [@whitphx](https://github.com/whitphx)! + +## 1.18.0 + +### Features + +- [#6839](https://github.com/gradio-app/gradio/pull/6839) [`e974cf0`](https://github.com/gradio-app/gradio/commit/e974cf045c82ce8d79efdda36b9dbf6ea557baa4) - Custom JS Guide. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! + +### Fixes + +- [#6863](https://github.com/gradio-app/gradio/pull/6863) [`d406855`](https://github.com/gradio-app/gradio/commit/d4068557953746662235d595ec435c42ceb24414) - Fix JS Client when app is running behind a proxy. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6846](https://github.com/gradio-app/gradio/pull/6846) [`48d6534`](https://github.com/gradio-app/gradio/commit/48d6534b40f80e7e70a4061f97d9f2e23ba77fe1) - Add `show_api` parameter to events, and fix `gr.load()`. Also makes some minor improvements to the "view API" page when running on Spaces. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 1.17.0 + +### Features + +- [#6831](https://github.com/gradio-app/gradio/pull/6831) [`f3abde8`](https://github.com/gradio-app/gradio/commit/f3abde80884d96ad69b825020c46486d9dd5cac5) - Add an option to enable header links for markdown. Thanks [@pngwn](https://github.com/pngwn)! + +### Fixes + +- [#6766](https://github.com/gradio-app/gradio/pull/6766) [`73268ee`](https://github.com/gradio-app/gradio/commit/73268ee2e39f23ebdd1e927cb49b8d79c4b9a144) - Improve source selection UX. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 1.16.2 + +### Patch Changes + +- Updated dependencies [[`245d58e`](https://github.com/gradio-app/gradio/commit/245d58eff788e8d44a59d37a2d9b26d0f08a62b4), [`c352811`](https://github.com/gradio-app/gradio/commit/c352811f76d4126613ece0a584f8c552fdd8d1f6)]: + - @gradio/client@0.9.2 + - @gradio/audio@0.6.2 + - @gradio/imageeditor@0.1.5 + - @gradio/annotatedimage@0.3.12 + - @gradio/button@0.2.12 + - @gradio/chatbot@0.5.4 + - @gradio/dataset@0.1.12 + - @gradio/file@0.4.2 + - @gradio/fileexplorer@0.3.12 + - @gradio/gallery@0.4.13 + - @gradio/image@0.5.2 + - @gradio/model3d@0.4.10 + - @gradio/upload@0.5.5 + - @gradio/uploadbutton@0.3.3 + - @gradio/video@0.2.2 + - @gradio/dataframe@0.4.2 + - @gradio/code@0.3.2 + +## 1.16.1 + +### Patch Changes + +- Updated dependencies [[`5d51fbc`](https://github.com/gradio-app/gradio/commit/5d51fbce7826da840a2fd4940feb5d9ad6f1bc5a), [`34f9431`](https://github.com/gradio-app/gradio/commit/34f943101bf7dd6b8a8974a6131c1ed7c4a0dac0)]: + - @gradio/model3d@0.4.9 + - @gradio/upload@0.5.4 + - @gradio/client@0.9.1 + - @gradio/annotatedimage@0.3.11 + - @gradio/audio@0.6.1 + - @gradio/button@0.2.11 + - @gradio/chatbot@0.5.3 + - @gradio/code@0.3.1 + - @gradio/dataframe@0.4.1 + - @gradio/dataset@0.1.11 + - @gradio/file@0.4.1 + - @gradio/fileexplorer@0.3.11 + - @gradio/gallery@0.4.12 + - @gradio/image@0.5.1 + - @gradio/imageeditor@0.1.4 + - @gradio/uploadbutton@0.3.2 + - @gradio/video@0.2.1 + +## 1.16.0 + +### Features + +- [#6398](https://github.com/gradio-app/gradio/pull/6398) [`67ddd40`](https://github.com/gradio-app/gradio/commit/67ddd40b4b70d3a37cb1637c33620f8d197dbee0) - Lite v4. Thanks [@whitphx](https://github.com/whitphx)! +- [#6738](https://github.com/gradio-app/gradio/pull/6738) [`f3c4d78`](https://github.com/gradio-app/gradio/commit/f3c4d78b710854b94d9a15db78178e504a02c680) - reload on css changes + fix css specificity. Thanks [@pngwn](https://github.com/pngwn)! + +### Fixes + +- [#6639](https://github.com/gradio-app/gradio/pull/6639) [`9a6ff70`](https://github.com/gradio-app/gradio/commit/9a6ff704cd8429289c5376d3af5e4b8492df4773) - Fix issue with `head` param when adding more than one script tag. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! + +## 1.15.0 + +### Features + +- [#6512](https://github.com/gradio-app/gradio/pull/6512) [`4f040c7`](https://github.com/gradio-app/gradio/commit/4f040c752bb3b0586a4e16eca25a1e5f596eee48) - Update zh-CN.json. Thanks [@cibimo](https://github.com/cibimo)! + +## 1.14.0 + +### Features + +- [#6537](https://github.com/gradio-app/gradio/pull/6537) [`6d3fecfa4`](https://github.com/gradio-app/gradio/commit/6d3fecfa42dde1c70a60c397434c88db77289be6) - chore(deps): update all non-major dependencies. Thanks [@renovate](https://github.com/apps/renovate)! + +### Fixes + +- [#6530](https://github.com/gradio-app/gradio/pull/6530) [`13ef0f0ca`](https://github.com/gradio-app/gradio/commit/13ef0f0caa13e5a1cea70d572684122419419599) - Quick fix: Make component interactive when it is in focus. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! + +## 1.13.1 + +### Fixes + +- [#6536](https://github.com/gradio-app/gradio/pull/6536) [`1bbd6cab3`](https://github.com/gradio-app/gradio/commit/1bbd6cab3f0abe183b514b82061f0937c8480966) - Fix undefined `data` TypeError in Blocks. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 1.13.0 + +### Highlights + +#### New `ImageEditor` component ([#6169](https://github.com/gradio-app/gradio/pull/6169) [`9caddc17b`](https://github.com/gradio-app/gradio/commit/9caddc17b1dea8da1af8ba724c6a5eab04ce0ed8)) + +A brand new component, completely separate from `Image` that provides simple editing capabilities. + +- Set background images from file uploads, webcam, or just paste! +- Crop images with an improved cropping UI. App authors can event set specific crop size, or crop ratios (`1:1`, etc) +- Paint on top of any image (or no image) and erase any mistakes! +- The ImageEditor supports layers, confining draw and erase actions to that layer. +- More flexible access to data. The image component returns a composite image representing the final state of the canvas as well as providing the background and all layers as individual images. +- Fully customisable. All features can be enabled and disabled. Even the brush color swatches can be customised. + + + +```py + +def fn(im): + im["composite"] # the full canvas + im["background"] # the background image + im["layers"] # a list of individual layers + + +im = gr.ImageEditor( + # decide which sources you'd like to accept + sources=["upload", "webcam", "clipboard"], + # set a cropsize constraint, can either be a ratio or a concrete [width, height] + crop_size="1:1", + # enable crop (or disable it) + transforms=["crop"], + # customise the brush + brush=Brush( + default_size="25", # or leave it as 'auto' + color_mode="fixed", # 'fixed' hides the user swatches and colorpicker, 'defaults' shows it + default_color="hotpink", # html names are supported + colors=[ + "rgba(0, 150, 150, 1)", # rgb(a) + "#fff", # hex rgb + "hsl(360, 120, 120)" # in fact any valid colorstring + ] + ), + brush=Eraser(default_size="25") +) + +``` + +Thanks [@pngwn](https://github.com/pngwn)! + +## 1.12.0 + +### Features + +- [#6427](https://github.com/gradio-app/gradio/pull/6427) [`e0fc14659`](https://github.com/gradio-app/gradio/commit/e0fc146598ba9b081bc5fa9616d0a41c2aba2427) - Allow google analytics to work on Spaces (and other iframe situations). Thanks [@abidlabs](https://github.com/abidlabs)! + +### Fixes + +- [#6254](https://github.com/gradio-app/gradio/pull/6254) [`f816136a0`](https://github.com/gradio-app/gradio/commit/f816136a039fa6011be9c4fb14f573e4050a681a) - Add volume control to Audio. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#6457](https://github.com/gradio-app/gradio/pull/6457) [`d00fcf89d`](https://github.com/gradio-app/gradio/commit/d00fcf89d1c3ecbc910e81bb1311479ec2b73e4e) - Gradio custom component dev mode now detects changes to Example.svelte file. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 1.11.0 + +### Features + +- [#6099](https://github.com/gradio-app/gradio/pull/6099) [`d84209703`](https://github.com/gradio-app/gradio/commit/d84209703b7a0728cdb49221e543500ddb6a8d33) - Lite: SharedWorker mode. Thanks [@whitphx](https://github.com/whitphx)! + +### Fixes + +- [#6383](https://github.com/gradio-app/gradio/pull/6383) [`324867f63`](https://github.com/gradio-app/gradio/commit/324867f63c920113d89a565892aa596cf8b1e486) - Fix event target. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 1.10.3 + +### Patch Changes + +- Updated dependencies [[`6204ccac5`](https://github.com/gradio-app/gradio/commit/6204ccac5967763e0ebde550d04d12584243a120), [`4d3aad33a`](https://github.com/gradio-app/gradio/commit/4d3aad33a0b66639dbbb2928f305a79fb7789b2d), [`854b482f5`](https://github.com/gradio-app/gradio/commit/854b482f598e0dc47673846631643c079576da9c), [`55fda81fa`](https://github.com/gradio-app/gradio/commit/55fda81fa5918b48952729232d6e2fc55af9351d), [`37dd335e5`](https://github.com/gradio-app/gradio/commit/37dd335e5f04a8e689dd7f23ae24ad1934ea08d8), [`f1409f95e`](https://github.com/gradio-app/gradio/commit/f1409f95ed39c5565bed6a601e41f94e30196a57)]: + - @gradio/image@0.3.4 + - @gradio/upload@0.4.0 + - @gradio/code@0.2.4 + - @gradio/textbox@0.4.2 + - @gradio/audio@0.5.0 + - @gradio/client@0.8.0 + - @gradio/gallery@0.4.5 + - @gradio/row@0.1.0 + - @gradio/video@0.1.4 + - @gradio/annotatedimage@0.3.4 + - @gradio/button@0.2.4 + - @gradio/chatbot@0.4.4 + - @gradio/dataframe@0.3.5 + - @gradio/dataset@0.1.4 + - @gradio/file@0.2.4 + - @gradio/fileexplorer@0.3.4 + - @gradio/model3d@0.4.2 + - @gradio/uploadbutton@0.1.4 + +## 1.10.2 + +### Patch Changes + +- Updated dependencies [[`4b1011bab`](https://github.com/gradio-app/gradio/commit/4b1011bab03c0b6a09329e0beb9c1b17b2189878), [`bca6c2c80`](https://github.com/gradio-app/gradio/commit/bca6c2c80f7e5062427019de45c282238388af95), [`19af2806a`](https://github.com/gradio-app/gradio/commit/19af2806a58419cc551d2d1d6d8987df0db91ccb), [`d3b53a457`](https://github.com/gradio-app/gradio/commit/d3b53a4577ea05cd27e37ce7fec952028c18ed45), [`3cdeabc68`](https://github.com/gradio-app/gradio/commit/3cdeabc6843000310e1a9e1d17190ecbf3bbc780), [`fad92c29d`](https://github.com/gradio-app/gradio/commit/fad92c29dc1f5cd84341aae417c495b33e01245f)]: + - @gradio/chatbot@0.4.3 + - @gradio/client@0.7.2 + - @gradio/audio@0.4.3 + - @gradio/dataframe@0.3.4 + - @gradio/atoms@0.2.1 + - @gradio/upload@0.3.3 + - @gradio/video@0.1.3 + - @gradio/annotatedimage@0.3.3 + - @gradio/button@0.2.3 + - @gradio/dataset@0.1.3 + - @gradio/file@0.2.3 + - @gradio/fileexplorer@0.3.3 + - @gradio/gallery@0.4.4 + - @gradio/image@0.3.3 + - @gradio/model3d@0.4.1 + - @gradio/uploadbutton@0.1.3 + - @gradio/accordion@0.2.1 + - @gradio/box@0.1.1 + - @gradio/checkbox@0.2.1 + - @gradio/checkboxgroup@0.3.2 + - @gradio/code@0.2.3 + - @gradio/colorpicker@0.2.1 + - @gradio/dropdown@0.3.1 + - @gradio/fallback@0.2.1 + - @gradio/form@0.1.1 + - @gradio/highlightedtext@0.4.1 + - @gradio/html@0.1.1 + - @gradio/json@0.1.1 + - @gradio/label@0.2.1 + - @gradio/markdown@0.3.1 + - @gradio/number@0.3.1 + - @gradio/plot@0.2.1 + - @gradio/radio@0.3.2 + - @gradio/simpledropdown@0.1.1 + - @gradio/simpletextbox@0.1.1 + - @gradio/slider@0.2.1 + - @gradio/statustracker@0.3.1 + - @gradio/textbox@0.4.1 + - @gradio/row@0.1.0 + +## 1.10.1 + +### Patch Changes + +- Updated dependencies [[`92278729e`](https://github.com/gradio-app/gradio/commit/92278729ee008126af15ffe6be399236211b2f34), [`e8216be94`](https://github.com/gradio-app/gradio/commit/e8216be948f76ce064595183d11e9148badf9421)]: + - @gradio/gallery@0.4.3 + - @gradio/dataframe@0.3.3 + +## 1.10.0 + +### Features + +- [#6261](https://github.com/gradio-app/gradio/pull/6261) [`8bbeca0e7`](https://github.com/gradio-app/gradio/commit/8bbeca0e772a5a2853d02a058b35abb2c15ffaf1) - Improve Embed and CDN handling and fix a couple of related bugs. Thanks [@pngwn](https://github.com/pngwn)! + +### Fixes + +- [#6266](https://github.com/gradio-app/gradio/pull/6266) [`e32bac894`](https://github.com/gradio-app/gradio/commit/e32bac8944c85e0ec4831963299889d6bbfa0351) - Fix updating interactive prop. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#6213](https://github.com/gradio-app/gradio/pull/6213) [`27194a987`](https://github.com/gradio-app/gradio/commit/27194a987fa7ba1234b5fc0ce8bf7fabef7033a9) - Ensure the statustracker for `gr.Image` displays in static mode. Thanks [@pngwn](https://github.com/pngwn)! +- [#6234](https://github.com/gradio-app/gradio/pull/6234) [`aaa55ce85`](https://github.com/gradio-app/gradio/commit/aaa55ce85e12f95aba9299445e9c5e59824da18e) - Video/Audio fixes. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6236](https://github.com/gradio-app/gradio/pull/6236) [`6bce259c5`](https://github.com/gradio-app/gradio/commit/6bce259c5db7b21b327c2067e74ea20417bc89ec) - Ensure `gr.CheckboxGroup` updates as expected. Thanks [@pngwn](https://github.com/pngwn)! + +## 1.9.2 + +### Fixes + +- [#6191](https://github.com/gradio-app/gradio/pull/6191) [`b555bc09f`](https://github.com/gradio-app/gradio/commit/b555bc09ffe8e58b10da6227e2f11a0c084aa71d) - fix cdn build. Thanks [@pngwn](https://github.com/pngwn)! + +## 1.9.1 + +### Features + +- [#6137](https://github.com/gradio-app/gradio/pull/6137) [`2ba14b284`](https://github.com/gradio-app/gradio/commit/2ba14b284f908aa13859f4337167a157075a68eb) - JS Param. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! + +## 1.9.0 + +### Features + +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Improve Audio Component. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Adds the ability to build the frontend and backend of custom components in preparation for publishing to pypi using `gradio_component build`. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Improve Video Component. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Custom components. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Swap websockets for SSE. Thanks [@pngwn](https://github.com/pngwn)! + +### Fixes + +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Pending events behavior. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Reinstate types that were removed in error in #5832. Thanks [@pngwn](https://github.com/pngwn)! + +## 1.9.0-beta.3 + +### Features + +- [#6124](https://github.com/gradio-app/gradio/pull/6124) [`a7435ba9e`](https://github.com/gradio-app/gradio/commit/a7435ba9e6f8b88a838e80893eb8fedf60ccda67) - Fix static issues with Lite on v4. Thanks [@aliabd](https://github.com/aliabd)! +- [#6136](https://github.com/gradio-app/gradio/pull/6136) [`667802a6c`](https://github.com/gradio-app/gradio/commit/667802a6cdbfb2ce454a3be5a78e0990b194548a) - JS Component Documentation. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6149](https://github.com/gradio-app/gradio/pull/6149) [`90318b1dd`](https://github.com/gradio-app/gradio/commit/90318b1dd118ae08a695a50e7c556226234ab6dc) - swap `mode` on the frontned to `interactive` to match the backend. Thanks [@pngwn](https://github.com/pngwn)! +- [#6118](https://github.com/gradio-app/gradio/pull/6118) [`88bccfdba`](https://github.com/gradio-app/gradio/commit/88bccfdba3df2df4b2747ea5d649ed528047cf50) - Improve Video Component. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#6126](https://github.com/gradio-app/gradio/pull/6126) [`865a22d5c`](https://github.com/gradio-app/gradio/commit/865a22d5c60fd97aeca968e55580b403743a23ec) - Refactor `Blocks.load()` so that it is in the same style as the other listeners. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#6157](https://github.com/gradio-app/gradio/pull/6157) [`db143bdd1`](https://github.com/gradio-app/gradio/commit/db143bdd13b830f3bfd513bbfbc0cd1403522b84) - Make output components not editable if they are being updated. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! +- [#6069](https://github.com/gradio-app/gradio/pull/6069) [`bf127e124`](https://github.com/gradio-app/gradio/commit/bf127e1241a41401e144874ea468dff8474eb505) - Swap websockets for SSE. Thanks [@aliabid94](https://github.com/aliabid94)! + +## 1.9.0-beta.2 + +### Features + +- [#6016](https://github.com/gradio-app/gradio/pull/6016) [`83e947676`](https://github.com/gradio-app/gradio/commit/83e947676d327ca2ab6ae2a2d710c78961c771a0) - Format js in v4 branch. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5966](https://github.com/gradio-app/gradio/pull/5966) [`9cad2127b`](https://github.com/gradio-app/gradio/commit/9cad2127b965023687470b3abfe620e188a9da6e) - Improve Audio Component. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5955](https://github.com/gradio-app/gradio/pull/5955) [`825c9cddc`](https://github.com/gradio-app/gradio/commit/825c9cddc83a09457d8c85ebeecb4bc705572d82) - Fix dev mode model3D. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6107](https://github.com/gradio-app/gradio/pull/6107) [`9a40de7bf`](https://github.com/gradio-app/gradio/commit/9a40de7bff5844c8a135e73c7d175eb02b63a966) - Fix: Move to cache in init postprocess + Fallback Fixes. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6089](https://github.com/gradio-app/gradio/pull/6089) [`cd8146ba0`](https://github.com/gradio-app/gradio/commit/cd8146ba053fbcb56cf5052e658e4570d457fb8a) - Update logos for v4. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5996](https://github.com/gradio-app/gradio/pull/5996) [`9cf40f76f`](https://github.com/gradio-app/gradio/commit/9cf40f76fed1c0f84b5a5336a9b0100f8a9b4ee3) - V4: Simple dropdown. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5990](https://github.com/gradio-app/gradio/pull/5990) [`85056de5c`](https://github.com/gradio-app/gradio/commit/85056de5cd4e90a10cbfcefab74037dbc622b26b) - V4: Simple textbox. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Fixes + +- [#6065](https://github.com/gradio-app/gradio/pull/6065) [`7d07001e8`](https://github.com/gradio-app/gradio/commit/7d07001e8e7ca9cbd2251632667b3a043de49f49) - fix storybook. Thanks [@pngwn](https://github.com/pngwn)! +- [#5826](https://github.com/gradio-app/gradio/pull/5826) [`ce036c5d4`](https://github.com/gradio-app/gradio/commit/ce036c5d47e741e29812654bcc641ea6be876504) - Pending events behavior. Thanks [@dawoodkhan82](https://github.com/dawoodkhan82)! +- [#6046](https://github.com/gradio-app/gradio/pull/6046) [`dbb7de5e0`](https://github.com/gradio-app/gradio/commit/dbb7de5e02c53fee05889d696d764d212cb96c74) - fix tests. Thanks [@pngwn](https://github.com/pngwn)! +- [#6076](https://github.com/gradio-app/gradio/pull/6076) [`f3f98f923`](https://github.com/gradio-app/gradio/commit/f3f98f923c9db506284b8440e18a3ac7ddd8398b) - Lite error handler. Thanks [@whitphx](https://github.com/whitphx)! + +## 1.9.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`174b73619`](https://github.com/gradio-app/gradio/commit/174b736194756e23f51bbaf6f850bac5f1ca95b5), [`5fbda0bd2`](https://github.com/gradio-app/gradio/commit/5fbda0bd2b2bbb2282249b8875d54acf87cd7e84)]: + - @gradio/wasm@0.2.0-beta.1 + - @gradio/audio@0.4.0-beta.7 + - @gradio/image@0.3.0-beta.7 + - @gradio/video@0.1.0-beta.7 + - @gradio/gallery@0.4.0-beta.7 + - @gradio/row@0.1.0-beta.1 + +## 1.9.0-beta.0 + +### Features + +- [#5960](https://github.com/gradio-app/gradio/pull/5960) [`319c30f3f`](https://github.com/gradio-app/gradio/commit/319c30f3fccf23bfe1da6c9b132a6a99d59652f7) - rererefactor frontend files. Thanks [@pngwn](https://github.com/pngwn)! +- [#5956](https://github.com/gradio-app/gradio/pull/5956) [`f769876e0`](https://github.com/gradio-app/gradio/commit/f769876e0fa62336425c4e8ada5e09f38353ff01) - Apply formatter (and small refactoring) to the Lite-related frontend code. Thanks [@whitphx](https://github.com/whitphx)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`85ba6de13`](https://github.com/gradio-app/gradio/commit/85ba6de136a45b3e92c74e410bb27e3cbe7138d7) - Adds the ability to build the frontend and backend of custom components in preparation for publishing to pypi using `gradio_component build`. Thanks [@pngwn](https://github.com/pngwn)! + +### Fixes + +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`85ba6de13`](https://github.com/gradio-app/gradio/commit/85ba6de136a45b3e92c74e410bb27e3cbe7138d7) - Reinstate types that were removed in error in #5832. Thanks [@pngwn](https://github.com/pngwn)! + +## 1.8.0 + +### Features + +- [#5627](https://github.com/gradio-app/gradio/pull/5627) [`b67115e8e`](https://github.com/gradio-app/gradio/commit/b67115e8e6e489fffd5271ea830211863241ddc5) - Lite: Make the Examples component display media files using pseudo HTTP requests to the Wasm server. Thanks [@whitphx](https://github.com/whitphx)! +- [#5886](https://github.com/gradio-app/gradio/pull/5886) [`121f25b2d`](https://github.com/gradio-app/gradio/commit/121f25b2d50a33e1e06721b79e20b4f5651987ba) - Lite: Fix is_self_host() to detect `127.0.0.1` as localhost as well. Thanks [@whitphx](https://github.com/whitphx)! + +## 1.7.1 + +### Patch Changes + +- Updated dependencies [[`796145e2c`](https://github.com/gradio-app/gradio/commit/796145e2c48c4087bec17f8ec0be4ceee47170cb)]: + - @gradio/client@0.5.1 + - @gradio/file@0.2.1 + - @gradio/fileexplorer@0.2.1 + - @gradio/uploadbutton@0.0.11 + +## 1.7.0 + +### Highlights + +#### new `FileExplorer` component ([#5672](https://github.com/gradio-app/gradio/pull/5672) [`e4a307ed6`](https://github.com/gradio-app/gradio/commit/e4a307ed6cde3bbdf4ff2f17655739addeec941e)) + +Thanks to a new capability that allows components to communicate directly with the server _without_ passing data via the value, we have created a new `FileExplorer` component. + +This component allows you to populate the explorer by passing a glob, but only provides the selected file(s) in your prediction function. + +Users can then navigate the virtual filesystem and select files which will be accessible in your predict function. This component will allow developers to build more complex spaces, with more flexible input options. + +![output](https://github.com/pngwn/MDsveX/assets/12937446/ef108f0b-0e84-4292-9984-9dc66b3e144d) + +For more information check the [`FileExplorer` documentation](https://gradio.app/docs/fileexplorer). + +Thanks [@aliabid94](https://github.com/aliabid94)! + +### Fixes + +- [#5794](https://github.com/gradio-app/gradio/pull/5794) [`f096c3ae1`](https://github.com/gradio-app/gradio/commit/f096c3ae168c0df00f90fe131c1e48c572e0574b) - Throw helpful error when media devices are not found. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 1.6.4 + +### Features + +- [#5124](https://github.com/gradio-app/gradio/pull/5124) [`6e56a0d9b`](https://github.com/gradio-app/gradio/commit/6e56a0d9b0c863e76c69e1183d9d40196922b4cd) - Lite: Websocket queueing. Thanks [@whitphx](https://github.com/whitphx)! + +## 1.6.3 + +### Patch Changes + +- Updated dependencies [[`abb5e9df4`](https://github.com/gradio-app/gradio/commit/abb5e9df47989b2c56c2c312d74944678f9f2d4e), [`e842a561a`](https://github.com/gradio-app/gradio/commit/e842a561af4394f8109291ee5725bcf74743e816), [`8f0fed857`](https://github.com/gradio-app/gradio/commit/8f0fed857d156830626eb48b469d54d211a582d2), [`502054848`](https://github.com/gradio-app/gradio/commit/502054848fdbe39fc03ec42445242b4e49b7affc), [`2a5b9e03b`](https://github.com/gradio-app/gradio/commit/2a5b9e03b15ea324d641fe6982f26d81b1ca7210)]: + - @gradio/gallery@0.4.1 + - @gradio/chatbot@0.5.0 + - @gradio/dataframe@0.3.0 + - @gradio/markdown@0.3.0 + - @gradio/icons@0.2.0 + - @gradio/annotatedimage@0.2.1 + - @gradio/atoms@0.1.3 + - @gradio/audio@0.3.6 + - @gradio/code@0.2.1 + - @gradio/dropdown@0.3.1 + - @gradio/file@0.1.5 + - @gradio/form@0.0.6 + - @gradio/highlightedtext@0.3.2 + - @gradio/image@0.3.1 + - @gradio/json@0.1.1 + - @gradio/label@0.2.1 + - @gradio/model3d@0.2.3 + - @gradio/plot@0.2.1 + - @gradio/statustracker@0.2.1 + - @gradio/textbox@0.4.1 + - @gradio/timeseries@0.0.7 + - @gradio/upload@0.3.1 + - @gradio/video@0.0.10 + - @gradio/accordion@0.1.1 + - @gradio/box@0.0.5 + - @gradio/checkbox@0.2.1 + - @gradio/checkboxgroup@0.3.1 + - @gradio/colorpicker@0.1.3 + - @gradio/html@0.0.5 + - @gradio/number@0.3.1 + - @gradio/radio@0.3.1 + - @gradio/slider@0.2.1 + - @gradio/row@0.0.1 + - @gradio/button@0.2.1 + - @gradio/uploadbutton@0.0.8 + +## 1.6.2 + +### Features + +- [#5721](https://github.com/gradio-app/gradio/pull/5721) [`84e03fe50`](https://github.com/gradio-app/gradio/commit/84e03fe506e08f1f81bac6d504c9fba7924f2d93) - Adds copy buttons to website, and better descriptions to API Docs. Thanks [@aliabd](https://github.com/aliabd)! + +### Fixes + +- [#5705](https://github.com/gradio-app/gradio/pull/5705) [`78e7cf516`](https://github.com/gradio-app/gradio/commit/78e7cf5163e8d205e8999428fce4c02dbdece25f) - ensure internal data has updated before dispatching `success` or `then` events. Thanks [@pngwn](https://github.com/pngwn)! +- [#5726](https://github.com/gradio-app/gradio/pull/5726) [`96c4b97c7`](https://github.com/gradio-app/gradio/commit/96c4b97c742311e90a87d8e8ee562c6ad765e9f0) - Adjust translation. Thanks [@ylhsieh](https://github.com/ylhsieh)! + +## 1.6.1 + +### Patch Changes + +- Updated dependencies [[`ee8eec1e5`](https://github.com/gradio-app/gradio/commit/ee8eec1e5e544a0127e0aa68c2522a7085b8ada5)]: + - @gradio/markdown@0.2.2 + - @gradio/chatbot@0.4.1 + - @gradio/dataframe@0.2.4 + +## 1.6.0 + +### Features + +- [#5639](https://github.com/gradio-app/gradio/pull/5639) [`e1874aff8`](https://github.com/gradio-app/gradio/commit/e1874aff814d13b23f3e59ef239cc13e18ad3fa7) - Add `gr.on` listener method. Thanks [@aliabid94](https://github.com/aliabid94)! +- [#5554](https://github.com/gradio-app/gradio/pull/5554) [`75ddeb390`](https://github.com/gradio-app/gradio/commit/75ddeb390d665d4484667390a97442081b49a423) - Accessibility Improvements. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 1.5.4 + +### Features + +- [#5514](https://github.com/gradio-app/gradio/pull/5514) [`52f783175`](https://github.com/gradio-app/gradio/commit/52f7831751b432411e109bd41add4ab286023a8e) - refactor: Use package.json for version management. Thanks [@DarhkVoyd](https://github.com/DarhkVoyd)! + +## 1.5.3 + +### Fixes + +- [#5562](https://github.com/gradio-app/gradio/pull/5562) [`50d9747d0`](https://github.com/gradio-app/gradio/commit/50d9747d061962cff7f60a8da648bb3781794102) - chore(deps): update dependency iframe-resizer to v4.3.7. Thanks [@renovate](https://github.com/apps/renovate)! +- [#5550](https://github.com/gradio-app/gradio/pull/5550) [`4ed5902e7`](https://github.com/gradio-app/gradio/commit/4ed5902e7dda2d95cd43e4ccaaef520ddd8eba57) - Adding basque language. Thanks [@EkhiAzur](https://github.com/EkhiAzur)! + +## 1.5.2 + +### Patch Changes + +- Updated dependencies [[`a0cc9ac9`](https://github.com/gradio-app/gradio/commit/a0cc9ac931554e06dcb091158c9b9ac0cc580b6c)]: + - @gradio/dropdown@0.2.2 + +## 1.5.1 + +### Patch Changes + +- Updated dependencies [[`dc86e4a7`](https://github.com/gradio-app/gradio/commit/dc86e4a7e1c40b910c74558e6f88fddf9b3292bc), [`21f1db40`](https://github.com/gradio-app/gradio/commit/21f1db40de6d1717eba97a550e11422a457ba7e9)]: + - @gradio/gallery@0.3.3 + - @gradio/image@0.2.3 + - @gradio/dropdown@0.2.1 + - @gradio/row@0.0.1 + - @gradio/video@0.0.7 + +## 1.5.0 + +### Features + +- [#5505](https://github.com/gradio-app/gradio/pull/5505) [`9ee20f49`](https://github.com/gradio-app/gradio/commit/9ee20f499f62c1fe5af6b8f84918b3a334eb1c8d) - Validate i18n file names with ISO-639x. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5475](https://github.com/gradio-app/gradio/pull/5475) [`c60b89b0`](https://github.com/gradio-app/gradio/commit/c60b89b0a54758a27277f0a6aa20d0653647c7c8) - Adding Central Kurdish. Thanks [@Hrazhan](https://github.com/Hrazhan)! +- [#5400](https://github.com/gradio-app/gradio/pull/5400) [`d112e261`](https://github.com/gradio-app/gradio/commit/d112e2611b0fc79ecedfaed367571f3157211387) - Allow interactive input in `gr.HighlightedText`. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 1.4.3 + +### Patch Changes + +- Updated dependencies [[`6e381c4f`](https://github.com/gradio-app/gradio/commit/6e381c4f146cc8177a4e2b8e39f914f09cd7ff0c)]: + - @gradio/dataframe@0.2.2 + +## 1.4.2 + +### Fixes + +- [#5447](https://github.com/gradio-app/gradio/pull/5447) [`7a4a89e5`](https://github.com/gradio-app/gradio/commit/7a4a89e5ca1dedb39e5366867501584b0c636bbb) - ensure iframe is correct size on spaces. Thanks [@pngwn](https://github.com/pngwn)! + +## 1.4.1 + +### Patch Changes + +- Updated dependencies [[`afac0006`](https://github.com/gradio-app/gradio/commit/afac0006337ce2840cf497cd65691f2f60ee5912), [`d14d63e3`](https://github.com/gradio-app/gradio/commit/d14d63e30c4af3f9c2a664fd11b0a01943a8300c), [`26fef8c7`](https://github.com/gradio-app/gradio/commit/26fef8c7f85a006c7e25cdbed1792df19c512d02)]: + - @gradio/dataframe@0.2.0 + - @gradio/markdown@0.2.0 + - @gradio/statustracker@0.2.0 + - @gradio/theme@0.1.0 + - @gradio/textbox@0.2.0 + - @gradio/client@0.3.1 + - @gradio/chatbot@0.3.1 + - @gradio/accordion@0.0.4 + - @gradio/annotatedimage@0.1.2 + - @gradio/audio@0.3.2 + - @gradio/checkbox@0.1.3 + - @gradio/checkboxgroup@0.1.2 + - @gradio/code@0.1.2 + - @gradio/colorpicker@0.1.2 + - @gradio/dropdown@0.1.3 + - @gradio/file@0.1.2 + - @gradio/gallery@0.3.2 + - @gradio/highlightedtext@0.2.3 + - @gradio/html@0.0.4 + - @gradio/image@0.2.2 + - @gradio/json@0.0.5 + - @gradio/label@0.1.2 + - @gradio/model3d@0.2.1 + - @gradio/number@0.2.2 + - @gradio/plot@0.1.2 + - @gradio/radio@0.1.2 + - @gradio/slider@0.1.2 + - @gradio/timeseries@0.0.5 + - @gradio/video@0.0.6 + - @gradio/utils@0.1.1 + - @gradio/uploadbutton@0.0.5 + - @gradio/row@0.0.1 + - @gradio/atoms@0.1.2 + - @gradio/button@0.1.3 + - @gradio/form@0.0.5 + - @gradio/tabitem@0.0.4 + - @gradio/tabs@0.0.5 + - @gradio/box@0.0.4 + - @gradio/upload@0.2.1 + +## 1.4.0 + +### Features + +- [#5267](https://github.com/gradio-app/gradio/pull/5267) [`119c8343`](https://github.com/gradio-app/gradio/commit/119c834331bfae60d4742c8f20e9cdecdd67e8c2) - Faster reload mode. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5373](https://github.com/gradio-app/gradio/pull/5373) [`79d8f9d8`](https://github.com/gradio-app/gradio/commit/79d8f9d891901683c5a1b7486efb44eab2478c96) - Adds `height` and `zoom_speed` parameters to `Model3D` component, as well as a button to reset the camera position. Thanks [@abidlabs](https://github.com/abidlabs)! + +## 1.3.2 + +### Patch Changes + +- Updated dependencies [[`5f25eb68`](https://github.com/gradio-app/gradio/commit/5f25eb6836f6a78ce6208b53495a01e1fc1a1d2f), [`3341148c`](https://github.com/gradio-app/gradio/commit/3341148c109b5458cc88435d27eb154210efc472), [`df090e89`](https://github.com/gradio-app/gradio/commit/df090e89f74a16e4cb2b700a1e3263cabd2bdd91)]: + - @gradio/highlightedtext@0.2.1 + - @gradio/chatbot@0.2.2 + - @gradio/checkbox@0.1.1 + +## 1.3.1 + +### Fixes + +- [#5324](https://github.com/gradio-app/gradio/pull/5324) [`31996c99`](https://github.com/gradio-app/gradio/commit/31996c991d6bfca8cef975eb8e3c9f61a7aced19) - ensure login form has correct styles. Thanks [@pngwn](https://github.com/pngwn)! + +## 1.3.0 + +### Highlights + +#### Improve startup performance and markdown support ([#5279](https://github.com/gradio-app/gradio/pull/5279) [`fe057300`](https://github.com/gradio-app/gradio/commit/fe057300f0672c62dab9d9b4501054ac5d45a4ec)) + +##### Improved markdown support + +We now have better support for markdown in `gr.Markdown` and `gr.Dataframe`. Including syntax highlighting and Github Flavoured Markdown. We also have more consistent markdown behaviour and styling. + +##### Various performance improvements + +These improvements will be particularly beneficial to large applications. + +- Rather than attaching events manually, they are now delegated, leading to a significant performance improvement and addressing a performance regression introduced in a recent version of Gradio. App startup for large applications is now around twice as fast. +- Optimised the mounting of individual components, leading to a modest performance improvement during startup (~30%). +- Corrected an issue that was causing markdown to re-render infinitely. +- Ensured that the `gr.3DModel` does re-render prematurely. + +Thanks [@pngwn](https://github.com/pngwn)! + +#### Add `render` function to `` ([#5158](https://github.com/gradio-app/gradio/pull/5158) [`804fcc05`](https://github.com/gradio-app/gradio/commit/804fcc058e147f283ece67f1f353874e26235535)) + +We now have an event `render` on the web component, which is triggered once the embedded space has finished rendering. + +```html + +``` + +Thanks [@hannahblair](https://github.com/hannahblair)! + +### Features + +- [#5215](https://github.com/gradio-app/gradio/pull/5215) [`fbdad78a`](https://github.com/gradio-app/gradio/commit/fbdad78af4c47454cbb570f88cc14bf4479bbceb) - Lazy load interactive or static variants of a component individually, rather than loading both variants regardless. This change will improve performance for many applications. Thanks [@pngwn](https://github.com/pngwn)! +- [#5216](https://github.com/gradio-app/gradio/pull/5216) [`4b58ea6d`](https://github.com/gradio-app/gradio/commit/4b58ea6d98e7a43b3f30d8a4cb6f379bc2eca6a8) - Update i18n tokens and locale files. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5219](https://github.com/gradio-app/gradio/pull/5219) [`e8fd4e4e`](https://github.com/gradio-app/gradio/commit/e8fd4e4ec68a6c974bc8c84b61f4a0ec50a85bc6) - Add `api_name` parameter to `gr.Interface`. Additionally, completely hide api page if show_api=False. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#5264](https://github.com/gradio-app/gradio/pull/5264) [`46a2b600`](https://github.com/gradio-app/gradio/commit/46a2b600a7ff030a9ea1560b882b3bf3ad266bbc) - ensure translations for audio work correctly. Thanks [@hannahblair](https://github.com/hannahblair)! + +### Fixes + +- [#5285](https://github.com/gradio-app/gradio/pull/5285) [`cdfd4217`](https://github.com/gradio-app/gradio/commit/cdfd42174a9c777eaee9c1209bf8e90d8c7791f2) - Tweaks to `icon` parameter in `gr.Button()`. Thanks [@abidlabs](https://github.com/abidlabs)! +- [#5312](https://github.com/gradio-app/gradio/pull/5312) [`f769cb67`](https://github.com/gradio-app/gradio/commit/f769cb67149d8e209091508f06d87014acaed965) - only start listening for events after the components are mounted. Thanks [@pngwn](https://github.com/pngwn)! +- [#5276](https://github.com/gradio-app/gradio/pull/5276) [`502f1015`](https://github.com/gradio-app/gradio/commit/502f1015bf23b365bc32446dd2e549b0c5d0dc72) - Ensure `Blocks` translation copy renders correctly. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 1.2.0 + +### Highlights + +#### Client.predict will now return the final output for streaming endpoints ([#5057](https://github.com/gradio-app/gradio/pull/5057) [`35856f8b`](https://github.com/gradio-app/gradio/commit/35856f8b54548cae7bd3b8d6a4de69e1748283b2)) + +### This is a breaking change (for gradio_client only)! + +Previously, `Client.predict` would only return the first output of an endpoint that streamed results. This was causing confusion for developers that wanted to call these streaming demos via the client. + +We realize that developers using the client don't know the internals of whether a demo streams or not, so we're changing the behavior of predict to match developer expectations. + +Using `Client.predict` will now return the final output of a streaming endpoint. This will make it even easier to use gradio apps via the client. + +Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Features + +- [#5025](https://github.com/gradio-app/gradio/pull/5025) [`6693660a`](https://github.com/gradio-app/gradio/commit/6693660a790996f8f481feaf22a8c49130d52d89) - Add download button to selected images in `Gallery`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5046](https://github.com/gradio-app/gradio/pull/5046) [`5244c587`](https://github.com/gradio-app/gradio/commit/5244c5873c355cf3e2f0acb7d67fda3177ef8b0b) - Allow new lines in `HighlightedText` with `/n` and preserve whitespace. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5047](https://github.com/gradio-app/gradio/pull/5047) [`883ac364`](https://github.com/gradio-app/gradio/commit/883ac364f69d92128774ac446ce49bdf8415fd7b) - Add `step` param to `Number`. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#5005](https://github.com/gradio-app/gradio/pull/5005) [`f5539c76`](https://github.com/gradio-app/gradio/commit/f5539c7618e31451420bd3228754774da14dc65f) - Enhancement: Add focus event to textbox and number component. Thanks [@JodyZ0203](https://github.com/JodyZ0203)! +- [#5136](https://github.com/gradio-app/gradio/pull/5136) [`eaa1ce14`](https://github.com/gradio-app/gradio/commit/eaa1ce14ac41de1c23321e93f11f1b03a2f3c7f4) - Enhancing Tamil Translation: Language Refinement 🌟. Thanks [@sanjaiyan-dev](https://github.com/sanjaiyan-dev)! + +## 1.1.0 + +### Features + +- [#4995](https://github.com/gradio-app/gradio/pull/4995) [`3f8c210b`](https://github.com/gradio-app/gradio/commit/3f8c210b01ef1ceaaf8ee73be4bf246b5b745bbf) - Implement left and right click in `Gallery` component and show implicit images in `Gallery` grid. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#4993](https://github.com/gradio-app/gradio/pull/4993) [`dc07a9f9`](https://github.com/gradio-app/gradio/commit/dc07a9f947de44b419d8384987a02dcf94977851) - Bringing back the "Add download button for audio" PR by [@leuryr](https://github.com/leuryr). Thanks [@abidlabs](https://github.com/abidlabs)! +- [#4979](https://github.com/gradio-app/gradio/pull/4979) [`44ac8ad0`](https://github.com/gradio-app/gradio/commit/44ac8ad08d82ea12c503dde5c78f999eb0452de2) - Allow setting sketch color default. Thanks [@aliabid94](https://github.com/aliabid94)! \ No newline at end of file diff --git a/js/app/README.md b/js/app/README.md new file mode 100644 index 0000000..5ce6766 --- /dev/null +++ b/js/app/README.md @@ -0,0 +1,38 @@ +# create-svelte + +Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npm create svelte@latest + +# create a new project in my-app +npm create svelte@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/js/app/after_build.js b/js/app/after_build.js new file mode 100644 index 0000000..a9a52db --- /dev/null +++ b/js/app/after_build.js @@ -0,0 +1,34 @@ +import { writeFileSync, copyFileSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; +import { execSync } from "child_process"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const out_path = resolve(__dirname, "../../gradio/templates/node/build"); + +writeFileSync( + resolve(out_path, "package.json"), + JSON.stringify( + { + name: "gradio-server-app", + type: "module", + dependencies: { "http-proxy": "^1.18.1" } + }, + null, + 2 + ) +); + +// Install http-proxy in the build output so it's available at runtime +execSync("npm install --production", { cwd: out_path, stdio: "inherit" }); + +// Replace the adapter-generated index.js with our custom proxy entry point +copyFileSync( + resolve(__dirname, "proxy_index.js"), + resolve(out_path, "index.js") +); +copyFileSync( + resolve(__dirname, "proxy_routes.js"), + resolve(out_path, "proxy_routes.js") +); diff --git a/js/app/cleanup_build.js b/js/app/cleanup_build.js new file mode 100644 index 0000000..68da2a7 --- /dev/null +++ b/js/app/cleanup_build.js @@ -0,0 +1,83 @@ +import { readdirSync, unlinkSync, statSync } from "fs"; +import { join, resolve } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const buildPath = resolve(__dirname, "../../gradio/templates/node/build"); + +/** + * Recursively walks a directory and calls the callback for each file + */ +function walkDir(dir, callback) { + const files = readdirSync(dir); + + for (const file of files) { + const filePath = join(dir, file); + const stat = statSync(filePath); + + if (stat.isDirectory()) { + walkDir(filePath, callback); + } else { + callback(filePath); + } + } +} + +/** + * Cleanup build directory to keep only .br files + * - Remove all .gz files + * - Remove all .map files + * - Remove all .js and .css files that have a corresponding .br file + */ +function cleanupBuild() { + console.log("Cleaning up build directory:", buildPath); + + let removedGz = 0; + let removedMap = 0; + let removedOriginal = 0; + + // First pass: collect all .br files + const brFiles = new Set(); + walkDir(buildPath, (filePath) => { + if (filePath.endsWith(".br")) { + // Store the original file path (without .br extension) + brFiles.add(filePath.slice(0, -3)); + } + }); + + // Second pass: remove files + walkDir(buildPath, (filePath) => { + // Remove .gz files + if (filePath.endsWith(".gz")) { + unlinkSync(filePath); + removedGz++; + return; + } + + // Remove .map files + if (filePath.endsWith(".map")) { + unlinkSync(filePath); + removedMap++; + return; + } + + // Remove original .js and .css files if they have a .br version + if ( + (filePath.endsWith(".js") || filePath.endsWith(".css")) && + brFiles.has(filePath) + ) { + unlinkSync(filePath); + removedOriginal++; + return; + } + }); + + console.log(`Cleanup complete:`); + console.log(` - Removed ${removedGz} .gz files`); + console.log(` - Removed ${removedMap} .map files`); + console.log( + ` - Removed ${removedOriginal} original files (kept .br versions)` + ); +} + +cleanupBuild(); diff --git a/js/app/package.json b/js/app/package.json new file mode 100644 index 0000000..ddcc72b --- /dev/null +++ b/js/app/package.json @@ -0,0 +1,35 @@ +{ + "name": "@self/app", + "version": "2.2.2", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build && node after_build.js && node cleanup_build.js", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check .", + "format": "prettier --write ." + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "^2.49.5", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "prettier": "^3.6.2", + "prettier-plugin-svelte": "^3.4.0", + "svelte-check": "^4.3.3", + "typescript": "^5.9.3", + "vite": "^7.1.12" + }, + "type": "module", + "dependencies": { + "@gradio/client": "workspace:^", + "@gradio/core": "workspace:^", + "@gradio/theme": "workspace:^", + "@huggingface/space-header": "^1.0.4", + "@self/build": "workspace:^", + "@sveltejs/adapter-node": "^5.4.0", + "http-proxy": "^1.18.1", + "svelte-preprocess": "^6.0.3" + } +} diff --git a/js/app/proxy_index.js b/js/app/proxy_index.js new file mode 100644 index 0000000..c692567 --- /dev/null +++ b/js/app/proxy_index.js @@ -0,0 +1,128 @@ +import http from "node:http"; +import process from "node:process"; +import httpProxy from "http-proxy"; +import { handler } from "./handler.js"; +import { classifyRoute } from "./proxy_routes.js"; + +const host = process.env.HOST || "0.0.0.0"; +const port = parseInt(process.env.PORT || "7860", 10); + +const pythonHost = process.env.GRADIO_PYTHON_HOST || "127.0.0.1"; +const pythonPort = parseInt(process.env.GRADIO_PYTHON_PORT || "7861", 10); +const serverModeEnabled = process.env.GRADIO_SERVER_MODE_ENABLED; + +const staticWorkerPorts = process.env.GRADIO_STATIC_WORKER_PORTS + ? process.env.GRADIO_STATIC_WORKER_PORTS.split(",") + .map((p) => parseInt(p.trim(), 10)) + .filter((p) => !isNaN(p)) + : []; + +let workerIndex = 0; + +const pythonTarget = `http://${pythonHost}:${pythonPort}`; + +const proxy = httpProxy.createProxyServer({ + // Don't modify the path + ignorePath: false, + // Forward the original host header + changeOrigin: false +}); + +proxy.on("error", (err, req, res) => { + // Suppress ECONNRESET from client-initiated disconnects + if (err.code === "ECONNRESET") return; + console.error(`[gradio-proxy] Proxy error for ${req.url}:`, err.message); + if (res.writeHead && !res.headersSent) { + res.writeHead(502, { "Content-Type": "text/plain" }); + res.end("Bad Gateway"); + } +}); + +// When the browser closes an SSE/streaming connection, propagate the +// disconnect to Python so that `request.is_disconnected()` returns True. +// http-proxy only listens for the deprecated `aborted` event, which +// doesn't fire reliably in newer Node versions. We listen on `res` +// (the outgoing response to the client) because for GET/SSE requests +// `req` closes immediately after the empty body is consumed — before +// the proxy response even arrives. +proxy.on("proxyRes", (proxyRes, req, res) => { + res.on("close", () => { + if (!res.writableEnded) { + proxyRes.destroy(); + } + }); +}); + +const server = http.createServer((req, res) => { + const url = req.url || "/"; + const qmark = url.indexOf("?"); + const path = qmark === -1 ? url : url.substring(0, qmark); + const queryString = qmark === -1 ? "" : url.substring(qmark + 1); + const { route, workerIndex: affinityIndex } = classifyRoute(path, { + hasWorkers: staticWorkerPorts.length > 0, + serverModeEnabled: !!serverModeEnabled, + numWorkers: staticWorkerPorts.length, + queryString + }); + + if (route === "worker") { + let targetPort; + if (affinityIndex !== undefined) { + // Affinity routing: upload_id hashed to a specific worker + targetPort = staticWorkerPorts[affinityIndex]; + } else { + // Round-robin for non-affinity static routes + targetPort = staticWorkerPorts[workerIndex % staticWorkerPorts.length]; + workerIndex = (workerIndex + 1) % staticWorkerPorts.length; + } + proxy.web(req, res, { + target: `http://${pythonHost}:${targetPort}` + }); + return; + } + + if (route === "python") { + proxy.web(req, res, { target: pythonTarget }); + return; + } + + // SvelteKit handler (SSR + immutable assets) + // Inject headers that SvelteKit's page.server.ts expects to find the Python backend. + // x-gradio-server is for internal Node->Python fetches (always http). + // x-gradio-original-url is the public-facing URL the browser uses, + // so it must respect x-forwarded-proto (e.g. https on HF Spaces). + const publicScheme = (req.headers["x-forwarded-proto"] || "http") + .split(",")[0] + .trim(); + const publicHost = + req.headers["x-forwarded-host"] || req.headers.host || `${host}:${port}`; + req.headers["x-gradio-server"] = pythonTarget; + req.headers["x-gradio-port"] = String(pythonPort); + req.headers["x-gradio-mounted-path"] = "/"; + req.headers["x-gradio-original-url"] = `${publicScheme}://${publicHost}`; + handler(req, res); +}); + +server.listen({ host, port }, () => { + console.log(`[gradio-proxy] Listening on http://${host}:${port}`); + console.log(`[gradio-proxy] Python backend: ${pythonTarget}`); + if (staticWorkerPorts.length > 0) { + console.log( + `[gradio-proxy] Static workers: ${staticWorkerPorts.join(", ")}` + ); + } +}); + +function graceful_shutdown() { + server.closeIdleConnections(); + server.close(() => { + proxy.close(); + process.exit(0); + }); + setTimeout(() => server.closeAllConnections(), 30000); +} + +process.on("SIGTERM", graceful_shutdown); +process.on("SIGINT", graceful_shutdown); + +export { host, port, server }; diff --git a/js/app/proxy_routes.js b/js/app/proxy_routes.js new file mode 100644 index 0000000..e69f4c5 --- /dev/null +++ b/js/app/proxy_routes.js @@ -0,0 +1,133 @@ +/** + * Routing logic for the Gradio Node proxy. + * + * Extracted so it can be unit-tested independently of the SvelteKit handler. + */ + +// Routes that must go to the Python server (FastAPI). +export const PYTHON_ROUTE_PREFIXES = [ + "/gradio_api", + "/config", + "/login", + "/logout", + "/theme.css", + "/robots.txt", + "/pwa_icon", + "/manifest.json", + "/monitoring" +]; + +// Routes that can be offloaded to static workers. +// Workers serve both /upload and /gradio_api/upload (same handler). +// Checked BEFORE the /gradio_api Python catch-all. +export const STATIC_ROUTE_PREFIXES = [ + "/gradio_api/upload", + "/gradio_api/upload_progress", + "/gradio_api/file=", + "/gradio_api/file/", + "/upload", + "/upload_progress", + "/file=", + "/file/", + "/static/", + "/assets/", + "/svelte/", + "/favicon.ico", + "/custom_component/" +]; + +// Upload routes that need affinity (upload + upload_progress for the same +// upload_id must land on the same worker). +const UPLOAD_AFFINITY_PREFIXES = [ + "/gradio_api/upload", + "/gradio_api/upload_progress", + "/upload", + "/upload_progress" +]; + +export function matchesPrefix(path, prefixes) { + for (const prefix of prefixes) { + if (path === prefix || path.startsWith(prefix)) { + return true; + } + } + return false; +} + +/** + * Simple string hash (djb2) that returns a non-negative integer. + * Used to map upload_id to a deterministic worker index. + * + * @param {string} str + * @returns {number} + */ +export function hashString(str) { + let hash = 5381; + for (let i = 0; i < str.length; i++) { + hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0; + } + return hash; +} + +/** + * Classify a request into a routing destination and optional worker index. + * + * @param {string} path - URL path (no query string) + * @param {object} options + * @param {boolean} options.hasWorkers - whether static workers are configured + * @param {boolean} options.serverModeEnabled - whether GRADIO_SERVER_MODE_ENABLED is set + * @param {number} options.numWorkers - number of static workers + * @param {string} [options.queryString] - raw query string (after ?) + * @returns {{route: "worker" | "python" | "sveltekit", workerIndex?: number}} + */ +export function classifyRoute( + path, + { + hasWorkers = false, + serverModeEnabled = false, + numWorkers = 0, + queryString = "" + } = {} +) { + if (matchesPrefix(path, STATIC_ROUTE_PREFIXES)) { + if (!hasWorkers) { + return { route: "python" }; + } + // For upload routes with an upload_id, use affinity hashing + if ( + numWorkers > 0 && + queryString && + matchesPrefix(path, UPLOAD_AFFINITY_PREFIXES) + ) { + const uploadId = extractUploadId(queryString); + if (uploadId) { + return { + route: "worker", + workerIndex: hashString(uploadId) % numWorkers + }; + } + } + return { route: "worker" }; + } + if (matchesPrefix(path, PYTHON_ROUTE_PREFIXES) || serverModeEnabled) { + return { route: "python" }; + } + return { route: "sveltekit" }; +} + +/** + * Extract upload_id from a query string. + * + * @param {string} qs - raw query string (without leading ?) + * @returns {string|null} + */ +function extractUploadId(qs) { + for (const part of qs.split("&")) { + const eq = part.indexOf("="); + if (eq === -1) continue; + if (part.substring(0, eq) === "upload_id") { + return decodeURIComponent(part.substring(eq + 1)); + } + } + return null; +} diff --git a/js/app/proxy_routes.test.js b/js/app/proxy_routes.test.js new file mode 100644 index 0000000..0959fa9 --- /dev/null +++ b/js/app/proxy_routes.test.js @@ -0,0 +1,318 @@ +/** + * Unit tests for proxy routing logic. + * + * Tests the classifyRoute function which determines where each request goes: + * "worker", "python", or "sveltekit". + * + * Run: node --test js/app/proxy_routes.test.js + */ + +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + classifyRoute, + hashString, + matchesPrefix, + PYTHON_ROUTE_PREFIXES, + STATIC_ROUTE_PREFIXES +} from "./proxy_routes.js"; + +describe("classifyRoute", () => { + describe("python routes", () => { + const pythonPaths = [ + "/config", + "/config/", + "/gradio_api/info", + "/gradio_api/queue/join", + "/gradio_api/call/predict", + "/login", + "/login/", + "/logout", + "/theme.css", + "/robots.txt", + "/pwa_icon", + "/manifest.json", + "/monitoring", + "/monitoring/some/path" + ]; + + for (const path of pythonPaths) { + it(`${path} -> python`, () => { + assert.equal(classifyRoute(path, { hasWorkers: true }).route, "python"); + }); + } + }); + + describe("worker routes (workers configured)", () => { + const workerPaths = [ + "/gradio_api/upload", + "/gradio_api/file=/tmp/test.txt", + "/gradio_api/file/some/path", + "/upload", + "/file=/tmp/test.txt", + "/file/some/path", + "/static/img/logo.svg", + "/static/css/style.css", + "/assets/some.js", + "/svelte/some/file.js", + "/favicon.ico", + "/custom_component/abc" + ]; + + for (const path of workerPaths) { + it(`${path} -> worker`, () => { + assert.equal(classifyRoute(path, { hasWorkers: true }).route, "worker"); + }); + } + }); + + describe("worker routes fall back to python (no workers)", () => { + const paths = [ + "/gradio_api/upload", + "/upload", + "/file=/tmp/test.txt", + "/static/img/logo.svg", + "/favicon.ico" + ]; + + for (const path of paths) { + it(`${path} -> python (no workers)`, () => { + assert.equal( + classifyRoute(path, { hasWorkers: false }).route, + "python" + ); + }); + } + }); + + describe("sveltekit routes", () => { + const sveltekitPaths = [ + "/", + "/_app/immutable/nodes/1.CPQUYxzy.js", + "/_app/immutable/chunks/dgSRqNc-.js", + "/some/random/page" + ]; + + for (const path of sveltekitPaths) { + it(`${path} -> sveltekit`, () => { + assert.equal( + classifyRoute(path, { hasWorkers: true }).route, + "sveltekit" + ); + }); + } + }); + + describe("priority", () => { + it("/gradio_api/upload goes to worker, not python", () => { + assert.equal( + classifyRoute("/gradio_api/upload", { hasWorkers: true }).route, + "worker" + ); + }); + + it("/gradio_api/upload_progress goes to worker, not python", () => { + assert.equal( + classifyRoute("/gradio_api/upload_progress", { + hasWorkers: true + }).route, + "worker" + ); + }); + + it("/gradio_api/file=/path goes to worker, not python", () => { + assert.equal( + classifyRoute("/gradio_api/file=/path", { hasWorkers: true }).route, + "worker" + ); + }); + + it("/gradio_api/file/path goes to worker, not python", () => { + assert.equal( + classifyRoute("/gradio_api/file/path", { hasWorkers: true }).route, + "worker" + ); + }); + + it("/gradio_api/info still goes to python", () => { + assert.equal( + classifyRoute("/gradio_api/info", { hasWorkers: true }).route, + "python" + ); + }); + + it("/gradio_api/queue/join still goes to python", () => { + assert.equal( + classifyRoute("/gradio_api/queue/join", { hasWorkers: true }).route, + "python" + ); + }); + }); + + describe("server mode", () => { + it("/ -> python when serverModeEnabled", () => { + assert.equal( + classifyRoute("/", { serverModeEnabled: true }).route, + "python" + ); + }); + + it("/any/path -> python when serverModeEnabled", () => { + assert.equal( + classifyRoute("/any/path", { serverModeEnabled: true }).route, + "python" + ); + }); + + it("worker routes still go to workers even in server mode", () => { + assert.equal( + classifyRoute("/gradio_api/upload", { + hasWorkers: true, + serverModeEnabled: true + }).route, + "worker" + ); + }); + }); + + describe("upload affinity routing", () => { + const opts = { hasWorkers: true, numWorkers: 3 }; + + it("upload with upload_id gets a deterministic workerIndex", () => { + const result = classifyRoute("/gradio_api/upload", { + ...opts, + queryString: "upload_id=abc123" + }); + assert.equal(result.route, "worker"); + assert.equal(typeof result.workerIndex, "number"); + assert.ok(result.workerIndex >= 0 && result.workerIndex < 3); + }); + + it("upload_progress with same upload_id gets same workerIndex", () => { + const uploadResult = classifyRoute("/gradio_api/upload", { + ...opts, + queryString: "upload_id=abc123" + }); + const progressResult = classifyRoute("/gradio_api/upload_progress", { + ...opts, + queryString: "upload_id=abc123" + }); + assert.equal(uploadResult.workerIndex, progressResult.workerIndex); + }); + + it("/upload (no gradio_api prefix) also gets affinity", () => { + const result = classifyRoute("/upload", { + ...opts, + queryString: "upload_id=test456" + }); + assert.equal(result.route, "worker"); + assert.equal(typeof result.workerIndex, "number"); + }); + + it("/upload_progress (no gradio_api prefix) also gets affinity", () => { + const uploadResult = classifyRoute("/upload", { + ...opts, + queryString: "upload_id=test456" + }); + const progressResult = classifyRoute("/upload_progress", { + ...opts, + queryString: "upload_id=test456" + }); + assert.equal(uploadResult.workerIndex, progressResult.workerIndex); + }); + + it("upload without upload_id gets no workerIndex (round-robin)", () => { + const result = classifyRoute("/gradio_api/upload", { + ...opts, + queryString: "" + }); + assert.equal(result.route, "worker"); + assert.equal(result.workerIndex, undefined); + }); + + it("different upload_ids can map to different workers", () => { + // Generate several IDs and check we get at least 2 distinct indices + const indices = new Set(); + for (let i = 0; i < 20; i++) { + const result = classifyRoute("/gradio_api/upload", { + ...opts, + queryString: `upload_id=id-${i}` + }); + indices.add(result.workerIndex); + } + assert.ok( + indices.size > 1, + "expected different upload_ids to map to different workers" + ); + }); + + it("non-upload worker routes get no workerIndex", () => { + const result = classifyRoute("/static/img/logo.svg", { + ...opts, + queryString: "upload_id=abc123" + }); + assert.equal(result.route, "worker"); + assert.equal(result.workerIndex, undefined); + }); + }); +}); + +describe("hashString", () => { + it("returns a non-negative integer", () => { + const h = hashString("test"); + assert.equal(typeof h, "number"); + assert.ok(h >= 0); + assert.ok(Number.isInteger(h)); + }); + + it("is deterministic", () => { + assert.equal(hashString("abc123"), hashString("abc123")); + }); + + it("different strings produce different hashes", () => { + assert.notEqual(hashString("abc"), hashString("xyz")); + }); +}); + +describe("matchesPrefix", () => { + it("exact match", () => { + assert.ok(matchesPrefix("/config", ["/config"])); + }); + + it("prefix match", () => { + assert.ok(matchesPrefix("/config/foo", ["/config"])); + }); + + it("no match", () => { + assert.ok(!matchesPrefix("/other", ["/config"])); + }); + + it("does not match partial prefix", () => { + // /configurable should NOT match /config as a prefix? + // Actually it does because /configurable.startsWith("/config") is true. + // This is the current behavior — documenting it. + assert.ok(matchesPrefix("/configurable", ["/config"])); + }); +}); + +describe("route prefix lists are consistent", () => { + it("STATIC_ROUTE_PREFIXES checked before PYTHON_ROUTE_PREFIXES catches /gradio_api/*", () => { + // The critical routes that must be in STATIC before PYTHON catches them + const critical = [ + "/gradio_api/upload", + "/gradio_api/upload_progress", + "/gradio_api/file=", + "/gradio_api/file/" + ]; + for (const route of critical) { + assert.ok( + STATIC_ROUTE_PREFIXES.some((p) => route.startsWith(p)), + `${route} must be in STATIC_ROUTE_PREFIXES` + ); + } + }); + + it("/gradio_api is in PYTHON_ROUTE_PREFIXES", () => { + assert.ok(PYTHON_ROUTE_PREFIXES.includes("/gradio_api")); + }); +}); diff --git a/js/app/src/app.d.ts b/js/app/src/app.d.ts new file mode 100644 index 0000000..743f07b --- /dev/null +++ b/js/app/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/js/app/src/app.html b/js/app/src/app.html new file mode 100644 index 0000000..1fd8b80 --- /dev/null +++ b/js/app/src/app.html @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/js/app/src/lib/index.ts b/js/app/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/js/app/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/js/app/src/routes/+layout.server.ts b/js/app/src/routes/+layout.server.ts new file mode 100644 index 0000000..129bd8d --- /dev/null +++ b/js/app/src/routes/+layout.server.ts @@ -0,0 +1,10 @@ +import { redirect } from "@sveltejs/kit"; +import { dev } from "$app/environment"; + +export function load({ url }): void { + const { pathname, search } = url; + + if (dev && url.pathname.startsWith("/theme")) { + redirect(308, `http://127.0.0.1:7860${pathname}${search}`); + } +} diff --git a/js/app/src/routes/+layout.svelte b/js/app/src/routes/+layout.svelte new file mode 100644 index 0000000..fd580c4 --- /dev/null +++ b/js/app/src/routes/+layout.svelte @@ -0,0 +1,22 @@ + + + + + diff --git a/js/app/src/routes/[...catchall]/+page.server.ts b/js/app/src/routes/[...catchall]/+page.server.ts new file mode 100644 index 0000000..fc06ddd --- /dev/null +++ b/js/app/src/routes/[...catchall]/+page.server.ts @@ -0,0 +1,54 @@ +import { dev } from "$app/environment"; + +export async function load({ request }: { request: Request }): Promise<{ + server: string; + port: string; + local_dev_mode: string | undefined; + accept_language: string; + root_url: string; + mount_path: string; + cookie: string | null; + auth_required: boolean; +}> { + const server = + request.headers.get("x-gradio-server") || "http://127.0.0.1:7860"; + const port = request.headers.get("x-gradio-port") || "7860"; + const local_dev_mode = + request.headers.get("x-gradio-local-dev-mode") || dev ? "true" : undefined; + const accept_language = request.headers.get("accept-language") || "en"; + const mount_path = request.headers.get("x-gradio-mounted-path") || "/"; + const real_url = new URL( + request.headers.get("x-gradio-original-url") || server + ).origin; + const cookie = request.headers.get("cookie"); + + // Check if auth is required by making a request to /config + // This runs only on the server, so it's safe to make this request + let auth_required = false; + try { + const configResponse = await fetch(`${server}/config`, { + headers: { + ...(cookie ? { Cookie: cookie } : {}) + } + }); + if (configResponse.status === 401) { + auth_required = true; + } + } catch (e) { + // If we can't reach the server, we'll find out in the universal load + } + + // Remove trailing slash from root_url to avoid double slashes in URLs + const root_url = new URL(mount_path, real_url).href.replace(/\/$/, ""); + + return { + server: server, + root_url: root_url, + mount_path: mount_path, + port: port, + local_dev_mode: local_dev_mode, + accept_language: accept_language, + cookie: cookie, + auth_required: auth_required + }; +} diff --git a/js/app/src/routes/[...catchall]/+page.svelte b/js/app/src/routes/[...catchall]/+page.svelte new file mode 100644 index 0000000..9d43ef3 --- /dev/null +++ b/js/app/src/routes/[...catchall]/+page.svelte @@ -0,0 +1,476 @@ + + + + + + + {/if} + {/each} + {/if} + + + + + {#if config?.auth_required} + + {:else if config && app} + + {/if} + diff --git a/js/app/src/routes/[...catchall]/+page.ts b/js/app/src/routes/[...catchall]/+page.ts new file mode 100644 index 0000000..d4c467b --- /dev/null +++ b/js/app/src/routes/[...catchall]/+page.ts @@ -0,0 +1,164 @@ +// import { type LayoutServerLoad } from "./$types"; +import { browser } from "$app/environment"; + +import { Client } from "@gradio/client"; + +import type { Config } from "@gradio/client"; +import { MISSING_CREDENTIALS_MSG } from "@gradio/client"; +import { setupi18n } from "@gradio/core"; + +export let ssr = true; + +export async function load({ + url, + data: { + server, + port, + local_dev_mode, + accept_language, + root_url, + mount_path, + cookie, + auth_required + }, + route +}): Promise<{ + config: Config; + api_url: string; + layout: unknown; + app: Client | null; +}> { + let app: Client; + const api_url = + browser && !local_dev_mode && root_url + ? new URL(mount_path || "/", root_url).href + : server; + const deepLink = url.searchParams.get("deep_link"); + const headers = new Headers(); + if (!browser) { + headers.append("x-gradio-server", root_url); + if (cookie) { + headers.append("Cookie", cookie); + } + } else { + headers.append( + "x-gradio-server", + new URL(mount_path, location.origin).href + ); + } + + // If the server-side check determined auth is required, skip Client.connect + // This prevents the 401 error on the client during hydration + if (auth_required) { + await setupi18n(undefined, accept_language); + return { + config: { + auth_required: true, + auth_message: "", + components: [], + current_page: "", + dependencies: [], + layout: {}, + pages: [], + page: {}, + root: root_url, + space_id: null, + analytics_enabled: false, + connect_heartbeat: false, + css: "", + js: "", + theme_hash: 0, + head: "", + dev_mode: false, + enable_queue: false, + show_error: false, + fill_height: false, + fill_width: false, + mode: "blocks", + theme: "default", + title: "", + version: "", + api_prefix: "", + is_space: false, + is_colab: false, + footer_links: ["gradio", "settings"], + stylesheets: [], + protocol: "sse_v3", + username: "" + }, + api_url, + layout: {}, + app: null + }; + } + + try { + app = await Client.connect(api_url, { + with_null_state: true, + events: ["data", "log", "status", "render"], + query_params: deepLink ? { deep_link: deepLink } : undefined, + headers, + cookies: cookie || undefined + }); + } catch (error: any) { + const error_message = error.message || ""; + let auth_message = ""; + if (!error_message.includes(MISSING_CREDENTIALS_MSG)) { + auth_message = error_message.replace(/^Error:?\s*/, ""); + } + await setupi18n(undefined, accept_language); + return { + config: { + auth_message: auth_message, + auth_required: true, + components: [], + current_page: "", + dependencies: [], + layout: {}, + pages: [], + page: {}, + root: root_url, + space_id: null, + analytics_enabled: false, + connect_heartbeat: false, + css: "", + js: "", + theme_hash: 0, + head: "", + dev_mode: false, + enable_queue: false, + show_error: false, + fill_height: false, + fill_width: false, + mode: "blocks", + theme: "default", + title: "", + version: "", + api_prefix: "", + + is_space: false, + is_colab: false, + footer_links: ["gradio", "settings"], + stylesheets: [], + protocol: "sse_v3", + username: "" + }, + api_url, + layout: {}, + app: null + }; + } + + if (!app.config) { + throw new Error("No config found"); + } + + let page_config = app.get_url_config(url.toString()); + + await setupi18n(app.config?.i18n_translations || undefined, accept_language); + return { + config: page_config, + api_url, + app + }; +} diff --git a/js/app/src/routes/svelte_init.ts b/js/app/src/routes/svelte_init.ts new file mode 100644 index 0000000..4de68af --- /dev/null +++ b/js/app/src/routes/svelte_init.ts @@ -0,0 +1,20 @@ +import * as svelte from "svelte"; +const is_browser = typeof window !== "undefined"; +if (is_browser) { + const o = { + SvelteComponent: svelte.SvelteComponent + }; + for (const key in svelte) { + // if (key === "SvelteComponent") continue; + // if (key === "SvelteComponentDev") { + // //@ts-ignore + // o[key] = o["SvelteComponent"]; + // } else { + // @ts-ignore + o[key] = svelte[key]; + // } + } + window.__gradio__svelte__internal = o; + window.__gradio__svelte__internal["globals"] = {}; + window.globals = window; +} diff --git a/js/app/svelte.config.js b/js/app/svelte.config.js new file mode 100644 index 0000000..183ec3c --- /dev/null +++ b/js/app/svelte.config.js @@ -0,0 +1,53 @@ +import adapter from "@sveltejs/adapter-node"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import { sveltePreprocess } from "svelte-preprocess"; +import custom_media from "postcss-custom-media"; +import global_data from "@csstools/postcss-global-data"; +import { resolve } from "path"; +import { fileURLToPath } from "url"; +import { join } from "path"; + +const __dirname = fileURLToPath(import.meta.url); +const out_path = resolve(__dirname, "../../../gradio/templates/node/build"); +const theme_token_path = join( + __dirname, + "..", + "..", + "theme", + "src", + "tokens.css" +); +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + + preprocess: [ + vitePreprocess(), + sveltePreprocess({ + postcss: { + plugins: [global_data({ files: [theme_token_path] }), custom_media()] + } + }) + ], + + vitePlugin: { + prebundleSvelteLibraries: false + }, + + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter({ + out: out_path + }) + }, + compilerOptions: { + experimental: { + async: true + } + } +}; + +export default config; diff --git a/js/app/tsconfig.json b/js/app/tsconfig.json new file mode 100644 index 0000000..e0c09a6 --- /dev/null +++ b/js/app/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler", + "module": "esnext", + "customConditions": ["gradio"], + "target": "esnext" + } +} diff --git a/js/app/vite.config.ts b/js/app/vite.config.ts new file mode 100644 index 0000000..f27bef3 --- /dev/null +++ b/js/app/vite.config.ts @@ -0,0 +1,96 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig, type Plugin } from "vite"; + +// @ts-ignore +import custom_media from "postcss-custom-media"; +// @ts-ignore +import prefixer from "postcss-prefix-selector"; +import { cpSync, readFileSync, writeFileSync } from "fs"; +import { resolve, join } from "path"; + +import { inject_component_loader } from "@self/build"; + +const version_path = resolve(__dirname, "../../gradio/package.json"); +const version_raw = JSON.parse( + readFileSync(version_path, { encoding: "utf-8" }) +).version.trim(); +const version = version_raw.replace(/\./g, "-"); + +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); +const svelte = require("svelte/package.json"); +const svelte_exports = Object.keys(svelte.exports) + .filter((p) => p.endsWith(".json")) + .map((entry) => entry.replace(/^\./, "svelte").split("/").join("_") + ".js"); + +export default defineConfig(({ mode }) => { + const production = mode === "production"; + return { + server: { + port: 9876, + open: "/", + proxy: { + "/manifest.json": "http://localhost:7860", + "^.*/theme\\.css": "http://localhost:7860", + "^/static/.*": "http://localhost:7860", + "^.*/svelte/.*": "http://localhost:7860", + "^/gradio_api/.*": "http://localhost:7860" + } + }, + resolve: { + conditions: ["gradio", "browser"] + }, + ssr: { + resolve: { + conditions: ["gradio"] + }, + noExternal: ["@gradio/*", "@huggingface/space-header"], + external: mode === "development" ? [] : ["svelte", "svelte/*"] + }, + + build: { + rollupOptions: { + external: svelte_exports + }, + minify: true, + sourcemap: false + }, + + define: { + BUILD_MODE: production ? JSON.stringify("prod") : JSON.stringify("dev"), + BACKEND_URL: production + ? JSON.stringify("") + : JSON.stringify("http://127.0.0.1:7860/"), + GRADIO_VERSION: JSON.stringify(version) + }, + css: { + postcss: { + plugins: [ + prefixer({ + prefix: `.gradio-container-${version}`, + // @ts-ignore + transform(prefix, selector, prefixedSelector, fileName) { + if (selector.indexOf("gradio-container") > -1) { + return prefix; + } else if ( + selector.indexOf(":root") > -1 || + selector.indexOf("dark") > -1 || + selector.indexOf("body") > -1 || + fileName.indexOf(".svelte") > -1 + ) { + return selector; + } + return prefixedSelector; + } + }), + custom_media() + ] + } + }, + optimizeDeps: { + exclude: ["@gradio/*", "/svelte", "/svelte/*"] + }, + plugins: [sveltekit(), inject_component_loader({ mode })] + }; +}); diff --git a/js/atoms/Block.test.ts b/js/atoms/Block.test.ts new file mode 100644 index 0000000..b5e48d9 --- /dev/null +++ b/js/atoms/Block.test.ts @@ -0,0 +1,69 @@ +import { test, describe, assert, afterEach, beforeEach } from "vitest"; +import { tick, mount, unmount } from "svelte"; + +import Block from "./src/Block.svelte"; + +describe("Block Accessibility", () => { + let target: HTMLElement; + let component: any; + + beforeEach(() => { + target = document.body.appendChild(document.createElement("div")); + }); + + afterEach(() => { + if (component) { + unmount(component); + } + if (target.parentNode) { + target.parentNode.removeChild(target); + } + }); + + test("renders with aria-label when label prop is provided", async () => { + component = mount(Block, { + target, + props: { + label: "Test Block Label" + } + }); + await tick(); + const block = target.querySelector(".block"); + assert.equal(block?.getAttribute("aria-label"), "Test Block Label"); + }); + + test("renders without role when type is normal", async () => { + component = mount(Block, { + target, + props: { + type: "normal" + } + }); + await tick(); + const block = target.querySelector(".block"); + assert.isNull(block?.getAttribute("role")); + assert.equal(block?.tagName.toLowerCase(), "div"); + }); + + test("renders as div by default", async () => { + component = mount(Block, { + target, + props: {} + }); + await tick(); + const block = target.querySelector(".block"); + assert.equal(block?.tagName.toLowerCase(), "div"); + }); + + test("renders as fieldset when type is fieldset", async () => { + component = mount(Block, { + target, + props: { + type: "fieldset" + } + }); + await tick(); + const block = target.querySelector(".block"); + assert.equal(block?.tagName.toLowerCase(), "fieldset"); + }); +}); diff --git a/js/atoms/CHANGELOG.md b/js/atoms/CHANGELOG.md new file mode 100644 index 0000000..853a150 --- /dev/null +++ b/js/atoms/CHANGELOG.md @@ -0,0 +1,780 @@ +# @gradio/atoms + +## 0.26.0 + +### Features + +- [#13543](https://github.com/gradio-app/gradio/pull/13543) [`0533483`](https://github.com/gradio-app/gradio/commit/0533483bccdee38f334a598f18297e8c02966343) - Migrate Image components to Svelte 5. Thanks @dawoodkhan82! + +### Dependency updates + +- @gradio/icons@0.16.0 + +## 0.25.0 + +### Features + +- [#13526](https://github.com/gradio-app/gradio/pull/13526) [`53cb4ca`](https://github.com/gradio-app/gradio/commit/53cb4cae1ec3521e9170d12867253516413ba37a) - Run `pnpm lint` and `pnpm ts:check` on CI. Thanks @abidlabs! + +### Dependency updates + +- @gradio/utils@0.13.0 + +## 0.24.0 + +### Features + +- [#13252](https://github.com/gradio-app/gradio/pull/13252) [`e39f028`](https://github.com/gradio-app/gradio/commit/e39f0284582bbac93fd01b2a5eae4ecae219f252) - Add ImageSlider unit tests. Thanks @pngwn! + +## 0.23.1 + +### Features + +- [#13190](https://github.com/gradio-app/gradio/pull/13190) [`bc53d08`](https://github.com/gradio-app/gradio/commit/bc53d0891466c09b3b40b89b70df5770cfc6967f) - Chatbot Unit Tests. Thanks @freddyaboulton! + +## 0.23.0 + +### Features + +- [#13122](https://github.com/gradio-app/gradio/pull/13122) [`64828b0`](https://github.com/gradio-app/gradio/commit/64828b08d5be4fdde8a73932b3f288c073ec49bd) - Add Image Unit Tests. Thanks @freddyaboulton! + +### Fixes + +- [#13041](https://github.com/gradio-app/gradio/pull/13041) [`835e4bd`](https://github.com/gradio-app/gradio/commit/835e4bd1adcaf5716283fa379e909f916a032b8a) - Reduce load times of all components. Thanks @dawoodkhan82! + +### Dependency updates + +- @gradio/utils@0.12.2 + +## 0.22.2 + +### Dependency updates + +- @gradio/utils@0.12.1 + +## 0.22.2 + +### Dependency updates + +- @gradio/utils@0.12.0 +- @gradio/markdown-code@0.6.1 + +## 0.22.1 + +### Features + +- [#12933](https://github.com/gradio-app/gradio/pull/12933) [`44a8a9f`](https://github.com/gradio-app/gradio/commit/44a8a9fe1316f1c110638c3c40158a75b6c0d135) - Fix border in gr.html as layout. Thanks @aliabid94! + +## 0.22.0 + +### Features + +- [#12839](https://github.com/gradio-app/gradio/pull/12839) [`1c671b3`](https://github.com/gradio-app/gradio/commit/1c671b39830ccf1c87f6cfcb4669e97dfb3a7367) - Hide forms with no elements. Thanks @aliabid94! + +### Dependency updates + +- @gradio/utils@0.11.3 + +## 0.21.0 + +### Features + +- [#12804](https://github.com/gradio-app/gradio/pull/12804) [`fc366b4`](https://github.com/gradio-app/gradio/commit/fc366b485325c900b0c2b85ba05b7d23a81a0dee) - Allow webcam uploads and clipboard paste for gallery. Thanks @freddyaboulton! + +## 0.20.1 + +### Fixes + +- [#12800](https://github.com/gradio-app/gradio/pull/12800) [`7a1c321`](https://github.com/gradio-app/gradio/commit/7a1c321b6546ba05a353488f5133e8262c4a8a39) - Bump svelte/kit for security reasons. Thanks @freddyaboulton! +- [#12779](https://github.com/gradio-app/gradio/pull/12779) [`ea2d3e9`](https://github.com/gradio-app/gradio/commit/ea2d3e985a8b42d188e551f517c5825c00790628) - Migrate Audio + Upload + Atoms to Svelte 5. Thanks @dawoodkhan82! +- [#12607](https://github.com/gradio-app/gradio/pull/12607) [`299728b`](https://github.com/gradio-app/gradio/commit/299728b707c88e7afbc6a60f1f266a9013fa424d) - fix: add ARIA landmarks for accessibility. Thanks @majiayu000! +- [#12795](https://github.com/gradio-app/gradio/pull/12795) [`184968a`](https://github.com/gradio-app/gradio/commit/184968aa13636b5afe78f8ae443cd7fc29635bfa) - Add fade effect to overflowing text. Thanks @hannahblair! + +### Dependency updates + +- @gradio/utils@0.11.2 +- @gradio/icons@0.15.1 +- @gradio/markdown-code@0.6.1 + +## 0.20.0 + +### Dependency updates + +- @gradio/utils@0.11.1 + +## 0.20.0 + +### Features + +- [#12539](https://github.com/gradio-app/gradio/pull/12539) [`f1d83fa`](https://github.com/gradio-app/gradio/commit/f1d83fac3d6e4bad60cf896a026fa2d572f26073) - Add ability to add custom buttons to components. Thanks @abidlabs! + +### Dependency updates + +- @gradio/utils@0.11.0 + +## 0.19.0 + +### Dependency updates + +- @gradio/utils@0.10.4 + +## 0.19.0 + +### Features + +- [#11908](https://github.com/gradio-app/gradio/pull/11908) [`029034f`](https://github.com/gradio-app/gradio/commit/029034f7853ea018d110efe9b7e2ef7d1407091c) - Improve audio player UI in gr.Chatbot +- [#12438](https://github.com/gradio-app/gradio/pull/12438) [`25ffc03`](https://github.com/gradio-app/gradio/commit/25ffc0398f8feb43d817c02b2ab970c16de6d797) - Svelte5 migration and bugfix + +### Dependencies + +- @gradio/icons@0.15.0 +- @gradio/markdown-code@0.6.0 +- @gradio/utils@0.10.3 + +## 0.19.0-dev.1 + +### Features + +- [#12102](https://github.com/gradio-app/gradio/pull/12102) [`baa1cd6`](https://github.com/gradio-app/gradio/commit/baa1cd67573292f95d4b4263c8f15fe89fbeeaa1) - Improve audio player UI in gr.Chatbot. Thanks @hannahblair! + +### Dependency updates + +- @gradio/markdown-code@0.5.2 + +## 0.18.2-dev.0 + +### Dependency updates + +- @gradio/utils@0.10.3-dev.0 +- @gradio/icons@0.15.0-dev.0 + +## 0.18.1 + +### Fixes + +- [#11964](https://github.com/gradio-app/gradio/pull/11964) [`86e6176`](https://github.com/gradio-app/gradio/commit/86e61763a14c745f7eb0ce35b861ea2be2336a15) - Add dark mode to storybook. Thanks @hannahblair! + +## 0.18.0 + +### Dependency updates + +- @gradio/markdown-code@0.5.2 + +## 0.18.0 + +### Features + +- [#11858](https://github.com/gradio-app/gradio/pull/11858) [`3f8ea13`](https://github.com/gradio-app/gradio/commit/3f8ea13a8ca92abf0ad34392e403a449fda3c6c2) - remove lite. Thanks @pngwn! + +### Fixes + +- [#11853](https://github.com/gradio-app/gradio/pull/11853) [`0d0699b`](https://github.com/gradio-app/gradio/commit/0d0699b0348dfd4e5e0ac82d438a405cf7a2228d) - Fix bugs in `FileExplorer`. Thanks @abidlabs! +- [#11784](https://github.com/gradio-app/gradio/pull/11784) [`d9dd3f5`](https://github.com/gradio-app/gradio/commit/d9dd3f54b7fb34cf7118e549d39fc63937ca3489) - Add "hidden" option to component's `visible` kwarg to render but visually hide the component. Thanks @pngwn! + +## 0.17.0 + +### Features + +- [#11814](https://github.com/gradio-app/gradio/pull/11814) [`013784a`](https://github.com/gradio-app/gradio/commit/013784a7086047651e8e661a38bde7d5c7f10db7) - add validation support. Thanks @pngwn! + +### Dependency updates + +- @gradio/icons@0.14.0 + +## 0.16.5 + +### Dependency updates + +- @gradio/markdown-code@0.5.1 + +## 0.16.5 + +### Dependency updates + +- @gradio/icons@0.13.1 + +## 0.16.5 + +### Dependency updates + +- @gradio/icons@0.13.0 + +## 0.16.4 + +### Fixes + +- [#11692](https://github.com/gradio-app/gradio/pull/11692) [`0b91ca9`](https://github.com/gradio-app/gradio/commit/0b91ca914774fe617bc5987986e14bf1b81fcb8d) - Fix z-index of icon button wrapper. Thanks @dawoodkhan82! + +## 0.16.3 + +### Dependency updates + +- @gradio/markdown-code@0.5.0 + +## 0.16.2 + +### Fixes + +- [#11387](https://github.com/gradio-app/gradio/pull/11387) [`8245afc`](https://github.com/gradio-app/gradio/commit/8245afc669501e1e5f0d619f452455f68a3b7667) - Define root URL in frontend. Thanks @aliabid94! + +### Dependency updates + +- @gradio/markdown-code@0.4.4 + +## 0.16.1 + +### Dependency updates + +- @gradio/markdown-code@0.4.3 + +## 0.16.1 + +### Dependency updates + +- @gradio/markdown-code@0.4.3 + +## 0.16.1 + +### Features + +- [#11177](https://github.com/gradio-app/gradio/pull/11177) [`3068196`](https://github.com/gradio-app/gradio/commit/3068196d47234fd1c1634f33b9ddfc089f5cbbe0) - Improved, smoother fullscreen mode for components. Thanks @aliabid94! + +## 0.16.0 + +### Features + +- [#11027](https://github.com/gradio-app/gradio/pull/11027) [`eff532b`](https://github.com/gradio-app/gradio/commit/eff532b913a3c8f06f10a4f9471d3177e3744053) - Add new `ImageSlider` component. Thanks @pngwn! + +## 0.15.2 + +### Dependency updates + +- @gradio/markdown-code@0.4.3 +- @gradio/utils@0.10.2 + +## 0.15.1 + +### Dependency updates + +- @gradio/icons@0.12.0 + +## 0.15.0 + +### Features + +- [#10933](https://github.com/gradio-app/gradio/pull/10933) [`b768651`](https://github.com/gradio-app/gradio/commit/b7686515d99276123731698bfde09bb328b7d286) - Add `rtl` to Block Label. Thanks @hannahblair! +- [#10924](https://github.com/gradio-app/gradio/pull/10924) [`be46b94`](https://github.com/gradio-app/gradio/commit/be46b94f51c29d66fef9320d59a7f7e5a0df81b4) - Add `rtl` to gr.HighlightedText. Thanks @hannahblair! +- [#10635](https://github.com/gradio-app/gradio/pull/10635) [`2f68c9d`](https://github.com/gradio-app/gradio/commit/2f68c9d988dcbc53a0b8e53bdb1de49c9c8c65d8) - Refactor and redesign `ImageEditor` component. Thanks @pngwn! +- [#10923](https://github.com/gradio-app/gradio/pull/10923) [`8a62c7e`](https://github.com/gradio-app/gradio/commit/8a62c7e90b5e0945709d97e34f405a7f92675ab0) - Add `rtl` to `gr.Radio`. Thanks @hannahblair! + +### Fixes + +- [#10925](https://github.com/gradio-app/gradio/pull/10925) [`c37de0f`](https://github.com/gradio-app/gradio/commit/c37de0f9081f30ac963b0c837e8f1985461afbb1) - Tweak `rtl` UI in `gr.MultimodalTextbox`. Thanks @hannahblair! + +### Dependency updates + +- @gradio/icons@0.11.0 + +## 0.14.1 + +### Dependency updates + +- @gradio/markdown-code@0.4.2 + +## 0.14.0 + +### Features + +- [#10778](https://github.com/gradio-app/gradio/pull/10778) [`373007b`](https://github.com/gradio-app/gradio/commit/373007b3e9d019ee41589d1dbb09a7511a024a51) - Allow sorting by multiple columns in dataframe. Thanks @hannahblair! + +### Dependency updates + +- @gradio/markdown-code@0.4.1 + +## 0.13.3 + +### Features + +- [#10625](https://github.com/gradio-app/gradio/pull/10625) [`ce4fb99`](https://github.com/gradio-app/gradio/commit/ce4fb994e042489d8c6fbcab3e94d97b08369dce) - fix spelling of `resizable` parameter in `gr.Chatbot`. Thanks @abidlabs! + +## 0.13.2 + +### Dependency updates + +- @gradio/utils@0.10.1 +- @gradio/markdown-code@0.4.0 + +## 0.13.1 + +### Dependency updates + +- @gradio/icons@0.10.0 + +## 0.13.0 + +### Features + +- [#10272](https://github.com/gradio-app/gradio/pull/10272) [`a1f2649`](https://github.com/gradio-app/gradio/commit/a1f2649586752a013fb4d36b83d5fea2e137bb81) - Chat Interface flagging and chatbot feedback. Thanks @aliabid94! +- [#10292](https://github.com/gradio-app/gradio/pull/10292) [`f2bd72f`](https://github.com/gradio-app/gradio/commit/f2bd72f9ef23552f0c6018396320eca9baef04f5) - Reset flagged values when switching conversations in chat history. Thanks @abidlabs! +- [#10192](https://github.com/gradio-app/gradio/pull/10192) [`4fc7fb7`](https://github.com/gradio-app/gradio/commit/4fc7fb777c42af537e4af612423fa44029657d41) - Ensure components can be remounted with their previous data. Thanks @pngwn! + +### Dependency updates + +- @gradio/utils@0.10.0 +- @gradio/markdown-code@0.3.0 +- @gradio/icons@0.9.0 + +## 0.12.0 + +### Features + +- [#10149](https://github.com/gradio-app/gradio/pull/10149) [`9cd291b`](https://github.com/gradio-app/gradio/commit/9cd291b7f1c37ef0ffd3e8721fb2b648003b50fc) - Resizeable chatbot. Thanks @aliabid94! +- [#10098](https://github.com/gradio-app/gradio/pull/10098) [`9a6ce6f`](https://github.com/gradio-app/gradio/commit/9a6ce6f6b089d94c06da0b8620f28967f39f8383) - Refactor full screen logic to be reusable. Thanks @hannahblair! + +### Fixes + +- [#10161](https://github.com/gradio-app/gradio/pull/10161) [`3a053cc`](https://github.com/gradio-app/gradio/commit/3a053cc76c173c6386e0b5102b03e1a56786cbb9) - Fix chatbot `visible` prop not reacting to changes. Thanks @freddyaboulton! + +## 0.11.2 + +### Dependency updates + +- @gradio/utils@0.9.0 + +## 0.11.1 + +### Dependency updates + +- @gradio/utils@0.8.0 + +## 0.11.0 + +### Features + +- [#9891](https://github.com/gradio-app/gradio/pull/9891) [`fc12496`](https://github.com/gradio-app/gradio/commit/fc124964a1b4922e54a4ca4755f0a536dfae1a21) - Allow uploading more files in gr.File. Thanks @hannahblair! + +### Fixes + +- [#9882](https://github.com/gradio-app/gradio/pull/9882) [`6c8a064`](https://github.com/gradio-app/gradio/commit/6c8a064feeaa89a2ffc96260032f24f18eb032fa) - Ensure non-form elements are correctly positioned when scale is applied. Thanks @hannahblair! + +## 0.10.1 + +### Fixes + +- [#9651](https://github.com/gradio-app/gradio/pull/9651) [`1163a37`](https://github.com/gradio-app/gradio/commit/1163a372a61cf84d110160c1711892b9b689d1d3) - Fixes component info font size. Thanks @dawoodkhan82! + +### Dependency updates + +- @gradio/markdown-code@0.2.1 + +## 0.10.0 + +### Features + +- [#9756](https://github.com/gradio-app/gradio/pull/9756) [`92f337c`](https://github.com/gradio-app/gradio/commit/92f337cc85d545060ea343f1cee85c22b85f6444) - Fix website build issue. Thanks @aliabd! + +### Dependency updates + +- @gradio/icons@0.8.1 +- @gradio/markdown-code@0.2.0 + +## 0.9.2 + +### Fixes + +- [#9730](https://github.com/gradio-app/gradio/pull/9730) [`39a0e8c`](https://github.com/gradio-app/gradio/commit/39a0e8c2fb038eb0afc213fa6290c9b2acee7941) - Fix chatbot component streaming bug and visible bug. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/markdown@0.10.3 + +## 0.9.1 + +### Fixes + +- [#9711](https://github.com/gradio-app/gradio/pull/9711) [`7134fc2`](https://github.com/gradio-app/gradio/commit/7134fc272e9e60be4b80dfd294ff8926d5995188) - Custom component fixes. Thanks @freddyaboulton! + +### Dependency updates + +- @gradio/markdown@0.10.2 + +## 0.9.0 + +### Features + +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Adding new themes to Gradio 5.0 +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Improve Icon Button consistency +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Centre components within `Block` when height and width are set +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Move icons into `IconButtonWrapper` +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Allow `info=` to render markdown +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Fix chatinterface embedding height issues +- [#8843](https://github.com/gradio-app/gradio/pull/8843) [`6f95286`](https://github.com/gradio-app/gradio/commit/6f95286337459efbccb95c9cfac63355669df9ee) - Standardize `height` across components and add `max_height` and `min_height` parameters where appropriate + +### Dependencies + +- @gradio/icons@0.8.0 +- @gradio/utils@0.7.0 + +## 0.9.0-beta.5 + +### Features + +- [#9437](https://github.com/gradio-app/gradio/pull/9437) [`c3d93be`](https://github.com/gradio-app/gradio/commit/c3d93bef94b9401747a363f7bad88a1d347d535b) - Adding new themes to Gradio 5.0. Thanks @allisonwhilden! + +### Dependency updates + +- @gradio/icons@0.8.0-beta.4 + +## 0.9.0-beta.4 + +### Features + +- [#9521](https://github.com/gradio-app/gradio/pull/9521) [`06ef22e`](https://github.com/gradio-app/gradio/commit/06ef22e83cdd27e7afb381396d153d9db3dea16e) - Allow `info=` to render markdown. Thanks @dawoodkhan82! +- [#9571](https://github.com/gradio-app/gradio/pull/9571) [`148345d`](https://github.com/gradio-app/gradio/commit/148345d107763754710505281ad70368ebc6f3ec) - Fix chatinterface embedding height issues. Thanks @aliabid94! + +## 0.9.0-beta.3 + +### Features + +- [#9504](https://github.com/gradio-app/gradio/pull/9504) [`d054262`](https://github.com/gradio-app/gradio/commit/d054262f611d5f1eb1a1c936db7152347a891f8e) - Centre components within `Block` when height and width are set. Thanks @hannahblair! + +### Dependency updates + +- @gradio/icons@0.8.0-beta.3 + +## 0.9.0-beta.2 + +### Features + +- [#9261](https://github.com/gradio-app/gradio/pull/9261) [`73647a0`](https://github.com/gradio-app/gradio/commit/73647a07b0439efabe3dd218ff6c366ffa3b84a0) - Move icons into `IconButtonWrapper`. Thanks @hannahblair! +- [#9313](https://github.com/gradio-app/gradio/pull/9313) [`1fef9d9`](https://github.com/gradio-app/gradio/commit/1fef9d9a26f0ebce4de18c486702661f6539b1c6) - Standardize `height` across components and add `max_height` and `min_height` parameters where appropriate. Thanks @abidlabs! +- [#9250](https://github.com/gradio-app/gradio/pull/9250) [`350b0a5`](https://github.com/gradio-app/gradio/commit/350b0a5cafb9176f914f62e7c90de51d4352cc77) - Improve Icon Button consistency. Thanks @hannahblair! + +### Dependency updates + +- @gradio/icons@0.8.0-beta.2 +- @gradio/utils@0.7.0-beta.2 + +## 0.8.1-beta.1 + +### Dependency updates + +- @gradio/icons@0.8.0-beta.1 +- @gradio/utils@0.7.0-beta.1 + +## 0.8.1-beta.0 + +### Fixes + +- [#9163](https://github.com/gradio-app/gradio/pull/9163) [`2b6cbf2`](https://github.com/gradio-app/gradio/commit/2b6cbf25908e42cf027324e54ef2cc0baad11a91) - fix exports and generate types. Thanks @pngwn! + +### Dependency updates + +- @gradio/utils@0.7.0-beta.0 +- @gradio/icons@0.8.0-beta.0 + +## 0.8.0 + +### Features + +- [#8930](https://github.com/gradio-app/gradio/pull/8930) [`41d5ab9`](https://github.com/gradio-app/gradio/commit/41d5ab987ba9728753be4509490c79041655809b) - Add `placeholder` param to Image and ImageEditor to replace upload image text. Thanks @hannahblair! +- [#9118](https://github.com/gradio-app/gradio/pull/9118) [`e1c404d`](https://github.com/gradio-app/gradio/commit/e1c404da1143fb52b659d03e028bdba1badf443d) - setup npm-previews of all packages. Thanks @pngwn! + +### Dependency updates + +- @gradio/utils@0.6.0 +- @gradio/icons@0.7.1 + +## 0.7.9 + +### Dependency updates + +- @gradio/icons@0.7.0 + +## 0.7.8 + +### Dependency updates + +- @gradio/icons@0.6.1 +- @gradio/utils@0.5.2 + +## 0.7.7 + +### Fixes + +- [#8852](https://github.com/gradio-app/gradio/pull/8852) [`16b8200`](https://github.com/gradio-app/gradio/commit/16b820038df43905447ab7623d39d91ceb0d6238) - Fix gr.Image height inconsistencies. Thanks @hannahblair! + +## 0.7.6 + +### Dependency updates + +- @gradio/utils@0.5.1 +- @gradio/icons@0.6.0 + +## 0.7.5 + +### Dependency updates + +- @gradio/utils@0.5.0 +- @gradio/icons@0.5.0 + +## 0.7.4 + +### Dependency updates + +- @gradio/utils@0.4.2 + +## 0.7.3 + +### Dependency updates + +- @gradio/icons@0.4.1 + +## 0.7.2 + +### Dependency updates + +- @gradio/utils@0.4.1 + +## 0.7.1 + +### Dependency updates + +- @gradio/utils@0.4.0 + +## 0.7.0 + +### Features + +- [#8042](https://github.com/gradio-app/gradio/pull/8042) [`92139f3`](https://github.com/gradio-app/gradio/commit/92139f3d7d6b832b649ff1f6c10c87e6fb522cde) - refresh the `ImageEditor` UI. Thanks @pngwn! + +### Dependency updates + +- @gradio/utils@0.3.2 +- @gradio/icons@0.4.0 + +## 0.6.2 + +### Dependency updates + +- @gradio/utils@0.3.1 + +## 0.6.1 + +### Dependency updates + +- @gradio/icons@0.3.4 + +## 0.6.0 + +### Features + +- [#7732](https://github.com/gradio-app/gradio/pull/7732) [`2efb05e`](https://github.com/gradio-app/gradio/commit/2efb05ed99a8a3575aab0a6c14a8d8b91f4e9ed7) - Adds support for kwargs and default arguments in the python client, and improves how parameter information is displayed in the "view API" page. Thanks @abidlabs! + +## 0.5.3 + +### Patch Changes + +- Updated dependencies [[`f191786`](https://github.com/gradio-app/gradio/commit/f1917867916647d383b8d7ce15e0c17f2abbdec1)]: + - @gradio/icons@0.3.3 + +## 0.5.2 + +### Patch Changes + +- Updated dependencies [[`065c5b1`](https://github.com/gradio-app/gradio/commit/065c5b163c4badb9d9cbd06d627fb4ba086003e7)]: + - @gradio/utils@0.3.0 + +## 0.5.1 + +### Patch Changes + +- Updated dependencies [[`fdd1521`](https://github.com/gradio-app/gradio/commit/fdd15213c24b9cbc58bbc1b6beb4af7c18f48557)]: + - @gradio/utils@0.2.2 + +## 0.5.0 + +### Features + +- [#7148](https://github.com/gradio-app/gradio/pull/7148) [`c60ad4d`](https://github.com/gradio-app/gradio/commit/c60ad4d34ab5b56a89bf6796822977e51e7a4a32) - Use Gallery as input component. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +## 0.4.1 + +### Fixes + +- [#6766](https://github.com/gradio-app/gradio/pull/6766) [`73268ee`](https://github.com/gradio-app/gradio/commit/73268ee2e39f23ebdd1e927cb49b8d79c4b9a144) - Improve source selection UX. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 0.4.0 + +### Features + +- [#6569](https://github.com/gradio-app/gradio/pull/6569) [`4d1cbbc`](https://github.com/gradio-app/gradio/commit/4d1cbbcf30833ef1de2d2d2710c7492a379a9a00) - Allow passing height and width as string in `Blocks.svelte`. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [[`206af31`](https://github.com/gradio-app/gradio/commit/206af31d7c1a31013364a44e9b40cf8df304ba50)]: + - @gradio/icons@0.3.1 + +## 0.3.0 + +### Highlights + +#### New `ImageEditor` component ([#6169](https://github.com/gradio-app/gradio/pull/6169) [`9caddc17b`](https://github.com/gradio-app/gradio/commit/9caddc17b1dea8da1af8ba724c6a5eab04ce0ed8)) + +A brand new component, completely separate from `Image` that provides simple editing capabilities. + +- Set background images from file uploads, webcam, or just paste! +- Crop images with an improved cropping UI. App authors can event set specific crop size, or crop ratios (`1:1`, etc) +- Paint on top of any image (or no image) and erase any mistakes! +- The ImageEditor supports layers, confining draw and erase actions to that layer. +- More flexible access to data. The image component returns a composite image representing the final state of the canvas as well as providing the background and all layers as individual images. +- Fully customisable. All features can be enabled and disabled. Even the brush color swatches can be customised. + + + +```py + +def fn(im): + im["composite"] # the full canvas + im["background"] # the background image + im["layers"] # a list of individual layers + + +im = gr.ImageEditor( + # decide which sources you'd like to accept + sources=["upload", "webcam", "clipboard"], + # set a cropsize constraint, can either be a ratio or a concrete [width, height] + crop_size="1:1", + # enable crop (or disable it) + transforms=["crop"], + # customise the brush + brush=Brush( + default_size="25", # or leave it as 'auto' + color_mode="fixed", # 'fixed' hides the user swatches and colorpicker, 'defaults' shows it + default_color="hotpink", # html names are supported + colors=[ + "rgba(0, 150, 150, 1)", # rgb(a) + "#fff", # hex rgb + "hsl(360, 120, 120)" # in fact any valid colorstring + ] + ), + brush=Eraser(default_size="25") +) + +``` + +Thanks [@pngwn](https://github.com/pngwn)! + +## 0.2.2 + +### Fixes + +- [#6254](https://github.com/gradio-app/gradio/pull/6254) [`f816136a0`](https://github.com/gradio-app/gradio/commit/f816136a039fa6011be9c4fb14f573e4050a681a) - Add volume control to Audio. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 0.2.1 + +### Fixes + +- [#6279](https://github.com/gradio-app/gradio/pull/6279) [`3cdeabc68`](https://github.com/gradio-app/gradio/commit/3cdeabc6843000310e1a9e1d17190ecbf3bbc780) - Ensure source selection does not get hidden in overflow. Thanks [@hannahblair](https://github.com/hannahblair)! +- [#6314](https://github.com/gradio-app/gradio/pull/6314) [`fad92c29d`](https://github.com/gradio-app/gradio/commit/fad92c29dc1f5cd84341aae417c495b33e01245f) - Improve default source behaviour in Audio. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 0.2.0 + +### Features + +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Image v4. Thanks [@pngwn](https://github.com/pngwn)! +- [#5498](https://github.com/gradio-app/gradio/pull/5498) [`287fe6782`](https://github.com/gradio-app/gradio/commit/287fe6782825479513e79a5cf0ba0fbfe51443d7) - Custom components. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.2.0-beta.6 + +### Features + +- [#6136](https://github.com/gradio-app/gradio/pull/6136) [`667802a6c`](https://github.com/gradio-app/gradio/commit/667802a6cdbfb2ce454a3be5a78e0990b194548a) - JS Component Documentation. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! +- [#6094](https://github.com/gradio-app/gradio/pull/6094) [`c476bd5a5`](https://github.com/gradio-app/gradio/commit/c476bd5a5b70836163b9c69bf4bfe068b17fbe13) - Image v4. Thanks [@pngwn](https://github.com/pngwn)! + +## 0.2.0-beta.5 + +### Patch Changes + +- Updated dependencies [[`9cad2127b`](https://github.com/gradio-app/gradio/commit/9cad2127b965023687470b3abfe620e188a9da6e), [`dbb7de5e0`](https://github.com/gradio-app/gradio/commit/dbb7de5e02c53fee05889d696d764d212cb96c74)]: + - @gradio/icons@0.2.0-beta.2 + - @gradio/utils@0.2.0-beta.5 + +## 0.2.0-beta.4 + +### Features + +- [#5938](https://github.com/gradio-app/gradio/pull/5938) [`13ed8a485`](https://github.com/gradio-app/gradio/commit/13ed8a485d5e31d7d75af87fe8654b661edcca93) - V4: Use beta release versions for '@gradio' packages. Thanks [@freddyaboulton](https://github.com/freddyaboulton)! + +### Fixes + +- [#5944](https://github.com/gradio-app/gradio/pull/5944) [`465f58957`](https://github.com/gradio-app/gradio/commit/465f58957f70c7cf3e894beef8a117b28339e3c1) - Show empty JSON icon when `value` is `null`. Thanks [@hannahblair](https://github.com/hannahblair)! + +## 0.2.0 + +### Features + +- [#5864](https://github.com/gradio-app/gradio/pull/5864) [`e70805d54`](https://github.com/gradio-app/gradio/commit/e70805d54cc792452545f5d8eccc1aa0212a4695) - Change `BlockLabel` element to use `