Spaces:
Running on Zero
Running on Zero
Daryl Lim Claude Opus 4.8 (1M context) commited on
Commit ·
f8a5dc6
1
Parent(s): 9bd40fb
feat: expose stable /translate API endpoint and tidy UI
Browse filesAdd api_name="translate" to the submit handler so the deployed Space
presents a clean, documented text+language -> string endpoint. Mark the
two UI-only handlers (swap, button loading) as api_visibility="private"
so they no longer leak as _swap_languages / _translate_with_loading
endpoints; only /translate is public. Autofocus the input textbox.
Add tests for the stable api_name, the single-public-endpoint surface
(via get_api_info), and input autofocus. Update CLAUDE.md test counts
(53 total / 43 fast).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CLAUDE.md +3 -3
- app.py +5 -0
- tests/test_app.py +22 -0
CLAUDE.md
CHANGED
|
@@ -25,8 +25,8 @@ uv run ruff format .
|
|
| 25 |
uv run ty check
|
| 26 |
|
| 27 |
# Test
|
| 28 |
-
uv run pytest # all
|
| 29 |
-
uv run pytest -m "not slow" #
|
| 30 |
uv run pytest -m slow # 10 model tests only (CUDA only)
|
| 31 |
|
| 32 |
# Generate language mapping (dev only)
|
|
@@ -41,7 +41,7 @@ uv run scripts/generate_langmap.py <path-to-paper.pdf>
|
|
| 41 |
|
| 42 |
**`scripts/`** — `generate_langmap.py` parses the MADLAD-400 paper PDF (Table 9, pages 16-22) using pdfplumber and generates the static language mapping with region assignments. Dev-only tool; requires `requirements-dev.txt` dependencies.
|
| 43 |
|
| 44 |
-
**`tests/`** —
|
| 45 |
|
| 46 |
## Tooling
|
| 47 |
|
|
|
|
| 25 |
uv run ty check
|
| 26 |
|
| 27 |
# Test
|
| 28 |
+
uv run pytest # all 53 tests (slow require CUDA + model download)
|
| 29 |
+
uv run pytest -m "not slow" # 43 fast tests only
|
| 30 |
uv run pytest -m slow # 10 model tests only (CUDA only)
|
| 31 |
|
| 32 |
# Generate language mapping (dev only)
|
|
|
|
| 41 |
|
| 42 |
**`scripts/`** — `generate_langmap.py` parses the MADLAD-400 paper PDF (Table 9, pages 16-22) using pdfplumber and generates the static language mapping with region assignments. Dev-only tool; requires `requirements-dev.txt` dependencies.
|
| 43 |
|
| 44 |
+
**`tests/`** — 53 tests (43 fast, 10 slow). `test_langmap.py` has 10 fast tests for mapping validation (dict shape, regions, spot-checks). `test_app.py` has 33 fast tests (signatures, device fallback, UI layout with symmetric dropdowns, swap button, textbox config including toolbar buttons and input autofocus, handler wiring, stable `translate` API endpoint with UI-only handlers kept private, no HTML elements, locale codes, no title) and 10 slow tests (translation with various parameters, language mapping). Slow tests require CUDA and model download; auto-skipped without CUDA.
|
| 45 |
|
| 46 |
## Tooling
|
| 47 |
|
app.py
CHANGED
|
@@ -128,6 +128,7 @@ def _build_demo() -> gr.Blocks:
|
|
| 128 |
lines=6,
|
| 129 |
max_length=2000,
|
| 130 |
show_label=False,
|
|
|
|
| 131 |
)
|
| 132 |
output_text = gr.Textbox(
|
| 133 |
placeholder="Translation",
|
|
@@ -139,20 +140,24 @@ def _build_demo() -> gr.Blocks:
|
|
| 139 |
|
| 140 |
translate_btn = gr.Button("Translate", variant="primary")
|
| 141 |
|
|
|
|
| 142 |
swap_btn.click(
|
| 143 |
fn=_swap_languages,
|
| 144 |
inputs=[source_language, target_language, input_text, output_text],
|
| 145 |
outputs=[source_language, target_language, input_text, output_text],
|
|
|
|
| 146 |
)
|
| 147 |
translate_btn.click(
|
| 148 |
fn=_translate_with_loading,
|
| 149 |
inputs=[input_text, target_language],
|
| 150 |
outputs=[translate_btn, output_text],
|
|
|
|
| 151 |
)
|
| 152 |
input_text.submit(
|
| 153 |
fn=translate,
|
| 154 |
inputs=[input_text, target_language],
|
| 155 |
outputs=output_text,
|
|
|
|
| 156 |
)
|
| 157 |
|
| 158 |
return demo
|
|
|
|
| 128 |
lines=6,
|
| 129 |
max_length=2000,
|
| 130 |
show_label=False,
|
| 131 |
+
autofocus=True,
|
| 132 |
)
|
| 133 |
output_text = gr.Textbox(
|
| 134 |
placeholder="Translation",
|
|
|
|
| 140 |
|
| 141 |
translate_btn = gr.Button("Translate", variant="primary")
|
| 142 |
|
| 143 |
+
# UI-only handlers: kept off the public API surface (private) so only /translate is exposed.
|
| 144 |
swap_btn.click(
|
| 145 |
fn=_swap_languages,
|
| 146 |
inputs=[source_language, target_language, input_text, output_text],
|
| 147 |
outputs=[source_language, target_language, input_text, output_text],
|
| 148 |
+
api_visibility="private",
|
| 149 |
)
|
| 150 |
translate_btn.click(
|
| 151 |
fn=_translate_with_loading,
|
| 152 |
inputs=[input_text, target_language],
|
| 153 |
outputs=[translate_btn, output_text],
|
| 154 |
+
api_visibility="private",
|
| 155 |
)
|
| 156 |
input_text.submit(
|
| 157 |
fn=translate,
|
| 158 |
inputs=[input_text, target_language],
|
| 159 |
outputs=output_text,
|
| 160 |
+
api_name="translate",
|
| 161 |
)
|
| 162 |
|
| 163 |
return demo
|
tests/test_app.py
CHANGED
|
@@ -153,6 +153,13 @@ def test_input_textbox_has_no_placeholder(demo):
|
|
| 153 |
assert interactive[0].placeholder is None, f"Expected no placeholder, got {interactive[0].placeholder!r}"
|
| 154 |
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
def test_input_textbox_has_no_buttons(demo):
|
| 157 |
"""Input textbox should expose no toolbar buttons (gradio accepts invalid button values silently)."""
|
| 158 |
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
|
|
@@ -234,6 +241,21 @@ def test_all_handlers_wired(demo):
|
|
| 234 |
assert len(demo.fns) == 3, f"Expected 3 handlers, found {len(demo.fns)}"
|
| 235 |
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
def test_no_title(demo):
|
| 238 |
"""UI should not have an H1 title."""
|
| 239 |
markdowns = [b for b in demo.blocks.values() if type(b).__name__ == "Markdown"]
|
|
|
|
| 153 |
assert interactive[0].placeholder is None, f"Expected no placeholder, got {interactive[0].placeholder!r}"
|
| 154 |
|
| 155 |
|
| 156 |
+
def test_input_textbox_autofocus(demo):
|
| 157 |
+
"""Input textbox should autofocus so the cursor lands there on page load."""
|
| 158 |
+
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
|
| 159 |
+
interactive = [t for t in textboxes if t.interactive is not False]
|
| 160 |
+
assert interactive[0].autofocus is True, f"Expected autofocus=True, got {interactive[0].autofocus!r}"
|
| 161 |
+
|
| 162 |
+
|
| 163 |
def test_input_textbox_has_no_buttons(demo):
|
| 164 |
"""Input textbox should expose no toolbar buttons (gradio accepts invalid button values silently)."""
|
| 165 |
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
|
|
|
|
| 241 |
assert len(demo.fns) == 3, f"Expected 3 handlers, found {len(demo.fns)}"
|
| 242 |
|
| 243 |
|
| 244 |
+
def test_translate_endpoint_has_stable_api_name(demo):
|
| 245 |
+
"""The lean submit handler (text + language -> string) should expose a stable 'translate' API endpoint."""
|
| 246 |
+
api_fns = [fn for fn in demo.fns.values() if getattr(fn, "api_name", None) == "translate"]
|
| 247 |
+
assert len(api_fns) == 1, "Expected exactly one handler with api_name='translate'"
|
| 248 |
+
fn = api_fns[0]
|
| 249 |
+
assert [type(i).__name__ for i in fn.inputs] == ["Textbox", "Dropdown"]
|
| 250 |
+
assert [type(o).__name__ for o in fn.outputs] == ["Textbox"]
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def test_only_translate_endpoint_is_public(demo):
|
| 254 |
+
"""UI-only handlers (swap, button loading) should be private; only /translate is a public API endpoint."""
|
| 255 |
+
named = list(demo.get_api_info()["named_endpoints"].keys())
|
| 256 |
+
assert named == ["/translate"], f"Expected only ['/translate'] exposed, found {named}"
|
| 257 |
+
|
| 258 |
+
|
| 259 |
def test_no_title(demo):
|
| 260 |
"""UI should not have an H1 title."""
|
| 261 |
markdowns = [b for b in demo.blocks.values() if type(b).__name__ == "Markdown"]
|