Fcabla commited on
Commit
448f595
·
verified ·
1 Parent(s): f943b97

Upload folder using huggingface_hub

Browse files
.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 (Gradio 6 native).
9
- No Column-visibility toggling empirically fragile in Gradio 6.
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
- def face_png(spec: FaceSpec, width: int = 300) -> "Image.Image":
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:6px; font-size:30px; padding:10px 0 0; }
 
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, height=320, allow_preview=False, label="")
 
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 — the signature mechanic.
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
- # Swapped to our published fine-tune once training completes (Well-Tuned badge).
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
- prompt = PROMPT.format(schema=_schema_block(), testimony=testimony.replace('"', "'"))
 
 
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
- out[attr] = v if v in VOCAB[attr] else base.get(attr)
 
 
 
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, render_face_svg
 
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
- prompt_tpl = (
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: {hint}\nJSON:"
60
  )
61
  hints = ["food", "animals", "music", "transport", "sports", "art", "weather", "technology"]
62
  bank = []
63
  for i in range(n_cases):
64
- out = llm(prompt_tpl.format(hint=hints[i % len(hints)]),
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 (smug, theatrical)."""
 
 
 
 
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 = tts.generate(
91
- text=line,
92
- prompt_text="(smug, nasal, theatrical, slightly out of breath)",
93
- )
94
  buf = io.BytesIO()
95
- sf.write(buf, wav, 16000, format="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))]