Upload folder using huggingface_hub
Browse files- .claude/skills/huggingface-gradio/SKILL.md +298 -0
- .claude/skills/huggingface-gradio/examples.md +613 -0
- .gitattributes +6 -0
- app.py +24 -16
- assets/case_bank.json +97 -0
- assets/voice_caught_0.wav +3 -0
- assets/voice_caught_1.wav +3 -0
- assets/voice_caught_2.wav +3 -0
- assets/voice_escaped_0.wav +3 -0
- assets/voice_escaped_1.wav +3 -0
- assets/voice_escaped_2.wav +3 -0
- game/face.py +0 -8
- game/lineup.py +1 -1
- game/model.py +8 -3
- game/poster.py +2 -10
- game/render.py +15 -0
- modal_factory.py +59 -9
- train/gen_dataset.py +7 -0
.claude/skills/huggingface-gradio/SKILL.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: huggingface-gradio
|
| 3 |
+
description: Build Gradio web UIs and demos in Python. Use when creating or editing Gradio apps, components, event listeners, layouts, or chatbots.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Gradio
|
| 7 |
+
|
| 8 |
+
Gradio is a Python library for building interactive web UIs and ML demos. This skill covers the core API, patterns, and examples.
|
| 9 |
+
|
| 10 |
+
## Guides
|
| 11 |
+
|
| 12 |
+
Detailed guides on specific topics (read these when relevant):
|
| 13 |
+
|
| 14 |
+
- [Quickstart](https://www.gradio.app/guides/quickstart)
|
| 15 |
+
- [The Interface Class](https://www.gradio.app/guides/the-interface-class)
|
| 16 |
+
- [Blocks and Event Listeners](https://www.gradio.app/guides/blocks-and-event-listeners)
|
| 17 |
+
- [Controlling Layout](https://www.gradio.app/guides/controlling-layout)
|
| 18 |
+
- [More Blocks Features](https://www.gradio.app/guides/more-blocks-features)
|
| 19 |
+
- [Custom CSS and JS](https://www.gradio.app/guides/custom-CSS-and-JS)
|
| 20 |
+
- [Streaming Outputs](https://www.gradio.app/guides/streaming-outputs)
|
| 21 |
+
- [Streaming Inputs](https://www.gradio.app/guides/streaming-inputs)
|
| 22 |
+
- [Sharing Your App](https://www.gradio.app/guides/sharing-your-app)
|
| 23 |
+
- [Custom HTML Components](https://www.gradio.app/guides/custom-HTML-components)
|
| 24 |
+
- [Getting Started with the Python Client](https://www.gradio.app/guides/getting-started-with-the-python-client)
|
| 25 |
+
- [Getting Started with the JS Client](https://www.gradio.app/guides/getting-started-with-the-js-client)
|
| 26 |
+
|
| 27 |
+
## Core Patterns
|
| 28 |
+
|
| 29 |
+
**Interface** (high-level): wraps a function with input/output components.
|
| 30 |
+
|
| 31 |
+
```python
|
| 32 |
+
import gradio as gr
|
| 33 |
+
|
| 34 |
+
def greet(name):
|
| 35 |
+
return f"Hello {name}!"
|
| 36 |
+
|
| 37 |
+
gr.Interface(fn=greet, inputs="text", outputs="text").launch()
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
**Blocks** (low-level): flexible layout with explicit event wiring.
|
| 41 |
+
|
| 42 |
+
```python
|
| 43 |
+
import gradio as gr
|
| 44 |
+
|
| 45 |
+
with gr.Blocks() as demo:
|
| 46 |
+
name = gr.Textbox(label="Name")
|
| 47 |
+
output = gr.Textbox(label="Greeting")
|
| 48 |
+
btn = gr.Button("Greet")
|
| 49 |
+
btn.click(fn=lambda n: f"Hello {n}!", inputs=name, outputs=output)
|
| 50 |
+
|
| 51 |
+
demo.launch()
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
**ChatInterface**: high-level wrapper for chatbot UIs.
|
| 55 |
+
|
| 56 |
+
```python
|
| 57 |
+
import gradio as gr
|
| 58 |
+
|
| 59 |
+
def respond(message, history):
|
| 60 |
+
return f"You said: {message}"
|
| 61 |
+
|
| 62 |
+
gr.ChatInterface(fn=respond).launch()
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
## Key Component Signatures
|
| 66 |
+
|
| 67 |
+
### `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)`
|
| 68 |
+
Creates a textarea for user to enter string input or display string output..
|
| 69 |
+
|
| 70 |
+
### `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)`
|
| 71 |
+
Creates a numeric field for user to enter numbers as input or display numeric output..
|
| 72 |
+
|
| 73 |
+
### `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)`
|
| 74 |
+
Creates a slider that ranges from {minimum} to {maximum} with a step size of {step}..
|
| 75 |
+
|
| 76 |
+
### `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)`
|
| 77 |
+
Creates a checkbox that can be set to `True` or `False`.
|
| 78 |
+
|
| 79 |
+
### `Dropdown(choices: Sequence[str | int | float | tuple[str, 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)`
|
| 80 |
+
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)..
|
| 81 |
+
|
| 82 |
+
### `Radio(choices: Sequence[str | int | float | tuple[str, 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)`
|
| 83 |
+
Creates a set of (string or numeric type) radio buttons of which only one can be selected..
|
| 84 |
+
|
| 85 |
+
### `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)`
|
| 86 |
+
Creates an image component that can be used to upload images (as an input) or display images (as an output)..
|
| 87 |
+
|
| 88 |
+
### `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)`
|
| 89 |
+
Creates an audio component that can be used to upload/record audio (as an input) or display audio (as an output)..
|
| 90 |
+
|
| 91 |
+
### `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)`
|
| 92 |
+
Creates a video component that can be used to upload/record videos (as an input) or display videos (as an output).
|
| 93 |
+
|
| 94 |
+
### `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)`
|
| 95 |
+
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).
|
| 96 |
+
|
| 97 |
+
### `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: <class 'inspect._empty'> = 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)`
|
| 98 |
+
Creates a chatbot that displays user-submitted messages and responses.
|
| 99 |
+
|
| 100 |
+
### `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)`
|
| 101 |
+
Creates a button that can be assigned arbitrary .click() events.
|
| 102 |
+
|
| 103 |
+
### `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, 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)`
|
| 104 |
+
Used to render arbitrary Markdown output.
|
| 105 |
+
|
| 106 |
+
### `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, 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, server_functions: list[Callable] | None = None, props: Any)`
|
| 107 |
+
Creates a component with arbitrary HTML.
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
## Custom HTML Components
|
| 111 |
+
|
| 112 |
+
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).
|
| 113 |
+
|
| 114 |
+
Here's an example that shows how to create and use these kinds of components:
|
| 115 |
+
|
| 116 |
+
```python
|
| 117 |
+
import gradio as gr
|
| 118 |
+
|
| 119 |
+
class StarRating(gr.HTML):
|
| 120 |
+
def __init__(self, label, value=0, **kwargs):
|
| 121 |
+
html_template = """
|
| 122 |
+
<h2>${label} rating:</h2>
|
| 123 |
+
${Array.from({length: 5}, (_, i) => `<img class='${i < value ? '' : 'faded'}' src='https://upload.wikimedia.org/wikipedia/commons/d/df/Award-star-gold-3d.svg'>`).join('')}
|
| 124 |
+
"""
|
| 125 |
+
css_template = """
|
| 126 |
+
img { height: 50px; display: inline-block; cursor: pointer; }
|
| 127 |
+
.faded { filter: grayscale(100%); opacity: 0.3; }
|
| 128 |
+
"""
|
| 129 |
+
js_on_load = """
|
| 130 |
+
const imgs = element.querySelectorAll('img');
|
| 131 |
+
imgs.forEach((img, index) => {
|
| 132 |
+
img.addEventListener('click', () => {
|
| 133 |
+
props.value = index + 1;
|
| 134 |
+
});
|
| 135 |
+
});
|
| 136 |
+
"""
|
| 137 |
+
super().__init__(value=value, label=label, html_template=html_template, css_template=css_template, js_on_load=js_on_load, **kwargs)
|
| 138 |
+
|
| 139 |
+
def api_info(self):
|
| 140 |
+
return {"type": "integer", "minimum": 0, "maximum": 5}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
with gr.Blocks() as demo:
|
| 144 |
+
gr.Markdown("# Restaurant Review")
|
| 145 |
+
food_rating = StarRating(label="Food", value=3)
|
| 146 |
+
service_rating = StarRating(label="Service", value=3)
|
| 147 |
+
ambience_rating = StarRating(label="Ambience", value=3)
|
| 148 |
+
average_btn = gr.Button("Calculate Average Rating")
|
| 149 |
+
rating_output = StarRating(label="Average", value=3)
|
| 150 |
+
def calculate_average(food, service, ambience):
|
| 151 |
+
return round((food + service + ambience) / 3)
|
| 152 |
+
average_btn.click(
|
| 153 |
+
fn=calculate_average,
|
| 154 |
+
inputs=[food_rating, service_rating, ambience_rating],
|
| 155 |
+
outputs=rating_output
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
demo.launch()
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
## Event Listeners
|
| 162 |
+
|
| 163 |
+
All event listeners share the same signature:
|
| 164 |
+
|
| 165 |
+
```python
|
| 166 |
+
component.event_name(
|
| 167 |
+
fn: Callable | None | Literal["decorator"] = "decorator",
|
| 168 |
+
inputs: Component | Sequence[Component] | set[Component] | None = None,
|
| 169 |
+
outputs: Component | Sequence[Component] | set[Component] | None = None,
|
| 170 |
+
api_name: str | None = None,
|
| 171 |
+
api_description: str | None | Literal[False] = None,
|
| 172 |
+
scroll_to_output: bool = False,
|
| 173 |
+
show_progress: Literal["full", "minimal", "hidden"] = "full",
|
| 174 |
+
show_progress_on: Component | Sequence[Component] | None = None,
|
| 175 |
+
queue: bool = True,
|
| 176 |
+
batch: bool = False,
|
| 177 |
+
max_batch_size: int = 4,
|
| 178 |
+
preprocess: bool = True,
|
| 179 |
+
postprocess: bool = True,
|
| 180 |
+
cancels: dict[str, Any] | list[dict[str, Any]] | None = None,
|
| 181 |
+
trigger_mode: Literal["once", "multiple", "always_last"] | None = None,
|
| 182 |
+
js: str | Literal[True] | None = None,
|
| 183 |
+
concurrency_limit: int | None | Literal["default"] = "default",
|
| 184 |
+
concurrency_id: str | None = None,
|
| 185 |
+
api_visibility: Literal["public", "private", "undocumented"] = "public",
|
| 186 |
+
time_limit: int | None = None,
|
| 187 |
+
stream_every: float = 0.5,
|
| 188 |
+
key: int | str | tuple[int | str, ...] | None = None,
|
| 189 |
+
validator: Callable | None = None,
|
| 190 |
+
) -> Dependency
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
Supported events per component:
|
| 194 |
+
|
| 195 |
+
- **AnnotatedImage**: select
|
| 196 |
+
- **Audio**: stream, change, clear, play, pause, stop, pause, start_recording, pause_recording, stop_recording, upload, input
|
| 197 |
+
- **BarPlot**: select, double_click
|
| 198 |
+
- **BrowserState**: change
|
| 199 |
+
- **Button**: click
|
| 200 |
+
- **Chatbot**: change, select, like, retry, undo, example_select, option_select, clear, copy, edit
|
| 201 |
+
- **Checkbox**: change, input, select
|
| 202 |
+
- **CheckboxGroup**: change, input, select
|
| 203 |
+
- **ClearButton**: click
|
| 204 |
+
- **Code**: change, input, focus, blur
|
| 205 |
+
- **ColorPicker**: change, input, submit, focus, blur
|
| 206 |
+
- **Dataframe**: change, input, select, edit
|
| 207 |
+
- **Dataset**: click, select
|
| 208 |
+
- **DateTime**: change, submit
|
| 209 |
+
- **DeepLinkButton**: click
|
| 210 |
+
- **Dialogue**: change, input, submit
|
| 211 |
+
- **DownloadButton**: click
|
| 212 |
+
- **Dropdown**: change, input, select, focus, blur, key_up
|
| 213 |
+
- **DuplicateButton**: click
|
| 214 |
+
- **File**: change, select, clear, upload, delete, download
|
| 215 |
+
- **FileExplorer**: change, input, select
|
| 216 |
+
- **Gallery**: select, upload, change, delete, preview_close, preview_open
|
| 217 |
+
- **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
|
| 218 |
+
- **HighlightedText**: change, select
|
| 219 |
+
- **Image**: clear, change, stream, select, upload, input
|
| 220 |
+
- **ImageEditor**: clear, change, input, select, upload, apply
|
| 221 |
+
- **ImageSlider**: clear, change, stream, select, upload, input
|
| 222 |
+
- **JSON**: change
|
| 223 |
+
- **Label**: change, select
|
| 224 |
+
- **LinePlot**: select, double_click
|
| 225 |
+
- **LoginButton**: click
|
| 226 |
+
- **Markdown**: change, copy
|
| 227 |
+
- **Model3D**: change, upload, edit, clear
|
| 228 |
+
- **MultimodalTextbox**: change, input, select, submit, focus, blur, stop
|
| 229 |
+
- **Navbar**: change
|
| 230 |
+
- **Number**: change, input, submit, focus, blur
|
| 231 |
+
- **ParamViewer**: change, upload
|
| 232 |
+
- **Plot**: change
|
| 233 |
+
- **Radio**: select, change, input
|
| 234 |
+
- **ScatterPlot**: select, double_click
|
| 235 |
+
- **SimpleImage**: clear, change, upload
|
| 236 |
+
- **Slider**: change, input, release
|
| 237 |
+
- **State**: change
|
| 238 |
+
- **Textbox**: change, input, select, submit, focus, blur, stop, copy
|
| 239 |
+
- **Timer**: tick
|
| 240 |
+
- **UploadButton**: click, upload
|
| 241 |
+
- **Video**: change, clear, start_recording, stop_recording, stop, play, pause, end, upload, input
|
| 242 |
+
|
| 243 |
+
## Prediction CLI
|
| 244 |
+
|
| 245 |
+
The `gradio` CLI includes `info` and `predict` commands for interacting with Gradio apps programmatically. These are especially useful for coding agents that need to use Spaces in their workflows.
|
| 246 |
+
|
| 247 |
+
### `gradio info` — Discover endpoints and parameters
|
| 248 |
+
|
| 249 |
+
```bash
|
| 250 |
+
gradio info <space_id_or_url>
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
Returns a JSON payload describing all endpoints, their parameters (with types and defaults), and return values.
|
| 254 |
+
|
| 255 |
+
```bash
|
| 256 |
+
gradio info gradio/calculator
|
| 257 |
+
# {
|
| 258 |
+
# "/predict": {
|
| 259 |
+
# "parameters": [
|
| 260 |
+
# {"name": "num1", "required": true, "default": null, "type": {"type": "number"}},
|
| 261 |
+
# {"name": "operation", "required": true, "default": null, "type": {"enum": ["add", "subtract", "multiply", "divide"], "type": "string"}},
|
| 262 |
+
# {"name": "num2", "required": true, "default": null, "type": {"type": "number"}}
|
| 263 |
+
# ],
|
| 264 |
+
# "returns": [{"name": "output", "type": {"type": "number"}}],
|
| 265 |
+
# "description": ""
|
| 266 |
+
# }
|
| 267 |
+
# }
|
| 268 |
+
```
|
| 269 |
+
|
| 270 |
+
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.
|
| 271 |
+
|
| 272 |
+
### `gradio predict` — Send predictions
|
| 273 |
+
|
| 274 |
+
```bash
|
| 275 |
+
gradio predict <space_id_or_url> <endpoint> <json_payload>
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
Returns a JSON object with named output keys.
|
| 279 |
+
|
| 280 |
+
```bash
|
| 281 |
+
# Simple numeric prediction
|
| 282 |
+
gradio predict gradio/calculator /predict '{"num1": 5, "operation": "multiply", "num2": 3}'
|
| 283 |
+
# {"output": 15}
|
| 284 |
+
|
| 285 |
+
# Image generation
|
| 286 |
+
gradio predict black-forest-labs/FLUX.2-dev /infer '{"prompt": "A majestic dragon"}'
|
| 287 |
+
# {"Result": "/tmp/gradio/.../image.webp", "Seed": 1117868604}
|
| 288 |
+
|
| 289 |
+
# File upload (must include meta key)
|
| 290 |
+
gradio predict gradio/image_mod /predict '{"image": {"path": "/path/to/image.png", "meta": {"_type": "gradio.FileData"}}}'
|
| 291 |
+
# {"output": "/tmp/gradio/.../output.png"}
|
| 292 |
+
```
|
| 293 |
+
|
| 294 |
+
Both commands accept `--token` for accessing private Spaces.
|
| 295 |
+
|
| 296 |
+
## Additional Reference
|
| 297 |
+
|
| 298 |
+
- [End-to-End Examples](examples.md) — complete working apps
|
.claude/skills/huggingface-gradio/examples.md
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gradio End-to-End Examples
|
| 2 |
+
|
| 3 |
+
Complete working Gradio apps for reference.
|
| 4 |
+
|
| 5 |
+
## Blocks Essay Simple
|
| 6 |
+
|
| 7 |
+
```python
|
| 8 |
+
import gradio as gr
|
| 9 |
+
|
| 10 |
+
def change_textbox(choice):
|
| 11 |
+
if choice == "short":
|
| 12 |
+
return gr.Textbox(lines=2, visible=True)
|
| 13 |
+
elif choice == "long":
|
| 14 |
+
return gr.Textbox(lines=8, visible=True, value="Lorem ipsum dolor sit amet")
|
| 15 |
+
else:
|
| 16 |
+
return gr.Textbox(visible=False)
|
| 17 |
+
|
| 18 |
+
with gr.Blocks() as demo:
|
| 19 |
+
radio = gr.Radio(
|
| 20 |
+
["short", "long", "none"], label="What kind of essay would you like to write?"
|
| 21 |
+
)
|
| 22 |
+
text = gr.Textbox(lines=2, interactive=True, buttons=["copy"])
|
| 23 |
+
radio.change(fn=change_textbox, inputs=radio, outputs=text)
|
| 24 |
+
|
| 25 |
+
demo.launch()
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## Blocks Flipper
|
| 29 |
+
|
| 30 |
+
```python
|
| 31 |
+
import numpy as np
|
| 32 |
+
import gradio as gr
|
| 33 |
+
|
| 34 |
+
def flip_text(x):
|
| 35 |
+
return x[::-1]
|
| 36 |
+
|
| 37 |
+
def flip_image(x):
|
| 38 |
+
return np.fliplr(x)
|
| 39 |
+
|
| 40 |
+
with gr.Blocks() as demo:
|
| 41 |
+
gr.Markdown("Flip text or image files using this demo.")
|
| 42 |
+
with gr.Tab("Flip Text"):
|
| 43 |
+
text_input = gr.Textbox()
|
| 44 |
+
text_output = gr.Textbox()
|
| 45 |
+
text_button = gr.Button("Flip")
|
| 46 |
+
with gr.Tab("Flip Image"):
|
| 47 |
+
with gr.Row():
|
| 48 |
+
image_input = gr.Image()
|
| 49 |
+
image_output = gr.Image()
|
| 50 |
+
image_button = gr.Button("Flip")
|
| 51 |
+
|
| 52 |
+
with gr.Accordion("Open for More!", open=False):
|
| 53 |
+
gr.Markdown("Look at me...")
|
| 54 |
+
temp_slider = gr.Slider(
|
| 55 |
+
0, 1,
|
| 56 |
+
value=0.1,
|
| 57 |
+
step=0.1,
|
| 58 |
+
interactive=True,
|
| 59 |
+
label="Slide me",
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
text_button.click(flip_text, inputs=text_input, outputs=text_output)
|
| 63 |
+
image_button.click(flip_image, inputs=image_input, outputs=image_output)
|
| 64 |
+
|
| 65 |
+
demo.launch()
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
## Blocks Form
|
| 69 |
+
|
| 70 |
+
```python
|
| 71 |
+
import gradio as gr
|
| 72 |
+
|
| 73 |
+
with gr.Blocks() as demo:
|
| 74 |
+
name_box = gr.Textbox(label="Name")
|
| 75 |
+
age_box = gr.Number(label="Age", minimum=0, maximum=100)
|
| 76 |
+
symptoms_box = gr.CheckboxGroup(["Cough", "Fever", "Runny Nose"])
|
| 77 |
+
submit_btn = gr.Button("Submit")
|
| 78 |
+
|
| 79 |
+
with gr.Column(visible=False) as output_col:
|
| 80 |
+
diagnosis_box = gr.Textbox(label="Diagnosis")
|
| 81 |
+
patient_summary_box = gr.Textbox(label="Patient Summary")
|
| 82 |
+
|
| 83 |
+
def submit(name, age, symptoms):
|
| 84 |
+
return {
|
| 85 |
+
submit_btn: gr.Button(visible=False),
|
| 86 |
+
output_col: gr.Column(visible=True),
|
| 87 |
+
diagnosis_box: "covid" if "Cough" in symptoms else "flu",
|
| 88 |
+
patient_summary_box: f"{name}, {age} y/o",
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
submit_btn.click(
|
| 92 |
+
submit,
|
| 93 |
+
[name_box, age_box, symptoms_box],
|
| 94 |
+
[submit_btn, diagnosis_box, patient_summary_box, output_col],
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
demo.launch()
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
## Blocks Hello
|
| 101 |
+
|
| 102 |
+
```python
|
| 103 |
+
import gradio as gr
|
| 104 |
+
|
| 105 |
+
def welcome(name):
|
| 106 |
+
return f"Welcome to Gradio, {name}!"
|
| 107 |
+
|
| 108 |
+
with gr.Blocks() as demo:
|
| 109 |
+
gr.Markdown(
|
| 110 |
+
"""
|
| 111 |
+
# Hello World!
|
| 112 |
+
Start typing below to see the output.
|
| 113 |
+
""")
|
| 114 |
+
inp = gr.Textbox(placeholder="What is your name?")
|
| 115 |
+
out = gr.Textbox()
|
| 116 |
+
inp.change(welcome, inp, out)
|
| 117 |
+
|
| 118 |
+
demo.launch()
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
## Blocks Layout
|
| 122 |
+
|
| 123 |
+
```python
|
| 124 |
+
import gradio as gr
|
| 125 |
+
|
| 126 |
+
demo = gr.Blocks()
|
| 127 |
+
|
| 128 |
+
with demo:
|
| 129 |
+
with gr.Row():
|
| 130 |
+
gr.Image(interactive=True, scale=2)
|
| 131 |
+
gr.Image()
|
| 132 |
+
with gr.Row():
|
| 133 |
+
gr.Textbox(label="Text")
|
| 134 |
+
gr.Number(label="Count", scale=2)
|
| 135 |
+
gr.Radio(choices=["One", "Two"])
|
| 136 |
+
with gr.Row():
|
| 137 |
+
gr.Button("500", scale=0, min_width=500)
|
| 138 |
+
gr.Button("A", scale=0)
|
| 139 |
+
gr.Button("grow")
|
| 140 |
+
with gr.Row():
|
| 141 |
+
gr.Textbox()
|
| 142 |
+
gr.Textbox()
|
| 143 |
+
gr.Button()
|
| 144 |
+
with gr.Row():
|
| 145 |
+
with gr.Row():
|
| 146 |
+
with gr.Column():
|
| 147 |
+
gr.Textbox(label="Text")
|
| 148 |
+
gr.Number(label="Count")
|
| 149 |
+
gr.Radio(choices=["One", "Two"])
|
| 150 |
+
gr.Image()
|
| 151 |
+
with gr.Column():
|
| 152 |
+
gr.Image(interactive=True)
|
| 153 |
+
gr.Image()
|
| 154 |
+
gr.Image()
|
| 155 |
+
gr.Textbox(label="Text")
|
| 156 |
+
gr.Number(label="Count")
|
| 157 |
+
gr.Radio(choices=["One", "Two"])
|
| 158 |
+
|
| 159 |
+
demo.launch()
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
## Calculator
|
| 163 |
+
|
| 164 |
+
```python
|
| 165 |
+
import gradio as gr
|
| 166 |
+
|
| 167 |
+
def calculator(num1, operation, num2):
|
| 168 |
+
if operation == "add":
|
| 169 |
+
return num1 + num2
|
| 170 |
+
elif operation == "subtract":
|
| 171 |
+
return num1 - num2
|
| 172 |
+
elif operation == "multiply":
|
| 173 |
+
return num1 * num2
|
| 174 |
+
elif operation == "divide":
|
| 175 |
+
if num2 == 0:
|
| 176 |
+
raise gr.Error("Cannot divide by zero!")
|
| 177 |
+
return num1 / num2
|
| 178 |
+
|
| 179 |
+
demo = gr.Interface(
|
| 180 |
+
calculator,
|
| 181 |
+
[
|
| 182 |
+
"number",
|
| 183 |
+
gr.Radio(["add", "subtract", "multiply", "divide"]),
|
| 184 |
+
"number"
|
| 185 |
+
],
|
| 186 |
+
"number",
|
| 187 |
+
examples=[
|
| 188 |
+
[45, "add", 3],
|
| 189 |
+
[3.14, "divide", 2],
|
| 190 |
+
[144, "multiply", 2.5],
|
| 191 |
+
[0, "subtract", 1.2],
|
| 192 |
+
],
|
| 193 |
+
title="Toy Calculator",
|
| 194 |
+
description="Here's a sample toy calculator.",
|
| 195 |
+
api_name="predict"
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
demo.launch()
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
## Chatbot Simple
|
| 202 |
+
|
| 203 |
+
```python
|
| 204 |
+
import gradio as gr
|
| 205 |
+
import random
|
| 206 |
+
import time
|
| 207 |
+
|
| 208 |
+
with gr.Blocks() as demo:
|
| 209 |
+
chatbot = gr.Chatbot()
|
| 210 |
+
msg = gr.Textbox()
|
| 211 |
+
clear = gr.ClearButton([msg, chatbot])
|
| 212 |
+
|
| 213 |
+
def respond(message, chat_history):
|
| 214 |
+
bot_message = random.choice(["How are you?", "Today is a great day", "I'm very hungry"])
|
| 215 |
+
chat_history.append({"role": "user", "content": message})
|
| 216 |
+
chat_history.append({"role": "assistant", "content": bot_message})
|
| 217 |
+
time.sleep(2)
|
| 218 |
+
return "", chat_history
|
| 219 |
+
|
| 220 |
+
msg.submit(respond, [msg, chatbot], [msg, chatbot])
|
| 221 |
+
|
| 222 |
+
demo.launch()
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
## Chatbot Streaming
|
| 226 |
+
|
| 227 |
+
```python
|
| 228 |
+
import gradio as gr
|
| 229 |
+
import random
|
| 230 |
+
import time
|
| 231 |
+
|
| 232 |
+
with gr.Blocks() as demo:
|
| 233 |
+
chatbot = gr.Chatbot()
|
| 234 |
+
msg = gr.Textbox()
|
| 235 |
+
clear = gr.Button("Clear")
|
| 236 |
+
|
| 237 |
+
def user(user_message, history: list):
|
| 238 |
+
return "", history + [{"role": "user", "content": user_message}]
|
| 239 |
+
|
| 240 |
+
def bot(history: list):
|
| 241 |
+
bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"])
|
| 242 |
+
history.append({"role": "assistant", "content": ""})
|
| 243 |
+
for character in bot_message:
|
| 244 |
+
history[-1]['content'] += character
|
| 245 |
+
time.sleep(0.05)
|
| 246 |
+
yield history
|
| 247 |
+
|
| 248 |
+
msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
|
| 249 |
+
bot, chatbot, chatbot
|
| 250 |
+
)
|
| 251 |
+
clear.click(lambda: None, None, chatbot, queue=False)
|
| 252 |
+
|
| 253 |
+
demo.launch()
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
+
## Custom Css
|
| 257 |
+
|
| 258 |
+
```python
|
| 259 |
+
import gradio as gr
|
| 260 |
+
|
| 261 |
+
with gr.Blocks() as demo:
|
| 262 |
+
with gr.Column(elem_classes="cool-col"):
|
| 263 |
+
gr.Markdown("### Gradio Demo with Custom CSS", elem_classes="darktest")
|
| 264 |
+
gr.Markdown(
|
| 265 |
+
elem_classes="markdown",
|
| 266 |
+
value="Resize the browser window to see the CSS media query in action.",
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
if __name__ == "__main__":
|
| 270 |
+
demo.launch(css_paths=["demo/custom_css/custom_css.css"])
|
| 271 |
+
```
|
| 272 |
+
|
| 273 |
+
## Fake Diffusion
|
| 274 |
+
|
| 275 |
+
```python
|
| 276 |
+
import gradio as gr
|
| 277 |
+
import numpy as np
|
| 278 |
+
import time
|
| 279 |
+
|
| 280 |
+
def fake_diffusion(steps):
|
| 281 |
+
rng = np.random.default_rng()
|
| 282 |
+
for i in range(steps):
|
| 283 |
+
time.sleep(1)
|
| 284 |
+
image = rng.random(size=(600, 600, 3))
|
| 285 |
+
yield image
|
| 286 |
+
image = np.ones((1000,1000,3), np.uint8)
|
| 287 |
+
image[:] = [255, 124, 0]
|
| 288 |
+
yield image
|
| 289 |
+
|
| 290 |
+
demo = gr.Interface(fake_diffusion,
|
| 291 |
+
inputs=gr.Slider(1, 10, 3, step=1),
|
| 292 |
+
outputs="image",
|
| 293 |
+
api_name="predict")
|
| 294 |
+
|
| 295 |
+
demo.launch()
|
| 296 |
+
```
|
| 297 |
+
|
| 298 |
+
## Hello World
|
| 299 |
+
|
| 300 |
+
```python
|
| 301 |
+
import gradio as gr
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def greet(name):
|
| 305 |
+
return "Hello " + name + "!"
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
demo = gr.Interface(fn=greet, inputs="textbox", outputs="textbox", api_name="predict")
|
| 309 |
+
|
| 310 |
+
demo.launch()
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
## Image Editor
|
| 314 |
+
|
| 315 |
+
```python
|
| 316 |
+
import gradio as gr
|
| 317 |
+
import time
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def sleep(im):
|
| 321 |
+
time.sleep(5)
|
| 322 |
+
return [im["background"], im["layers"][0], im["layers"][1], im["composite"]]
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def predict(im):
|
| 326 |
+
return im["composite"]
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
with gr.Blocks() as demo:
|
| 330 |
+
with gr.Row():
|
| 331 |
+
im = gr.ImageEditor(
|
| 332 |
+
type="numpy",
|
| 333 |
+
)
|
| 334 |
+
im_preview = gr.Image()
|
| 335 |
+
n_upload = gr.Number(0, label="Number of upload events", step=1)
|
| 336 |
+
n_change = gr.Number(0, label="Number of change events", step=1)
|
| 337 |
+
n_input = gr.Number(0, label="Number of input events", step=1)
|
| 338 |
+
|
| 339 |
+
im.upload(lambda x: x + 1, outputs=n_upload, inputs=n_upload)
|
| 340 |
+
im.change(lambda x: x + 1, outputs=n_change, inputs=n_change)
|
| 341 |
+
im.input(lambda x: x + 1, outputs=n_input, inputs=n_input)
|
| 342 |
+
im.change(predict, outputs=im_preview, inputs=im, show_progress="hidden")
|
| 343 |
+
|
| 344 |
+
demo.launch()
|
| 345 |
+
```
|
| 346 |
+
|
| 347 |
+
## On Listener Decorator
|
| 348 |
+
|
| 349 |
+
```python
|
| 350 |
+
import gradio as gr
|
| 351 |
+
|
| 352 |
+
with gr.Blocks() as demo:
|
| 353 |
+
name = gr.Textbox(label="Name")
|
| 354 |
+
output = gr.Textbox(label="Output Box")
|
| 355 |
+
greet_btn = gr.Button("Greet")
|
| 356 |
+
|
| 357 |
+
@gr.on(triggers=[name.submit, greet_btn.click], inputs=name, outputs=output)
|
| 358 |
+
def greet(name):
|
| 359 |
+
return "Hello " + name + "!"
|
| 360 |
+
|
| 361 |
+
demo.launch()
|
| 362 |
+
```
|
| 363 |
+
|
| 364 |
+
## Render Merge
|
| 365 |
+
|
| 366 |
+
```python
|
| 367 |
+
import gradio as gr
|
| 368 |
+
import time
|
| 369 |
+
|
| 370 |
+
with gr.Blocks() as demo:
|
| 371 |
+
text_count = gr.Slider(1, 5, value=1, step=1, label="Textbox Count")
|
| 372 |
+
|
| 373 |
+
@gr.render(inputs=text_count)
|
| 374 |
+
def render_count(count):
|
| 375 |
+
boxes = []
|
| 376 |
+
for i in range(count):
|
| 377 |
+
box = gr.Textbox(label=f"Box {i}")
|
| 378 |
+
boxes.append(box)
|
| 379 |
+
|
| 380 |
+
def merge(*args):
|
| 381 |
+
time.sleep(0.2) # simulate a delay
|
| 382 |
+
return " ".join(args)
|
| 383 |
+
|
| 384 |
+
merge_btn.click(merge, boxes, output)
|
| 385 |
+
|
| 386 |
+
def clear():
|
| 387 |
+
time.sleep(0.2) # simulate a delay
|
| 388 |
+
return [" "] * count
|
| 389 |
+
|
| 390 |
+
clear_btn.click(clear, None, boxes)
|
| 391 |
+
|
| 392 |
+
def countup():
|
| 393 |
+
time.sleep(0.2) # simulate a delay
|
| 394 |
+
return list(range(count))
|
| 395 |
+
|
| 396 |
+
count_btn.click(countup, None, boxes, queue=False)
|
| 397 |
+
|
| 398 |
+
with gr.Row():
|
| 399 |
+
merge_btn = gr.Button("Merge")
|
| 400 |
+
clear_btn = gr.Button("Clear")
|
| 401 |
+
count_btn = gr.Button("Count")
|
| 402 |
+
|
| 403 |
+
output = gr.Textbox()
|
| 404 |
+
|
| 405 |
+
demo.launch()
|
| 406 |
+
```
|
| 407 |
+
|
| 408 |
+
## Reverse Audio 2
|
| 409 |
+
|
| 410 |
+
```python
|
| 411 |
+
import gradio as gr
|
| 412 |
+
import numpy as np
|
| 413 |
+
|
| 414 |
+
def reverse_audio(audio):
|
| 415 |
+
sr, data = audio
|
| 416 |
+
return (sr, np.flipud(data))
|
| 417 |
+
|
| 418 |
+
demo = gr.Interface(fn=reverse_audio,
|
| 419 |
+
inputs="microphone",
|
| 420 |
+
outputs="audio", api_name="predict")
|
| 421 |
+
|
| 422 |
+
demo.launch()
|
| 423 |
+
```
|
| 424 |
+
|
| 425 |
+
## Sepia Filter
|
| 426 |
+
|
| 427 |
+
```python
|
| 428 |
+
import numpy as np
|
| 429 |
+
import gradio as gr
|
| 430 |
+
|
| 431 |
+
def sepia(input_img):
|
| 432 |
+
sepia_filter = np.array([
|
| 433 |
+
[0.393, 0.769, 0.189],
|
| 434 |
+
[0.349, 0.686, 0.168],
|
| 435 |
+
[0.272, 0.534, 0.131]
|
| 436 |
+
])
|
| 437 |
+
sepia_img = input_img.dot(sepia_filter.T)
|
| 438 |
+
sepia_img /= sepia_img.max()
|
| 439 |
+
return sepia_img
|
| 440 |
+
|
| 441 |
+
demo = gr.Interface(sepia, gr.Image(), "image", api_name="predict")
|
| 442 |
+
demo.launch()
|
| 443 |
+
```
|
| 444 |
+
|
| 445 |
+
## Sort Records
|
| 446 |
+
|
| 447 |
+
```python
|
| 448 |
+
import gradio as gr
|
| 449 |
+
|
| 450 |
+
def sort_records(records):
|
| 451 |
+
return records.sort("Quantity")
|
| 452 |
+
|
| 453 |
+
demo = gr.Interface(
|
| 454 |
+
sort_records,
|
| 455 |
+
gr.Dataframe(
|
| 456 |
+
headers=["Item", "Quantity"],
|
| 457 |
+
datatype=["str", "number"],
|
| 458 |
+
row_count=3,
|
| 459 |
+
column_count=2,
|
| 460 |
+
column_limits=(2, 2),
|
| 461 |
+
type="polars"
|
| 462 |
+
),
|
| 463 |
+
"dataframe",
|
| 464 |
+
description="Sort by Quantity"
|
| 465 |
+
)
|
| 466 |
+
|
| 467 |
+
demo.launch()
|
| 468 |
+
```
|
| 469 |
+
|
| 470 |
+
## Streaming Simple
|
| 471 |
+
|
| 472 |
+
```python
|
| 473 |
+
import gradio as gr
|
| 474 |
+
|
| 475 |
+
with gr.Blocks() as demo:
|
| 476 |
+
with gr.Row():
|
| 477 |
+
with gr.Column():
|
| 478 |
+
input_img = gr.Image(label="Input", sources="webcam")
|
| 479 |
+
with gr.Column():
|
| 480 |
+
output_img = gr.Image(label="Output")
|
| 481 |
+
input_img.stream(lambda s: s, input_img, output_img, time_limit=15, stream_every=0.1, concurrency_limit=30)
|
| 482 |
+
|
| 483 |
+
if __name__ == "__main__":
|
| 484 |
+
|
| 485 |
+
demo.launch()
|
| 486 |
+
```
|
| 487 |
+
|
| 488 |
+
## Tabbed Interface Lite
|
| 489 |
+
|
| 490 |
+
```python
|
| 491 |
+
import gradio as gr
|
| 492 |
+
|
| 493 |
+
hello_world = gr.Interface(lambda name: "Hello " + name, "text", "text", api_name="predict")
|
| 494 |
+
bye_world = gr.Interface(lambda name: "Bye " + name, "text", "text", api_name="predict")
|
| 495 |
+
chat = gr.ChatInterface(lambda *args: "Hello " + args[0], api_name="chat")
|
| 496 |
+
|
| 497 |
+
demo = gr.TabbedInterface([hello_world, bye_world, chat], ["Hello World", "Bye World", "Chat"])
|
| 498 |
+
|
| 499 |
+
demo.launch()
|
| 500 |
+
```
|
| 501 |
+
|
| 502 |
+
## Tax Calculator
|
| 503 |
+
|
| 504 |
+
```python
|
| 505 |
+
import gradio as gr
|
| 506 |
+
|
| 507 |
+
def tax_calculator(income, marital_status, assets):
|
| 508 |
+
tax_brackets = [(10, 0), (25, 8), (60, 12), (120, 20), (250, 30)]
|
| 509 |
+
total_deductible = sum(cost for cost, deductible in zip(assets["Cost"], assets["Deductible"]) if deductible)
|
| 510 |
+
taxable_income = income - total_deductible
|
| 511 |
+
|
| 512 |
+
total_tax = 0
|
| 513 |
+
for bracket, rate in tax_brackets:
|
| 514 |
+
if taxable_income > bracket:
|
| 515 |
+
total_tax += (taxable_income - bracket) * rate / 100
|
| 516 |
+
|
| 517 |
+
if marital_status == "Married":
|
| 518 |
+
total_tax *= 0.75
|
| 519 |
+
elif marital_status == "Divorced":
|
| 520 |
+
total_tax *= 0.8
|
| 521 |
+
|
| 522 |
+
return round(total_tax)
|
| 523 |
+
|
| 524 |
+
demo = gr.Interface(
|
| 525 |
+
tax_calculator,
|
| 526 |
+
[
|
| 527 |
+
"number",
|
| 528 |
+
gr.Radio(["Single", "Married", "Divorced"]),
|
| 529 |
+
gr.Dataframe(
|
| 530 |
+
headers=["Item", "Cost", "Deductible"],
|
| 531 |
+
datatype=["str", "number", "bool"],
|
| 532 |
+
label="Assets Purchased this Year",
|
| 533 |
+
),
|
| 534 |
+
],
|
| 535 |
+
gr.Number(label="Tax due"),
|
| 536 |
+
examples=[
|
| 537 |
+
[10000, "Married", [["Suit", 5000, True], ["Laptop (for work)", 800, False], ["Car", 1800, True]]],
|
| 538 |
+
[80000, "Single", [["Suit", 800, True], ["Watch", 1800, True], ["Food", 800, True]]],
|
| 539 |
+
],
|
| 540 |
+
live=True,
|
| 541 |
+
api_name="predict"
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
demo.launch()
|
| 545 |
+
```
|
| 546 |
+
|
| 547 |
+
## Timer Simple
|
| 548 |
+
|
| 549 |
+
```python
|
| 550 |
+
import gradio as gr
|
| 551 |
+
import random
|
| 552 |
+
import time
|
| 553 |
+
|
| 554 |
+
with gr.Blocks() as demo:
|
| 555 |
+
timer = gr.Timer(1)
|
| 556 |
+
timestamp = gr.Number(label="Time")
|
| 557 |
+
timer.tick(lambda: round(time.time()), outputs=timestamp, api_name="timestamp")
|
| 558 |
+
|
| 559 |
+
number = gr.Number(lambda: random.randint(1, 10), every=timer, label="Random Number")
|
| 560 |
+
with gr.Row():
|
| 561 |
+
gr.Button("Start").click(lambda: gr.Timer(active=True), None, timer)
|
| 562 |
+
gr.Button("Stop").click(lambda: gr.Timer(active=False), None, timer)
|
| 563 |
+
gr.Button("Go Fast").click(lambda: 0.2, None, timer)
|
| 564 |
+
|
| 565 |
+
if __name__ == "__main__":
|
| 566 |
+
demo.launch()
|
| 567 |
+
```
|
| 568 |
+
|
| 569 |
+
## Variable Outputs
|
| 570 |
+
|
| 571 |
+
```python
|
| 572 |
+
import gradio as gr
|
| 573 |
+
|
| 574 |
+
max_textboxes = 10
|
| 575 |
+
|
| 576 |
+
def variable_outputs(k):
|
| 577 |
+
k = int(k)
|
| 578 |
+
return [gr.Textbox(visible=True)]*k + [gr.Textbox(visible=False)]*(max_textboxes-k)
|
| 579 |
+
|
| 580 |
+
with gr.Blocks() as demo:
|
| 581 |
+
s = gr.Slider(1, max_textboxes, value=max_textboxes, step=1, label="How many textboxes to show:")
|
| 582 |
+
textboxes = []
|
| 583 |
+
for i in range(max_textboxes):
|
| 584 |
+
t = gr.Textbox(f"Textbox {i}")
|
| 585 |
+
textboxes.append(t)
|
| 586 |
+
|
| 587 |
+
s.change(variable_outputs, s, textboxes)
|
| 588 |
+
|
| 589 |
+
if __name__ == "__main__":
|
| 590 |
+
demo.launch()
|
| 591 |
+
```
|
| 592 |
+
|
| 593 |
+
## Video Identity
|
| 594 |
+
|
| 595 |
+
```python
|
| 596 |
+
import gradio as gr
|
| 597 |
+
from gradio.media import get_video
|
| 598 |
+
|
| 599 |
+
def video_identity(video):
|
| 600 |
+
return video
|
| 601 |
+
|
| 602 |
+
# get_video() returns file paths to sample media included with Gradio
|
| 603 |
+
demo = gr.Interface(video_identity,
|
| 604 |
+
gr.Video(),
|
| 605 |
+
"playable_video",
|
| 606 |
+
examples=[
|
| 607 |
+
get_video("world.mp4")
|
| 608 |
+
],
|
| 609 |
+
cache_examples=True,
|
| 610 |
+
api_name="predict",)
|
| 611 |
+
|
| 612 |
+
demo.launch()
|
| 613 |
+
```
|
.gitattributes
CHANGED
|
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/voice_caught_0.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/voice_caught_1.wav filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
assets/voice_caught_2.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
assets/voice_escaped_0.wav filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
assets/voice_escaped_1.wav filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
assets/voice_escaped_2.wav filter=lfs diff=lfs merge=lfs -text
|
app.py
CHANGED
|
@@ -5,8 +5,8 @@ Game loop: case intro -> timed glimpse -> testimony -> sketch reveal -> lineup
|
|
| 5 |
grow the lineup; from INSPECTOR the culprit changes one feature before the
|
| 6 |
lineup ("he's been to a barber since").
|
| 7 |
|
| 8 |
-
UI architecture: a single state-driven @gr.render stage
|
| 9 |
-
|
| 10 |
"""
|
| 11 |
from __future__ import annotations
|
| 12 |
|
|
@@ -46,16 +46,7 @@ def svg_uri(svg: str) -> str:
|
|
| 46 |
return "data:image/svg+xml;base64," + base64.b64encode(svg.encode()).decode()
|
| 47 |
|
| 48 |
|
| 49 |
-
|
| 50 |
-
"""Rasterize a face for components that need real images (gr.Gallery)."""
|
| 51 |
-
import io
|
| 52 |
-
|
| 53 |
-
import cairosvg
|
| 54 |
-
from PIL import Image
|
| 55 |
-
|
| 56 |
-
png = cairosvg.svg2png(bytestring=render_face_svg(spec, width=width).encode(),
|
| 57 |
-
output_width=width)
|
| 58 |
-
return Image.open(io.BytesIO(png)).convert("RGB")
|
| 59 |
|
| 60 |
|
| 61 |
def glimpse_html(case: Case) -> str:
|
|
@@ -151,21 +142,32 @@ def next_case(s: dict) -> dict:
|
|
| 151 |
s["case_no"] = min(s["case_no"] + 1, len(RANKS))
|
| 152 |
s["case"] = make_case(s["case_no"])
|
| 153 |
s["screen"] = "intro"
|
|
|
|
|
|
|
| 154 |
return s
|
| 155 |
|
| 156 |
|
| 157 |
# ------------------------------------------------------------------ UI
|
| 158 |
CSS = """
|
|
|
|
| 159 |
:root { --paper:#f4efe4; --ink:#2b2a28; --red:#a33327; --tape:#c9a227; }
|
| 160 |
-
.gradio-container { background:#191713 !important; font-family:'Courier New',monospace !important;
|
|
|
|
| 161 |
#ew-root { max-width: 900px; margin: 0 auto; }
|
| 162 |
-
.ew-header { text-align:center; color:var(--paper); letter-spacing:
|
|
|
|
| 163 |
.ew-sub { text-align:center; color:#8d8678; font-size:12px; letter-spacing:2px; margin-bottom:8px; }
|
| 164 |
.ew-rank { font-family:monospace; color:var(--tape); text-align:center; letter-spacing:2px; font-size:13px; }
|
| 165 |
.ew-card { background:var(--paper) !important; border-radius:4px; padding:22px 26px !important; color:var(--ink);
|
| 166 |
box-shadow: 0 10px 40px rgba(0,0,0,.5); }
|
| 167 |
.ew-card * { color: var(--ink); }
|
| 168 |
.ew-glimpse { position:relative; width:320px; margin:0 auto; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
.ew-glimpse img { width:100%; filter: blur(0); }
|
| 170 |
@keyframes ew-blurout { to { filter: blur(30px) contrast(0.4); } }
|
| 171 |
.ew-static { position:absolute; inset:0 0 13px 0; display:flex; align-items:center; justify-content:center;
|
|
@@ -182,7 +184,10 @@ CSS = """
|
|
| 182 |
.ew-hit td { color:#1d6b2f !important; } .ew-miss td { color:var(--red) !important; } .ew-silent td { color:#8d8678 !important; }
|
| 183 |
.ew-verdict { text-align:center; }
|
| 184 |
.ew-stamp { display:inline-block; border:4px solid var(--red); color:var(--red); padding:6px 22px;
|
| 185 |
-
font-size:26px; letter-spacing:4px; transform:rotate(-6deg); margin:6px 0 10px;
|
|
|
|
|
|
|
|
|
|
| 186 |
.ew-good .ew-stamp { border-color:#1d6b2f; color:#1d6b2f; }
|
| 187 |
.ew-quote { font-style:italic; margin-bottom:12px; }
|
| 188 |
.ew-pair { display:flex; gap:18px; justify-content:center; }
|
|
@@ -249,10 +254,13 @@ with gr.Blocks(title="EYEWITNESS") as demo:
|
|
| 249 |
gr.Markdown(f"**⚠ {case.disguise_line}**")
|
| 250 |
with gr.Column(scale=2):
|
| 251 |
gr.Markdown("### THE LINEUP — click the culprit")
|
|
|
|
|
|
|
| 252 |
gal = gr.Gallery(
|
| 253 |
value=[(face_png(f, width=240), f"Nº {i + 1}")
|
| 254 |
for i, f in enumerate(s["lineup"])],
|
| 255 |
-
columns=4,
|
|
|
|
| 256 |
|
| 257 |
def _pick(st: dict, evt: gr.SelectData):
|
| 258 |
return pick_suspect(st, evt.index)
|
|
|
|
| 5 |
grow the lineup; from INSPECTOR the culprit changes one feature before the
|
| 6 |
lineup ("he's been to a barber since").
|
| 7 |
|
| 8 |
+
UI architecture: a single state-driven @gr.render stage. Column-visibility
|
| 9 |
+
toggling desyncs in Gradio 6 once demo.load touches it (see FIELD_NOTES.md).
|
| 10 |
"""
|
| 11 |
from __future__ import annotations
|
| 12 |
|
|
|
|
| 46 |
return "data:image/svg+xml;base64," + base64.b64encode(svg.encode()).decode()
|
| 47 |
|
| 48 |
|
| 49 |
+
from game.render import face_image as face_png # gr.Gallery needs PIL, not data-URIs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
def glimpse_html(case: Case) -> str:
|
|
|
|
| 142 |
s["case_no"] = min(s["case_no"] + 1, len(RANKS))
|
| 143 |
s["case"] = make_case(s["case_no"])
|
| 144 |
s["screen"] = "intro"
|
| 145 |
+
for stale in ("described", "lineup", "culprit_idx", "picked", "correct"):
|
| 146 |
+
s.pop(stale, None)
|
| 147 |
return s
|
| 148 |
|
| 149 |
|
| 150 |
# ------------------------------------------------------------------ UI
|
| 151 |
CSS = """
|
| 152 |
+
@import url('https://fonts.googleapis.com/css2?family=Special+Elite&display=swap');
|
| 153 |
:root { --paper:#f4efe4; --ink:#2b2a28; --red:#a33327; --tape:#c9a227; }
|
| 154 |
+
.gradio-container { background:#191713 !important; font-family:'Courier New',monospace !important;
|
| 155 |
+
background-image: radial-gradient(ellipse at 50% -10%, rgba(201,162,39,0.07) 0%, transparent 55%) !important; }
|
| 156 |
#ew-root { max-width: 900px; margin: 0 auto; }
|
| 157 |
+
.ew-header { text-align:center; color:var(--paper); letter-spacing:7px; font-size:34px; padding:12px 0 0;
|
| 158 |
+
font-family:'Special Elite','Courier New',monospace; text-shadow: 0 2px 0 rgba(0,0,0,.6); }
|
| 159 |
.ew-sub { text-align:center; color:#8d8678; font-size:12px; letter-spacing:2px; margin-bottom:8px; }
|
| 160 |
.ew-rank { font-family:monospace; color:var(--tape); text-align:center; letter-spacing:2px; font-size:13px; }
|
| 161 |
.ew-card { background:var(--paper) !important; border-radius:4px; padding:22px 26px !important; color:var(--ink);
|
| 162 |
box-shadow: 0 10px 40px rgba(0,0,0,.5); }
|
| 163 |
.ew-card * { color: var(--ink); }
|
| 164 |
.ew-glimpse { position:relative; width:320px; margin:0 auto; }
|
| 165 |
+
.ew-glimpse::before { content:'● REC'; position:absolute; top:8px; left:10px; z-index:3;
|
| 166 |
+
color:#e03b2f; font-size:12px; letter-spacing:2px; animation: ew-blink 1.1s step-end infinite; }
|
| 167 |
+
.ew-glimpse::after { content:''; position:absolute; inset:0 0 13px 0; z-index:2; pointer-events:none;
|
| 168 |
+
box-shadow: inset 0 0 60px rgba(0,0,0,.55);
|
| 169 |
+
background: repeating-linear-gradient(0deg, transparent 0 3px, rgba(0,0,0,0.06) 3px 4px); }
|
| 170 |
+
@keyframes ew-blink { 50% { opacity: 0; } }
|
| 171 |
.ew-glimpse img { width:100%; filter: blur(0); }
|
| 172 |
@keyframes ew-blurout { to { filter: blur(30px) contrast(0.4); } }
|
| 173 |
.ew-static { position:absolute; inset:0 0 13px 0; display:flex; align-items:center; justify-content:center;
|
|
|
|
| 184 |
.ew-hit td { color:#1d6b2f !important; } .ew-miss td { color:var(--red) !important; } .ew-silent td { color:#8d8678 !important; }
|
| 185 |
.ew-verdict { text-align:center; }
|
| 186 |
.ew-stamp { display:inline-block; border:4px solid var(--red); color:var(--red); padding:6px 22px;
|
| 187 |
+
font-size:26px; letter-spacing:4px; transform:rotate(-6deg) scale(1); margin:6px 0 10px;
|
| 188 |
+
font-family:'Special Elite','Courier New',monospace;
|
| 189 |
+
animation: ew-stamp-in .28s cubic-bezier(.2,2.2,.5,1) both; }
|
| 190 |
+
@keyframes ew-stamp-in { from { opacity:0; transform: rotate(-6deg) scale(2.4); } }
|
| 191 |
.ew-good .ew-stamp { border-color:#1d6b2f; color:#1d6b2f; }
|
| 192 |
.ew-quote { font-style:italic; margin-bottom:12px; }
|
| 193 |
.ew-pair { display:flex; gap:18px; justify-content:center; }
|
|
|
|
| 254 |
gr.Markdown(f"**⚠ {case.disguise_line}**")
|
| 255 |
with gr.Column(scale=2):
|
| 256 |
gr.Markdown("### THE LINEUP — click the culprit")
|
| 257 |
+
n = len(s["lineup"])
|
| 258 |
+
rows = -(-n // 4) # ceil
|
| 259 |
gal = gr.Gallery(
|
| 260 |
value=[(face_png(f, width=240), f"Nº {i + 1}")
|
| 261 |
for i, f in enumerate(s["lineup"])],
|
| 262 |
+
columns=4, rows=rows, height=rows * 330,
|
| 263 |
+
allow_preview=False, label="")
|
| 264 |
|
| 265 |
def _pick(st: dict, evt: gr.SelectData):
|
| 266 |
return pick_suspect(st, evt.index)
|
assets/case_bank.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"name": "the <Pets> <Animal-Pet-Sharing-Comedy-Interview>",
|
| 4 |
+
"blurb": "<one sentence, third person>",
|
| 5 |
+
"seed": 1001
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"name": "The <CYBER SECURITY <CYBERG AGENCY> <SECURITY PATTERNS> <COM",
|
| 9 |
+
"blurb": "In this fast-paced, high-stakes game, you'll tackle complex cybersecurity challenges using a range of specialized techniques, tools, and resources. Whether it's",
|
| 10 |
+
"seed": 1007
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"name": "The <Food> <Cooking> <Cooking>",
|
| 14 |
+
"blurb": "The kitchen is full of delicious, mouth-watering recipes.",
|
| 15 |
+
"seed": 1008
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"name": "the <Artistic> <Job/Heist/Affair/Caper>",
|
| 19 |
+
"blurb": "<one sentence, third person>",
|
| 20 |
+
"seed": 1013
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"name": "the weather weather crime",
|
| 24 |
+
"blurb": "I was looking at this weather forecast and noticed that it was turning pretty rainy today. The game would be a simple one: I was trying to figure out if it was ",
|
| 25 |
+
"seed": 1014
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"name": "the tech crime game",
|
| 29 |
+
"blurb": "A smart detective with a keen eye for the latest tech gadgets.",
|
| 30 |
+
"seed": 1015
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"name": "The <Animal> <Animal> <Animal> <Animal> <Animal> <Animal> <A",
|
| 34 |
+
"blurb": "A humorous, family-friendly game about an animal detective who solves crimes, fights villains, and helps pets.",
|
| 35 |
+
"seed": 1017
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"name": "The <Sword, Armor, Crown, Glove, Bow, Locket, Sword of Valor",
|
| 39 |
+
"blurb": "<one sentence, third person>",
|
| 40 |
+
"seed": 1019
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"name": "the <sports> <Game> <Heist/Affair/Caper>",
|
| 44 |
+
"blurb": "<one sentence, third person>",
|
| 45 |
+
"seed": 1020
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"name": "the <Mime <Art> <Comic Detective> <Funny>",
|
| 49 |
+
"blurb": "<one sentence, third person>",
|
| 50 |
+
"seed": 1021
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"name": "the <Technology> <Job/Heist/Affair/Caper>",
|
| 54 |
+
"blurb": "<one sentence, third person>",
|
| 55 |
+
"seed": 1023
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"name": "the <transportation> <Job/Heist/Affair/Caper>",
|
| 59 |
+
"blurb": "<one sentence, third person>",
|
| 60 |
+
"seed": 1027
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"name": "the <Scent> <Crime/Affair/Caper>",
|
| 64 |
+
"blurb": "<one sentence, third person>",
|
| 65 |
+
"seed": 1029
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"name": "The Weather Game",
|
| 69 |
+
"blurb": "A hilarious game where you use weather reports to solve crimes.",
|
| 70 |
+
"seed": 1030
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"name": "The <Music> <Job/Heist/Affair/Caper>",
|
| 74 |
+
"blurb": "In this game of musical crime, you'll navigate a world where genres collide and you'll uncover musical secrets hidden around corners. You'll need to use your mu",
|
| 75 |
+
"seed": 1034
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"name": "the <FUNKY, FUNNY, FAMILY-FRIEND OFTEN-HOSTS, CO-CREATORS, A",
|
| 79 |
+
"blurb": "A hilarious, insightful, and action-packed game that turns ordinary players into the world's greatest comedy detective.",
|
| 80 |
+
"seed": 1036
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"name": "the weather",
|
| 84 |
+
"blurb": "A comedic game where you use the current weather to solve crimes or catch runaway family members.",
|
| 85 |
+
"seed": 1038
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"name": "the <Technology> <Job/Heist/Affair/Caper>",
|
| 89 |
+
"blurb": "<one sentence, third person>",
|
| 90 |
+
"seed": 1039
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"name": "the <Song> <Genre>",
|
| 94 |
+
"blurb": "<one sentence, third person>",
|
| 95 |
+
"seed": 1042
|
| 96 |
+
}
|
| 97 |
+
]
|
assets/voice_caught_0.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4ec4439f2344afbca9a284d72824ee89e114019a037aebe5e095270ceec02940
|
| 3 |
+
size 291884
|
assets/voice_caught_1.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ab9a952dda5c13ecd50d8e597d3a6382c1f72c6e2a84d13bbf9cb73713326718
|
| 3 |
+
size 307244
|
assets/voice_caught_2.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:eb465e4774aef4879739c4eb0a35205242a81eb012eb3179118d0922cddefcff
|
| 3 |
+
size 476204
|
assets/voice_escaped_0.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4762a0218d27de14e25bd7f5442b7fa6fdff55af5a3133e4d0daa226dd18b936
|
| 3 |
+
size 353324
|
assets/voice_escaped_1.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:97edae68d8513e9dec0b3b45ba85605c1b68428b20f0a258f9e1d04f19b286d6
|
| 3 |
+
size 245804
|
assets/voice_escaped_2.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ffbf5a76501545eb3de222712bc461acc4eaedda0f17475dffd254c2905b988c
|
| 3 |
+
size 291884
|
game/face.py
CHANGED
|
@@ -281,11 +281,3 @@ def render_face_svg(spec: FaceSpec, width: int = 300, paper: bool = True, seed_j
|
|
| 281 |
return svg
|
| 282 |
|
| 283 |
|
| 284 |
-
def render_unknown_svg(width: int = 300) -> str:
|
| 285 |
-
"""Placeholder silhouette for un-described attributes / mystery faces."""
|
| 286 |
-
return f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 360" width="{width}">
|
| 287 |
-
<rect width="300" height="360" fill="{PAPER}"/>
|
| 288 |
-
<path d="M118,250 L114,300 C112,318 100,322 84,330 L216,330 C200,322 188,318 186,300 L182,250 M150,68 C108,68 84,104 84,160 C84,216 112,262 150,262 C188,262 216,216 216,160 C216,104 192,68 150,68"
|
| 289 |
-
fill="#d8d0bd" stroke="#9a9284" stroke-width="3" stroke-dasharray="7 6"/>
|
| 290 |
-
<text x="150" y="180" text-anchor="middle" font-family="monospace" font-size="64" fill="#9a9284">?</text>
|
| 291 |
-
</svg>'''
|
|
|
|
| 281 |
return svg
|
| 282 |
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
game/lineup.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Lineup builder
|
| 2 |
|
| 3 |
Distractors are derived from the PLAYER'S OWN testimony errors:
|
| 4 |
- Attributes you got WRONG become shared between culprit and distractors
|
|
|
|
| 1 |
+
"""Lineup builder.
|
| 2 |
|
| 3 |
Distractors are derived from the PLAYER'S OWN testimony errors:
|
| 4 |
- Attributes you got WRONG become shared between culprit and distractors
|
game/model.py
CHANGED
|
@@ -20,7 +20,7 @@ from .parser import parse_testimony as _tier_a
|
|
| 20 |
|
| 21 |
import os
|
| 22 |
|
| 23 |
-
#
|
| 24 |
MODEL_ID = os.environ.get("EYEWITNESS_MODEL_ID", "openbmb/MiniCPM5-1B")
|
| 25 |
|
| 26 |
try: # ZeroGPU decorator when running in a HF Space
|
|
@@ -96,7 +96,9 @@ JSON:"""
|
|
| 96 |
@_gpu
|
| 97 |
def _generate(testimony: str) -> str:
|
| 98 |
model, tok = _load()
|
| 99 |
-
|
|
|
|
|
|
|
| 100 |
messages = [{"role": "user", "content": prompt}]
|
| 101 |
text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 102 |
enc = tok(text, return_tensors="pt").to(model.device)
|
|
@@ -123,5 +125,8 @@ def parse_testimony_model(testimony: str) -> dict[str, str | None]:
|
|
| 123 |
v = data.get(attr)
|
| 124 |
if isinstance(v, str):
|
| 125 |
v = v.strip().lower().replace(" ", "_")
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
| 127 |
return out
|
|
|
|
| 20 |
|
| 21 |
import os
|
| 22 |
|
| 23 |
+
# Overridable so the Space can point at the published fine-tune.
|
| 24 |
MODEL_ID = os.environ.get("EYEWITNESS_MODEL_ID", "openbmb/MiniCPM5-1B")
|
| 25 |
|
| 26 |
try: # ZeroGPU decorator when running in a HF Space
|
|
|
|
| 96 |
@_gpu
|
| 97 |
def _generate(testimony: str) -> str:
|
| 98 |
model, tok = _load()
|
| 99 |
+
# json.dumps gives fully-escaped quoting; bare replace() left injectable quotes
|
| 100 |
+
safe = json.dumps(testimony, ensure_ascii=False)[1:-1]
|
| 101 |
+
prompt = PROMPT.format(schema=_schema_block(), testimony=safe)
|
| 102 |
messages = [{"role": "user", "content": prompt}]
|
| 103 |
text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 104 |
enc = tok(text, return_tensors="pt").to(model.device)
|
|
|
|
| 125 |
v = data.get(attr)
|
| 126 |
if isinstance(v, str):
|
| 127 |
v = v.strip().lower().replace(" ", "_")
|
| 128 |
+
# Tier A is literal-match (high precision): where it spoke, it wins.
|
| 129 |
+
# The model fills the silence (messy/indirect phrasings) — this also
|
| 130 |
+
# caps model hallucinations to attributes Tier A had no opinion on.
|
| 131 |
+
out[attr] = base.get(attr) or (v if v in VOCAB[attr] else None)
|
| 132 |
return out
|
game/poster.py
CHANGED
|
@@ -5,12 +5,10 @@ downloads and dares friends with. Pure PIL — zero GPU, zero model.
|
|
| 5 |
"""
|
| 6 |
from __future__ import annotations
|
| 7 |
|
| 8 |
-
import io
|
| 9 |
-
|
| 10 |
-
import cairosvg
|
| 11 |
from PIL import Image, ImageDraw, ImageFont
|
| 12 |
|
| 13 |
-
from .face import FaceSpec
|
|
|
|
| 14 |
|
| 15 |
PAPER = (244, 239, 228)
|
| 16 |
INK = (43, 42, 40)
|
|
@@ -19,12 +17,6 @@ GREEN = (29, 107, 47)
|
|
| 19 |
TAPE = (201, 162, 39)
|
| 20 |
|
| 21 |
|
| 22 |
-
def _face_img(spec: FaceSpec, width: int) -> Image.Image:
|
| 23 |
-
png = cairosvg.svg2png(bytestring=render_face_svg(spec, width=width).encode(),
|
| 24 |
-
output_width=width)
|
| 25 |
-
return Image.open(io.BytesIO(png)).convert("RGB")
|
| 26 |
-
|
| 27 |
-
|
| 28 |
def _font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
|
| 29 |
candidates = [
|
| 30 |
"/usr/share/fonts/TTF/DejaVuSansMono-Bold.ttf" if bold else "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
|
|
|
|
| 5 |
"""
|
| 6 |
from __future__ import annotations
|
| 7 |
|
|
|
|
|
|
|
|
|
|
| 8 |
from PIL import Image, ImageDraw, ImageFont
|
| 9 |
|
| 10 |
+
from .face import FaceSpec
|
| 11 |
+
from .render import face_image as _face_img
|
| 12 |
|
| 13 |
PAPER = (244, 239, 228)
|
| 14 |
INK = (43, 42, 40)
|
|
|
|
| 17 |
TAPE = (201, 162, 39)
|
| 18 |
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
def _font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
|
| 21 |
candidates = [
|
| 22 |
"/usr/share/fonts/TTF/DejaVuSansMono-Bold.ttf" if bold else "/usr/share/fonts/TTF/DejaVuSansMono.ttf",
|
game/render.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared SVG→PIL rasterization for components that need real images."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import io
|
| 5 |
+
|
| 6 |
+
import cairosvg
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
from .face import FaceSpec, render_face_svg
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def face_image(spec: FaceSpec, width: int = 300) -> Image.Image:
|
| 13 |
+
png = cairosvg.svg2png(bytestring=render_face_svg(spec, width=width).encode(),
|
| 14 |
+
output_width=width)
|
| 15 |
+
return Image.open(io.BytesIO(png)).convert("RGB")
|
modal_factory.py
CHANGED
|
@@ -52,16 +52,16 @@ def build_case_bank(n_cases: int = 48) -> list[dict]:
|
|
| 52 |
filename="MiniCPM5-1B-Q4_K_M.gguf")
|
| 53 |
llm = Llama(model_path=gguf, n_ctx=2048, verbose=False)
|
| 54 |
|
| 55 |
-
|
| 56 |
"Write ONE short, funny, family-friendly petty-crime headline and a one-sentence "
|
| 57 |
"description for a comedy detective game. Format strictly as JSON: "
|
| 58 |
'{"name": "the <Something> <Job/Heist/Affair/Caper>", "blurb": "<one sentence, third person>"}'
|
| 59 |
-
"\nTheme hint:
|
| 60 |
)
|
| 61 |
hints = ["food", "animals", "music", "transport", "sports", "art", "weather", "technology"]
|
| 62 |
bank = []
|
| 63 |
for i in range(n_cases):
|
| 64 |
-
out = llm(
|
| 65 |
max_tokens=120, temperature=0.9, stop=["\n\n"])
|
| 66 |
text = out["choices"][0]["text"]
|
| 67 |
try:
|
|
@@ -77,26 +77,50 @@ def build_case_bank(n_cases: int = 48) -> list[dict]:
|
|
| 77 |
|
| 78 |
@app.function(image=voice_image, gpu="A10G", timeout=1800)
|
| 79 |
def build_voice_bank() -> dict[str, list[bytes]]:
|
| 80 |
-
"""Pre-render the culprit's verdict lines with VoxCPM2
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
import io
|
| 82 |
|
| 83 |
import soundfile as sf
|
| 84 |
from voxcpm import VoxCPM
|
| 85 |
|
| 86 |
tts = VoxCPM.from_pretrained("openbmb/VoxCPM2")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
rendered: dict[str, list[bytes]] = {"caught": [], "escaped": []}
|
| 88 |
for kind, lines in VERDICT_LINES.items():
|
| 89 |
for line in lines:
|
| 90 |
-
wav =
|
| 91 |
-
text=line,
|
| 92 |
-
prompt_text="(smug, nasal, theatrical, slightly out of breath)",
|
| 93 |
-
)
|
| 94 |
buf = io.BytesIO()
|
| 95 |
-
sf.write(buf, wav,
|
| 96 |
rendered[kind].append(buf.getvalue())
|
| 97 |
return rendered
|
| 98 |
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
@app.local_entrypoint()
|
| 101 |
def main():
|
| 102 |
bank = build_case_bank.remote(48)
|
|
@@ -108,3 +132,29 @@ def main():
|
|
| 108 |
with open(f"assets/voice_{kind}_{i}.wav", "wb") as f:
|
| 109 |
f.write(blob)
|
| 110 |
print("factory complete -> assets/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
filename="MiniCPM5-1B-Q4_K_M.gguf")
|
| 53 |
llm = Llama(model_path=gguf, n_ctx=2048, verbose=False)
|
| 54 |
|
| 55 |
+
prompt_head = (
|
| 56 |
"Write ONE short, funny, family-friendly petty-crime headline and a one-sentence "
|
| 57 |
"description for a comedy detective game. Format strictly as JSON: "
|
| 58 |
'{"name": "the <Something> <Job/Heist/Affair/Caper>", "blurb": "<one sentence, third person>"}'
|
| 59 |
+
"\nTheme hint: "
|
| 60 |
)
|
| 61 |
hints = ["food", "animals", "music", "transport", "sports", "art", "weather", "technology"]
|
| 62 |
bank = []
|
| 63 |
for i in range(n_cases):
|
| 64 |
+
out = llm(prompt_head + hints[i % len(hints)] + "\nJSON:",
|
| 65 |
max_tokens=120, temperature=0.9, stop=["\n\n"])
|
| 66 |
text = out["choices"][0]["text"]
|
| 67 |
try:
|
|
|
|
| 77 |
|
| 78 |
@app.function(image=voice_image, gpu="A10G", timeout=1800)
|
| 79 |
def build_voice_bank() -> dict[str, list[bytes]]:
|
| 80 |
+
"""Pre-render the culprit's verdict lines with VoxCPM2.
|
| 81 |
+
|
| 82 |
+
Voice consistency trick: render an anchor line with the default voice once,
|
| 83 |
+
then self-clone it (prompt_wav + its transcript) for every other line so the
|
| 84 |
+
culprit keeps ONE voice across the whole bank."""
|
| 85 |
import io
|
| 86 |
|
| 87 |
import soundfile as sf
|
| 88 |
from voxcpm import VoxCPM
|
| 89 |
|
| 90 |
tts = VoxCPM.from_pretrained("openbmb/VoxCPM2")
|
| 91 |
+
|
| 92 |
+
sr = tts.tts_model.sample_rate # 48 kHz; a wrong rate here slows the anchor
|
| 93 |
+
# and garbles every line cloned from it
|
| 94 |
+
anchor_text = "Okay, okay. It was me. Take me in."
|
| 95 |
+
anchor = tts.generate(text=anchor_text)
|
| 96 |
+
anchor_path = "/tmp/anchor.wav"
|
| 97 |
+
sf.write(anchor_path, anchor, sr)
|
| 98 |
+
|
| 99 |
+
def render(line: str):
|
| 100 |
+
if line == anchor_text:
|
| 101 |
+
return anchor
|
| 102 |
+
return tts.generate(text=line, prompt_wav_path=anchor_path, prompt_text=anchor_text)
|
| 103 |
+
|
| 104 |
rendered: dict[str, list[bytes]] = {"caught": [], "escaped": []}
|
| 105 |
for kind, lines in VERDICT_LINES.items():
|
| 106 |
for line in lines:
|
| 107 |
+
wav = render(line)
|
|
|
|
|
|
|
|
|
|
| 108 |
buf = io.BytesIO()
|
| 109 |
+
sf.write(buf, wav, sr, format="WAV")
|
| 110 |
rendered[kind].append(buf.getvalue())
|
| 111 |
return rendered
|
| 112 |
|
| 113 |
|
| 114 |
+
@app.local_entrypoint()
|
| 115 |
+
def voices_only():
|
| 116 |
+
voices = build_voice_bank.remote()
|
| 117 |
+
for kind, blobs in voices.items():
|
| 118 |
+
for i, blob in enumerate(blobs):
|
| 119 |
+
with open(f"assets/voice_{kind}_{i}.wav", "wb") as f:
|
| 120 |
+
f.write(blob)
|
| 121 |
+
print("voice bank complete -> assets/")
|
| 122 |
+
|
| 123 |
+
|
| 124 |
@app.local_entrypoint()
|
| 125 |
def main():
|
| 126 |
bank = build_case_bank.remote(48)
|
|
|
|
| 132 |
with open(f"assets/voice_{kind}_{i}.wav", "wb") as f:
|
| 133 |
f.write(blob)
|
| 134 |
print("factory complete -> assets/")
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@app.function(image=voice_image, gpu="A10G", timeout=600)
|
| 138 |
+
def probe_voice() -> dict:
|
| 139 |
+
"""Diagnose VoxCPM output: true sample rate, array shape, attrs."""
|
| 140 |
+
import numpy as np
|
| 141 |
+
from voxcpm import VoxCPM
|
| 142 |
+
|
| 143 |
+
tts = VoxCPM.from_pretrained("openbmb/VoxCPM2")
|
| 144 |
+
wav = tts.generate(text="Okay, okay. It was me. Take me in.")
|
| 145 |
+
info = {
|
| 146 |
+
"shape": list(np.asarray(wav).shape),
|
| 147 |
+
"dtype": str(np.asarray(wav).dtype),
|
| 148 |
+
"attrs": [a for a in dir(tts) if "rate" in a.lower() or "sr" == a.lower()],
|
| 149 |
+
}
|
| 150 |
+
for a in info["attrs"]:
|
| 151 |
+
try:
|
| 152 |
+
info[f"val_{a}"] = str(getattr(tts, a))[:120]
|
| 153 |
+
except Exception:
|
| 154 |
+
pass
|
| 155 |
+
return info
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
@app.local_entrypoint()
|
| 159 |
+
def probe():
|
| 160 |
+
print(probe_voice.remote())
|
train/gen_dataset.py
CHANGED
|
@@ -105,6 +105,13 @@ FILLERS_ES = ["creo", "me parece", "juraria que", "no se,", "o sea", "si no recu
|
|
| 105 |
CONNECT = [", ", ", and ", ". ", ", y ", " — ", "... "]
|
| 106 |
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
def spec_to_testimony(spec: FaceSpec, rng: random.Random) -> tuple[str, dict]:
|
| 109 |
"""Pick 3-7 attributes, phrase them noisily, return (text, partial truth dict)."""
|
| 110 |
attrs = [a for a in VOCAB if PHRASES[a].get(getattr(spec, a))]
|
|
|
|
| 105 |
CONNECT = [", ", ", and ", ". ", ", y ", " — ", "... "]
|
| 106 |
|
| 107 |
|
| 108 |
+
# keep in lockstep with game/parser.py SYNONYMS — drift here means the model
|
| 109 |
+
# trains on phrasings the live app never produces, and vice versa
|
| 110 |
+
assert set(PHRASES) == set(VOCAB), "PHRASES attributes drifted from VOCAB"
|
| 111 |
+
for _attr, _values in PHRASES.items():
|
| 112 |
+
assert set(_values) <= set(VOCAB[_attr]) | {"none"}, f"PHRASES[{_attr}] has values outside VOCAB"
|
| 113 |
+
|
| 114 |
+
|
| 115 |
def spec_to_testimony(spec: FaceSpec, rng: random.Random) -> tuple[str, dict]:
|
| 116 |
"""Pick 3-7 attributes, phrase them noisily, return (text, partial truth dict)."""
|
| 117 |
attrs = [a for a in VOCAB if PHRASES[a].get(getattr(spec, a))]
|