Spaces:
Running on Zero
Running on Zero
Commit ·
1fa8013
1
Parent(s): 94a932b
Fix image dict handling, restore original UI, remove .opencode from repo
Browse files- .gitignore +7 -0
- .opencode/skills/caveman-commit/SKILL.md +0 -65
- .opencode/skills/caveman-help/SKILL.md +0 -59
- .opencode/skills/caveman-review/SKILL.md +0 -55
- .opencode/skills/caveman/SKILL.md +0 -67
- .opencode/skills/compress/SKILL.md +0 -1
- .opencode/skills/compress/scripts +0 -1
- .opencode/skills/hf-cli/.hf-skill-manifest.json +0 -4
- .opencode/skills/hf-cli/SKILL.md +0 -189
- app.py +19 -3
- tagger_ui/templates/index.html +124 -110
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.opencode/
|
| 2 |
+
.venv/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.py[cod]
|
| 5 |
+
*.safetensors
|
| 6 |
+
*.pt
|
| 7 |
+
*.pth
|
.opencode/skills/caveman-commit/SKILL.md
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
name: caveman-commit
|
| 3 |
-
description: >
|
| 4 |
-
Ultra-compressed commit message generator. Cuts noise from commit messages while preserving
|
| 5 |
-
intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when "why"
|
| 6 |
-
isn't obvious. Use when user says "write a commit", "commit message", "generate commit",
|
| 7 |
-
"/commit", or invokes /caveman-commit. Auto-triggers when staging changes.
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
Write commit messages terse and exact. Conventional Commits format. No fluff. Why over what.
|
| 11 |
-
|
| 12 |
-
## Rules
|
| 13 |
-
|
| 14 |
-
**Subject line:**
|
| 15 |
-
- `<type>(<scope>): <imperative summary>` — `<scope>` optional
|
| 16 |
-
- Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`, `style`, `revert`
|
| 17 |
-
- Imperative mood: "add", "fix", "remove" — not "added", "adds", "adding"
|
| 18 |
-
- ≤50 chars when possible, hard cap 72
|
| 19 |
-
- No trailing period
|
| 20 |
-
- Match project convention for capitalization after the colon
|
| 21 |
-
|
| 22 |
-
**Body (only if needed):**
|
| 23 |
-
- Skip entirely when subject is self-explanatory
|
| 24 |
-
- Add body only for: non-obvious *why*, breaking changes, migration notes, linked issues
|
| 25 |
-
- Wrap at 72 chars
|
| 26 |
-
- Bullets `-` not `*`
|
| 27 |
-
- Reference issues/PRs at end: `Closes #42`, `Refs #17`
|
| 28 |
-
|
| 29 |
-
**What NEVER goes in:**
|
| 30 |
-
- "This commit does X", "I", "we", "now", "currently" — the diff says what
|
| 31 |
-
- "As requested by..." — use Co-authored-by trailer
|
| 32 |
-
- "Generated with Claude Code" or any AI attribution
|
| 33 |
-
- Emoji (unless project convention requires)
|
| 34 |
-
- Restating the file name when scope already says it
|
| 35 |
-
|
| 36 |
-
## Examples
|
| 37 |
-
|
| 38 |
-
Diff: new endpoint for user profile with body explaining the why
|
| 39 |
-
- ❌ "feat: add a new endpoint to get user profile information from the database"
|
| 40 |
-
- ✅
|
| 41 |
-
```
|
| 42 |
-
feat(api): add GET /users/:id/profile
|
| 43 |
-
|
| 44 |
-
Mobile client needs profile data without the full user payload
|
| 45 |
-
to reduce LTE bandwidth on cold-launch screens.
|
| 46 |
-
|
| 47 |
-
Closes #128
|
| 48 |
-
```
|
| 49 |
-
|
| 50 |
-
Diff: breaking API change
|
| 51 |
-
- ✅
|
| 52 |
-
```
|
| 53 |
-
feat(api)!: rename /v1/orders to /v1/checkout
|
| 54 |
-
|
| 55 |
-
BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout
|
| 56 |
-
before 2026-06-01. Old route returns 410 after that date.
|
| 57 |
-
```
|
| 58 |
-
|
| 59 |
-
## Auto-Clarity
|
| 60 |
-
|
| 61 |
-
Always include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context.
|
| 62 |
-
|
| 63 |
-
## Boundaries
|
| 64 |
-
|
| 65 |
-
Only generates the commit message. Does not run `git commit`, does not stage files, does not amend. Output the message as a code block ready to paste. "stop caveman-commit" or "normal mode": revert to verbose commit style.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.opencode/skills/caveman-help/SKILL.md
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
name: caveman-help
|
| 3 |
-
description: >
|
| 4 |
-
Quick-reference card for all caveman modes, skills, and commands.
|
| 5 |
-
One-shot display, not a persistent mode. Trigger: /caveman-help,
|
| 6 |
-
"caveman help", "what caveman commands", "how do I use caveman".
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
# Caveman Help
|
| 10 |
-
|
| 11 |
-
Display this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style.
|
| 12 |
-
|
| 13 |
-
## Modes
|
| 14 |
-
|
| 15 |
-
| Mode | Trigger | What change |
|
| 16 |
-
|------|---------|-------------|
|
| 17 |
-
| **Lite** | `/caveman lite` | Drop filler. Keep sentence structure. |
|
| 18 |
-
| **Full** | `/caveman` | Drop articles, filler, pleasantries, hedging. Fragments OK. Default. |
|
| 19 |
-
| **Ultra** | `/caveman ultra` | Extreme compression. Bare fragments. Tables over prose. |
|
| 20 |
-
| **Wenyan-Lite** | `/caveman wenyan-lite` | Classical Chinese style, light compression. |
|
| 21 |
-
| **Wenyan-Full** | `/caveman wenyan` | Full 文言文. Maximum classical terseness. |
|
| 22 |
-
| **Wenyan-Ultra** | `/caveman wenyan-ultra` | Extreme. Ancient scholar on a budget. |
|
| 23 |
-
|
| 24 |
-
Mode stick until changed or session end.
|
| 25 |
-
|
| 26 |
-
## Skills
|
| 27 |
-
|
| 28 |
-
| Skill | Trigger | What it do |
|
| 29 |
-
|-------|---------|-----------|
|
| 30 |
-
| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |
|
| 31 |
-
| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |
|
| 32 |
-
| **caveman-compress** | `/caveman:compress <file>` | Compress .md files to caveman prose. Saves ~46% input tokens. |
|
| 33 |
-
| **caveman-help** | `/caveman-help` | This card. |
|
| 34 |
-
|
| 35 |
-
## Deactivate
|
| 36 |
-
|
| 37 |
-
Say "stop caveman" or "normal mode". Resume anytime with `/caveman`.
|
| 38 |
-
|
| 39 |
-
## Configure Default Mode
|
| 40 |
-
|
| 41 |
-
Default mode = `full`. Change it:
|
| 42 |
-
|
| 43 |
-
**Environment variable** (highest priority):
|
| 44 |
-
```bash
|
| 45 |
-
export CAVEMAN_DEFAULT_MODE=ultra
|
| 46 |
-
```
|
| 47 |
-
|
| 48 |
-
**Config file** (`~/.config/caveman/config.json`):
|
| 49 |
-
```json
|
| 50 |
-
{ "defaultMode": "lite" }
|
| 51 |
-
```
|
| 52 |
-
|
| 53 |
-
Set `"off"` to disable auto-activation on session start. User can still activate manually with `/caveman`.
|
| 54 |
-
|
| 55 |
-
Resolution: env var > config file > `full`.
|
| 56 |
-
|
| 57 |
-
## More
|
| 58 |
-
|
| 59 |
-
Full docs: https://github.com/JuliusBrussee/caveman
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.opencode/skills/caveman-review/SKILL.md
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
name: caveman-review
|
| 3 |
-
description: >
|
| 4 |
-
Ultra-compressed code review comments. Cuts noise from PR feedback while preserving
|
| 5 |
-
the actionable signal. Each comment is one line: location, problem, fix. Use when user
|
| 6 |
-
says "review this PR", "code review", "review the diff", "/review", or invokes
|
| 7 |
-
/caveman-review. Auto-triggers when reviewing pull requests.
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
Write code review comments terse and actionable. One line per finding. Location, problem, fix. No throat-clearing.
|
| 11 |
-
|
| 12 |
-
## Rules
|
| 13 |
-
|
| 14 |
-
**Format:** `L<line>: <problem>. <fix>.` — or `<file>:L<line>: ...` when reviewing multi-file diffs.
|
| 15 |
-
|
| 16 |
-
**Severity prefix (optional, when mixed):**
|
| 17 |
-
- `🔴 bug:` — broken behavior, will cause incident
|
| 18 |
-
- `🟡 risk:` — works but fragile (race, missing null check, swallowed error)
|
| 19 |
-
- `🔵 nit:` — style, naming, micro-optim. Author can ignore
|
| 20 |
-
- `❓ q:` — genuine question, not a suggestion
|
| 21 |
-
|
| 22 |
-
**Drop:**
|
| 23 |
-
- "I noticed that...", "It seems like...", "You might want to consider..."
|
| 24 |
-
- "This is just a suggestion but..." — use `nit:` instead
|
| 25 |
-
- "Great work!", "Looks good overall but..." — say it once at the top, not per comment
|
| 26 |
-
- Restating what the line does — the reviewer can read the diff
|
| 27 |
-
- Hedging ("perhaps", "maybe", "I think") — if unsure use `q:`
|
| 28 |
-
|
| 29 |
-
**Keep:**
|
| 30 |
-
- Exact line numbers
|
| 31 |
-
- Exact symbol/function/variable names in backticks
|
| 32 |
-
- Concrete fix, not "consider refactoring this"
|
| 33 |
-
- The *why* if the fix isn't obvious from the problem statement
|
| 34 |
-
|
| 35 |
-
## Examples
|
| 36 |
-
|
| 37 |
-
❌ "I noticed that on line 42 you're not checking if the user object is null before accessing the email property. This could potentially cause a crash if the user is not found in the database. You might want to add a null check here."
|
| 38 |
-
|
| 39 |
-
✅ `L42: 🔴 bug: user can be null after .find(). Add guard before .email.`
|
| 40 |
-
|
| 41 |
-
❌ "It looks like this function is doing a lot of things and might benefit from being broken up into smaller functions for readability."
|
| 42 |
-
|
| 43 |
-
✅ `L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.`
|
| 44 |
-
|
| 45 |
-
❌ "Have you considered what happens if the API returns a 429? I think we should probably handle that case."
|
| 46 |
-
|
| 47 |
-
✅ `L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).`
|
| 48 |
-
|
| 49 |
-
## Auto-Clarity
|
| 50 |
-
|
| 51 |
-
Drop terse mode for: security findings (CVE-class bugs need full explanation + reference), architectural disagreements (need rationale, not just a one-liner), and onboarding contexts where the author is new and needs the "why". In those cases write a normal paragraph, then resume terse for the rest.
|
| 52 |
-
|
| 53 |
-
## Boundaries
|
| 54 |
-
|
| 55 |
-
Reviews only — does not write the code fix, does not approve/request-changes, does not run linters. Output the comment(s) ready to paste into the PR. "stop caveman-review" or "normal mode": revert to verbose review style.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.opencode/skills/caveman/SKILL.md
DELETED
|
@@ -1,67 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
name: caveman
|
| 3 |
-
description: >
|
| 4 |
-
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
|
| 5 |
-
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
|
| 6 |
-
wenyan-lite, wenyan-full, wenyan-ultra.
|
| 7 |
-
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
|
| 8 |
-
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
| 12 |
-
|
| 13 |
-
## Persistence
|
| 14 |
-
|
| 15 |
-
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
|
| 16 |
-
|
| 17 |
-
Default: **full**. Switch: `/caveman lite|full|ultra`.
|
| 18 |
-
|
| 19 |
-
## Rules
|
| 20 |
-
|
| 21 |
-
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
|
| 22 |
-
|
| 23 |
-
Pattern: `[thing] [action] [reason]. [next step].`
|
| 24 |
-
|
| 25 |
-
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
| 26 |
-
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
|
| 27 |
-
|
| 28 |
-
## Intensity
|
| 29 |
-
|
| 30 |
-
| Level | What change |
|
| 31 |
-
|-------|------------|
|
| 32 |
-
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
|
| 33 |
-
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
|
| 34 |
-
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough |
|
| 35 |
-
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
|
| 36 |
-
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
|
| 37 |
-
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
|
| 38 |
-
|
| 39 |
-
Example — "Why React component re-render?"
|
| 40 |
-
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
|
| 41 |
-
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
|
| 42 |
-
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
|
| 43 |
-
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
|
| 44 |
-
- wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。"
|
| 45 |
-
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
|
| 46 |
-
|
| 47 |
-
Example — "Explain database connection pooling."
|
| 48 |
-
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
|
| 49 |
-
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
|
| 50 |
-
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
|
| 51 |
-
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
|
| 52 |
-
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
|
| 53 |
-
|
| 54 |
-
## Auto-Clarity
|
| 55 |
-
|
| 56 |
-
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done.
|
| 57 |
-
|
| 58 |
-
Example — destructive op:
|
| 59 |
-
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
|
| 60 |
-
> ```sql
|
| 61 |
-
> DROP TABLE users;
|
| 62 |
-
> ```
|
| 63 |
-
> Caveman resume. Verify backup exist first.
|
| 64 |
-
|
| 65 |
-
## Boundaries
|
| 66 |
-
|
| 67 |
-
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.opencode/skills/compress/SKILL.md
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
../../caveman-compress/SKILL.md
|
|
|
|
|
|
.opencode/skills/compress/scripts
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
../../caveman-compress/scripts
|
|
|
|
|
|
.opencode/skills/hf-cli/.hf-skill-manifest.json
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"installed_revision": "072fb2635445f6b0ae9761fd6306569b4a87aa2c",
|
| 3 |
-
"schema_version": 1
|
| 4 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.opencode/skills/hf-cli/SKILL.md
DELETED
|
@@ -1,189 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
name: hf-cli
|
| 3 |
-
description: "Hugging Face Hub CLI (`hf`) for downloading, uploading, and managing models, datasets, spaces, buckets, repos, papers, jobs, and more on the Hugging Face Hub. Use when: handling authentication; managing local cache; managing Hugging Face Buckets; running or scheduling jobs on Hugging Face infrastructure; managing Hugging Face repos; discussions and pull requests; browsing models, datasets and spaces; reading, searching, or browsing academic papers; managing collections; querying datasets; configuring spaces; setting up webhooks; or deploying and managing HF Inference Endpoints. Make sure to use this skill whenever the user mentions 'hf', 'huggingface', 'Hugging Face', 'huggingface-cli', or 'hugging face cli', or wants to do anything related to the Hugging Face ecosystem and to AI and ML in general. Also use for cloud storage needs like training checkpoints, data pipelines, or agent traces. Use even if the user doesn't explicitly ask for a CLI command. Replaces the deprecated `huggingface-cli`."
|
| 4 |
-
---
|
| 5 |
-
|
| 6 |
-
Install: `curl -LsSf https://hf.co/cli/install.sh | bash -s`.
|
| 7 |
-
|
| 8 |
-
The Hugging Face Hub CLI tool `hf` is available. IMPORTANT: The `hf` command replaces the deprecated `huggingface-cli` command.
|
| 9 |
-
|
| 10 |
-
Use `hf --help` to view available functions. Note that auth commands are now all under `hf auth` e.g. `hf auth whoami`.
|
| 11 |
-
|
| 12 |
-
Generated with `huggingface_hub v1.10.2`. Run `hf skills add --force` to regenerate.
|
| 13 |
-
|
| 14 |
-
## Commands
|
| 15 |
-
|
| 16 |
-
- `hf download REPO_ID` — Download files from the Hub. `[--type CHOICE --revision TEXT --include TEXT --exclude TEXT --cache-dir TEXT --local-dir TEXT --force-download --dry-run --quiet --max-workers INTEGER]`
|
| 17 |
-
- `hf env` — Print information about the environment.
|
| 18 |
-
- `hf sync` — Sync files between local directory and a bucket. `[--delete --ignore-times --ignore-sizes --plan TEXT --apply TEXT --dry-run --include TEXT --exclude TEXT --filter-from TEXT --existing --ignore-existing --verbose --quiet]`
|
| 19 |
-
- `hf upload REPO_ID` — Upload a file or a folder to the Hub. Recommended for single-commit uploads. `[--type CHOICE --revision TEXT --private --include TEXT --exclude TEXT --delete TEXT --commit-message TEXT --commit-description TEXT --create-pr --every FLOAT --quiet]`
|
| 20 |
-
- `hf upload-large-folder REPO_ID LOCAL_PATH` — Upload a large folder to the Hub. Recommended for resumable uploads. `[--type CHOICE --revision TEXT --private --include TEXT --exclude TEXT --num-workers INTEGER --no-report --no-bars]`
|
| 21 |
-
- `hf version` — Print information about the hf version.
|
| 22 |
-
|
| 23 |
-
### `hf auth` — Manage authentication (login, logout, etc.).
|
| 24 |
-
|
| 25 |
-
- `hf auth list` — List all stored access tokens.
|
| 26 |
-
- `hf auth login` — Login using a token from huggingface.co/settings/tokens. `[--add-to-git-credential --force]`
|
| 27 |
-
- `hf auth logout` — Logout from a specific token. `[--token-name TEXT]`
|
| 28 |
-
- `hf auth switch` — Switch between access tokens. `[--token-name TEXT --add-to-git-credential]`
|
| 29 |
-
- `hf auth whoami` — Find out which huggingface.co account you are logged in as. `[--format CHOICE]`
|
| 30 |
-
|
| 31 |
-
### `hf buckets` — Commands to interact with buckets.
|
| 32 |
-
|
| 33 |
-
- `hf buckets cp SRC` — Copy files to or from buckets. `[--quiet]`
|
| 34 |
-
- `hf buckets create BUCKET_ID` — Create a new bucket. `[--private --exist-ok --quiet]`
|
| 35 |
-
- `hf buckets delete BUCKET_ID` — Delete a bucket. `[--yes --missing-ok --quiet]`
|
| 36 |
-
- `hf buckets info BUCKET_ID` — Get info about a bucket. `[--quiet]`
|
| 37 |
-
- `hf buckets list` — List buckets or files in a bucket. `[--human-readable --tree --recursive --format CHOICE --quiet]`
|
| 38 |
-
- `hf buckets move FROM_ID TO_ID` — Move (rename) a bucket to a new name or namespace.
|
| 39 |
-
- `hf buckets remove ARGUMENT` — Remove files from a bucket. `[--recursive --yes --dry-run --include TEXT --exclude TEXT --quiet]`
|
| 40 |
-
- `hf buckets sync` — Sync files between local directory and a bucket. `[--delete --ignore-times --ignore-sizes --plan TEXT --apply TEXT --dry-run --include TEXT --exclude TEXT --filter-from TEXT --existing --ignore-existing --verbose --quiet]`
|
| 41 |
-
|
| 42 |
-
### `hf cache` — Manage local cache directory.
|
| 43 |
-
|
| 44 |
-
- `hf cache list` — List cached repositories or revisions. `[--cache-dir TEXT --revisions --filter TEXT --format CHOICE --quiet --sort CHOICE --limit INTEGER]`
|
| 45 |
-
- `hf cache prune` — Remove detached revisions from the cache. `[--cache-dir TEXT --yes --dry-run]`
|
| 46 |
-
- `hf cache rm TARGETS` — Remove cached repositories or revisions. `[--cache-dir TEXT --yes --dry-run]`
|
| 47 |
-
- `hf cache verify REPO_ID` — Verify checksums for a single repo revision from cache or a local directory. `[--type CHOICE --revision TEXT --cache-dir TEXT --local-dir TEXT --fail-on-missing-files --fail-on-extra-files]`
|
| 48 |
-
|
| 49 |
-
### `hf collections` — Interact with collections on the Hub.
|
| 50 |
-
|
| 51 |
-
- `hf collections add-item COLLECTION_SLUG ITEM_ID ITEM_TYPE` — Add an item to a collection. `[--note TEXT --exists-ok]`
|
| 52 |
-
- `hf collections create TITLE` — Create a new collection on the Hub. `[--namespace TEXT --description TEXT --private --exists-ok]`
|
| 53 |
-
- `hf collections delete COLLECTION_SLUG` — Delete a collection from the Hub. `[--missing-ok]`
|
| 54 |
-
- `hf collections delete-item COLLECTION_SLUG ITEM_OBJECT_ID` — Delete an item from a collection. `[--missing-ok]`
|
| 55 |
-
- `hf collections info COLLECTION_SLUG` — Get info about a collection on the Hub. Output is in JSON format.
|
| 56 |
-
- `hf collections list` — List collections on the Hub. `[--owner TEXT --item TEXT --sort CHOICE --limit INTEGER --format CHOICE --quiet]`
|
| 57 |
-
- `hf collections update COLLECTION_SLUG` — Update a collection's metadata on the Hub. `[--title TEXT --description TEXT --position INTEGER --private --theme TEXT]`
|
| 58 |
-
- `hf collections update-item COLLECTION_SLUG ITEM_OBJECT_ID` — Update an item in a collection. `[--note TEXT --position INTEGER]`
|
| 59 |
-
|
| 60 |
-
### `hf datasets` — Interact with datasets on the Hub.
|
| 61 |
-
|
| 62 |
-
- `hf datasets info DATASET_ID` — Get info about a dataset on the Hub. `[--revision TEXT --expand TEXT --format CHOICE]`
|
| 63 |
-
- `hf datasets list` — List datasets on the Hub. `[--search TEXT --author TEXT --filter TEXT --sort CHOICE --limit INTEGER --expand TEXT --format CHOICE]`
|
| 64 |
-
- `hf datasets parquet DATASET_ID` — List parquet file URLs available for a dataset. `[--subset TEXT --split TEXT --format CHOICE]`
|
| 65 |
-
- `hf datasets sql SQL` — Execute a raw SQL query with DuckDB against dataset parquet URLs. `[--format CHOICE]`
|
| 66 |
-
|
| 67 |
-
### `hf discussions` — Manage discussions and pull requests on the Hub.
|
| 68 |
-
|
| 69 |
-
- `hf discussions close REPO_ID NUM` — Close a discussion or pull request. `[--comment TEXT --yes --type CHOICE]`
|
| 70 |
-
- `hf discussions comment REPO_ID NUM` — Comment on a discussion or pull request. `[--body TEXT --body-file PATH --type CHOICE]`
|
| 71 |
-
- `hf discussions create REPO_ID --title TEXT` — Create a new discussion or pull request on a repo. `[--body TEXT --body-file PATH --pull-request --type CHOICE]`
|
| 72 |
-
- `hf discussions diff REPO_ID NUM` — Show the diff of a pull request. `[--type CHOICE]`
|
| 73 |
-
- `hf discussions info REPO_ID NUM` — Get info about a discussion or pull request. `[--comments --diff --no-color --type CHOICE --format CHOICE]`
|
| 74 |
-
- `hf discussions list REPO_ID` — List discussions and pull requests on a repo. `[--status CHOICE --kind CHOICE --author TEXT --limit INTEGER --type CHOICE --format CHOICE --quiet]`
|
| 75 |
-
- `hf discussions merge REPO_ID NUM` — Merge a pull request. `[--comment TEXT --yes --type CHOICE]`
|
| 76 |
-
- `hf discussions rename REPO_ID NUM NEW_TITLE` — Rename a discussion or pull request. `[--type CHOICE]`
|
| 77 |
-
- `hf discussions reopen REPO_ID NUM` — Reopen a closed discussion or pull request. `[--comment TEXT --yes --type CHOICE]`
|
| 78 |
-
|
| 79 |
-
### `hf endpoints` — Manage Hugging Face Inference Endpoints.
|
| 80 |
-
|
| 81 |
-
- `hf endpoints catalog deploy --repo TEXT` — Deploy an Inference Endpoint from the Model Catalog. `[--name TEXT --accelerator TEXT --namespace TEXT]`
|
| 82 |
-
- `hf endpoints catalog list` — List available Catalog models.
|
| 83 |
-
- `hf endpoints delete NAME` — Delete an Inference Endpoint permanently. `[--namespace TEXT --yes]`
|
| 84 |
-
- `hf endpoints deploy NAME --repo TEXT --framework TEXT --accelerator TEXT --instance-size TEXT --instance-type TEXT --region TEXT --vendor TEXT` — Deploy an Inference Endpoint from a Hub repository. `[--namespace TEXT --task TEXT --min-replica INTEGER --max-replica INTEGER --scale-to-zero-timeout INTEGER --scaling-metric CHOICE --scaling-threshold FLOAT]`
|
| 85 |
-
- `hf endpoints describe NAME` — Get information about an existing endpoint. `[--namespace TEXT]`
|
| 86 |
-
- `hf endpoints list` — Lists all Inference Endpoints for the given namespace. `[--namespace TEXT --format CHOICE --quiet]`
|
| 87 |
-
- `hf endpoints pause NAME` — Pause an Inference Endpoint. `[--namespace TEXT]`
|
| 88 |
-
- `hf endpoints resume NAME` — Resume an Inference Endpoint. `[--namespace TEXT --fail-if-already-running]`
|
| 89 |
-
- `hf endpoints scale-to-zero NAME` — Scale an Inference Endpoint to zero. `[--namespace TEXT]`
|
| 90 |
-
- `hf endpoints update NAME` — Update an existing endpoint. `[--namespace TEXT --repo TEXT --accelerator TEXT --instance-size TEXT --instance-type TEXT --framework TEXT --revision TEXT --task TEXT --min-replica INTEGER --max-replica INTEGER --scale-to-zero-timeout INTEGER --scaling-metric CHOICE --scaling-threshold FLOAT]`
|
| 91 |
-
|
| 92 |
-
### `hf extensions` — Manage hf CLI extensions.
|
| 93 |
-
|
| 94 |
-
- `hf extensions exec NAME` — Execute an installed extension.
|
| 95 |
-
- `hf extensions install REPO_ID` — Install an extension from a public GitHub repository. `[--force]`
|
| 96 |
-
- `hf extensions list` — List installed extension commands. `[--format CHOICE --quiet]`
|
| 97 |
-
- `hf extensions remove NAME` — Remove an installed extension.
|
| 98 |
-
- `hf extensions search` — Search extensions available on GitHub (tagged with 'hf-extension' topic). `[--format CHOICE --quiet]`
|
| 99 |
-
|
| 100 |
-
### `hf jobs` — Run and manage Jobs on the Hub.
|
| 101 |
-
|
| 102 |
-
- `hf jobs cancel JOB_ID` — Cancel a Job `[--namespace TEXT]`
|
| 103 |
-
- `hf jobs hardware` — List available hardware options for Jobs
|
| 104 |
-
- `hf jobs inspect JOB_IDS` — Display detailed information on one or more Jobs `[--namespace TEXT]`
|
| 105 |
-
- `hf jobs logs JOB_ID` — Fetch the logs of a Job. `[--follow --tail INTEGER --namespace TEXT]`
|
| 106 |
-
- `hf jobs ps` — List Jobs. `[--all --namespace TEXT --filter TEXT --format TEXT --quiet]`
|
| 107 |
-
- `hf jobs run IMAGE COMMAND` — Run a Job. `[--env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --flavor CHOICE --timeout TEXT --detach --namespace TEXT]`
|
| 108 |
-
- `hf jobs scheduled delete SCHEDULED_JOB_ID` — Delete a scheduled Job. `[--namespace TEXT]`
|
| 109 |
-
- `hf jobs scheduled inspect SCHEDULED_JOB_IDS` — Display detailed information on one or more scheduled Jobs `[--namespace TEXT]`
|
| 110 |
-
- `hf jobs scheduled ps` — List scheduled Jobs `[--all --namespace TEXT --filter TEXT --format TEXT --quiet]`
|
| 111 |
-
- `hf jobs scheduled resume SCHEDULED_JOB_ID` — Resume (unpause) a scheduled Job. `[--namespace TEXT]`
|
| 112 |
-
- `hf jobs scheduled run SCHEDULE IMAGE COMMAND` — Schedule a Job. `[--suspend --concurrency --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --flavor CHOICE --timeout TEXT --namespace TEXT]`
|
| 113 |
-
- `hf jobs scheduled suspend SCHEDULED_JOB_ID` — Suspend (pause) a scheduled Job. `[--namespace TEXT]`
|
| 114 |
-
- `hf jobs scheduled uv run SCHEDULE SCRIPT` — Run a UV script (local file or URL) on HF infrastructure `[--suspend --concurrency --image TEXT --flavor CHOICE --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --timeout TEXT --namespace TEXT --with TEXT --python TEXT]`
|
| 115 |
-
- `hf jobs stats` — Fetch the resource usage statistics and metrics of Jobs `[--namespace TEXT]`
|
| 116 |
-
- `hf jobs uv run SCRIPT` — Run a UV script (local file or URL) on HF infrastructure `[--image TEXT --flavor CHOICE --env TEXT --secrets TEXT --label TEXT --volume TEXT --env-file TEXT --secrets-file TEXT --timeout TEXT --detach --namespace TEXT --with TEXT --python TEXT]`
|
| 117 |
-
|
| 118 |
-
### `hf models` — Interact with models on the Hub.
|
| 119 |
-
|
| 120 |
-
- `hf models info MODEL_ID` — Get info about a model on the Hub. `[--revision TEXT --expand TEXT --format CHOICE]`
|
| 121 |
-
- `hf models list` — List models on the Hub. `[--search TEXT --author TEXT --filter TEXT --num-parameters TEXT --sort CHOICE --limit INTEGER --expand TEXT --format CHOICE]`
|
| 122 |
-
|
| 123 |
-
### `hf papers` — Interact with papers on the Hub.
|
| 124 |
-
|
| 125 |
-
- `hf papers info PAPER_ID` — Get info about a paper on the Hub. `[--format CHOICE]`
|
| 126 |
-
- `hf papers list` — List daily papers on the Hub. `[--date TEXT --week TEXT --month TEXT --submitter TEXT --sort CHOICE --limit INTEGER --format CHOICE]`
|
| 127 |
-
- `hf papers read PAPER_ID` — Read a paper as markdown.
|
| 128 |
-
- `hf papers search QUERY` — Search papers on the Hub. `[--limit INTEGER --format CHOICE]`
|
| 129 |
-
|
| 130 |
-
### `hf repos` — Manage repos on the Hub.
|
| 131 |
-
|
| 132 |
-
- `hf repos branch create REPO_ID BRANCH` — Create a new branch for a repo on the Hub. `[--revision TEXT --type CHOICE --exist-ok]`
|
| 133 |
-
- `hf repos branch delete REPO_ID BRANCH` — Delete a branch from a repo on the Hub. `[--type CHOICE]`
|
| 134 |
-
- `hf repos create REPO_ID` — Create a new repo on the Hub. `[--type CHOICE --space-sdk TEXT --private --public --protected --exist-ok --resource-group-id TEXT --flavor CHOICE --storage CHOICE --sleep-time INTEGER --secrets TEXT --secrets-file TEXT --env TEXT --env-file TEXT --volume TEXT]`
|
| 135 |
-
- `hf repos delete REPO_ID` — Delete a repo from the Hub. This is an irreversible operation. `[--type CHOICE --missing-ok]`
|
| 136 |
-
- `hf repos delete-files REPO_ID PATTERNS` — Delete files from a repo on the Hub. `[--type CHOICE --revision TEXT --commit-message TEXT --commit-description TEXT --create-pr]`
|
| 137 |
-
- `hf repos duplicate FROM_ID` — Duplicate a repo on the Hub (model, dataset, or Space). `[--type CHOICE --private --public --protected --exist-ok --flavor CHOICE --storage CHOICE --sleep-time INTEGER --secrets TEXT --secrets-file TEXT --env TEXT --env-file TEXT --volume TEXT]`
|
| 138 |
-
- `hf repos move FROM_ID TO_ID` — Move a repository from a namespace to another namespace. `[--type CHOICE]`
|
| 139 |
-
- `hf repos settings REPO_ID` — Update the settings of a repository. `[--gated CHOICE --private --public --protected --type CHOICE]`
|
| 140 |
-
- `hf repos tag create REPO_ID TAG` — Create a tag for a repo. `[--message TEXT --revision TEXT --type CHOICE]`
|
| 141 |
-
- `hf repos tag delete REPO_ID TAG` — Delete a tag for a repo. `[--yes --type CHOICE]`
|
| 142 |
-
- `hf repos tag list REPO_ID` — List tags for a repo. `[--type CHOICE]`
|
| 143 |
-
|
| 144 |
-
### `hf skills` — Manage skills for AI assistants.
|
| 145 |
-
|
| 146 |
-
- `hf skills add` — Download a Hugging Face skill and install it for an AI assistant. `[--claude --global --dest PATH --force]`
|
| 147 |
-
- `hf skills preview` — Print the generated `hf-cli` SKILL.md to stdout.
|
| 148 |
-
- `hf skills upgrade` — Upgrade installed Hugging Face marketplace skills. `[--claude --global --dest PATH]`
|
| 149 |
-
|
| 150 |
-
### `hf spaces` — Interact with spaces on the Hub.
|
| 151 |
-
|
| 152 |
-
- `hf spaces dev-mode SPACE_ID` — Enable or disable dev mode on a Space. `[--stop]`
|
| 153 |
-
- `hf spaces hot-reload SPACE_ID` — Hot-reload any Python file of a Space without a full rebuild + restart. `[--local-file TEXT --skip-checks --skip-summary]`
|
| 154 |
-
- `hf spaces info SPACE_ID` — Get info about a space on the Hub. `[--revision TEXT --expand TEXT --format CHOICE]`
|
| 155 |
-
- `hf spaces list` — List spaces on the Hub. `[--search TEXT --author TEXT --filter TEXT --sort CHOICE --limit INTEGER --expand TEXT --format CHOICE]`
|
| 156 |
-
|
| 157 |
-
### `hf webhooks` — Manage webhooks on the Hub.
|
| 158 |
-
|
| 159 |
-
- `hf webhooks create --watch TEXT` — Create a new webhook. `[--url TEXT --job-id TEXT --domain CHOICE --secret TEXT]`
|
| 160 |
-
- `hf webhooks delete WEBHOOK_ID` — Delete a webhook permanently. `[--yes]`
|
| 161 |
-
- `hf webhooks disable WEBHOOK_ID` — Disable an active webhook.
|
| 162 |
-
- `hf webhooks enable WEBHOOK_ID` — Enable a disabled webhook.
|
| 163 |
-
- `hf webhooks info WEBHOOK_ID` — Show full details for a single webhook as JSON.
|
| 164 |
-
- `hf webhooks list` — List all webhooks for the current user. `[--format CHOICE --quiet]`
|
| 165 |
-
- `hf webhooks update WEBHOOK_ID` — Update an existing webhook. Only provided options are changed. `[--url TEXT --watch TEXT --domain CHOICE --secret TEXT]`
|
| 166 |
-
|
| 167 |
-
## Common options
|
| 168 |
-
|
| 169 |
-
- `--format` — Output format: `--format json` (or `--json`) or `--format table` (default).
|
| 170 |
-
- `-q / --quiet` — Minimal output.
|
| 171 |
-
- `--revision` — Git revision id which can be a branch name, a tag, or a commit hash.
|
| 172 |
-
- `--token` — Use a User Access Token. Prefer setting `HF_TOKEN` env var instead of passing `--token`.
|
| 173 |
-
- `--type` — The type of repository (model, dataset, or space).
|
| 174 |
-
|
| 175 |
-
## Mounting repos as local filesystems
|
| 176 |
-
|
| 177 |
-
To mount Hub repositories or buckets as local filesystems — no download, no copy, no waiting — use `hf-mount`. Files are fetched on demand. GitHub: https://github.com/huggingface/hf-mount
|
| 178 |
-
|
| 179 |
-
Install: `curl -fsSL https://raw.githubusercontent.com/huggingface/hf-mount/main/install.sh | sh`
|
| 180 |
-
|
| 181 |
-
Some command examples:
|
| 182 |
-
- `hf-mount start repo openai-community/gpt2 /tmp/gpt2` — mount a repo (read-only)
|
| 183 |
-
- `hf-mount start --hf-token $HF_TOKEN bucket myuser/my-bucket /tmp/data` — mount a bucket (read-write)
|
| 184 |
-
- `hf-mount status` / `hf-mount stop /tmp/data` — list or unmount
|
| 185 |
-
|
| 186 |
-
## Tips
|
| 187 |
-
|
| 188 |
-
- Use `hf <command> --help` for full options, descriptions, usage, and real-world examples
|
| 189 |
-
- Authenticate with `HF_TOKEN` env var (recommended) or with `--token`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -7,6 +7,7 @@ import io
|
|
| 7 |
import json
|
| 8 |
import os
|
| 9 |
from pathlib import Path
|
|
|
|
| 10 |
|
| 11 |
import spaces
|
| 12 |
import torchvision.transforms.v2 as v2
|
|
@@ -64,10 +65,23 @@ CATEGORY_META: dict[int, dict] = {
|
|
| 64 |
}
|
| 65 |
|
| 66 |
# ---------------------------------------------------------------------------
|
| 67 |
-
#
|
| 68 |
# ---------------------------------------------------------------------------
|
| 69 |
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
def _preprocess(img: Image.Image, max_size: int) -> torch.Tensor:
|
| 72 |
"""Resize + ImageNet-normalise → [1, 3, H, W] float32 CPU tensor."""
|
| 73 |
w, h = img.size
|
|
@@ -180,7 +194,8 @@ templates.env.filters["format_number"] = lambda v: f"{v:,}"
|
|
| 180 |
def get_tags(image: str, max_size: int = 1024, floor: float = 0.05) -> str:
|
| 181 |
"""Tag an image. image: local filepath (uploaded) or http(s) URL.
|
| 182 |
Returns JSON string: {tags, categories, count}."""
|
| 183 |
-
|
|
|
|
| 184 |
pv = _preprocess(img, max_size) # CPU: resize + normalise
|
| 185 |
logits = _gpu_infer(pv) # GPU: model forward
|
| 186 |
return _postprocess(logits, floor) # CPU: sort + bucketise → JSON str
|
|
@@ -189,7 +204,8 @@ def get_tags(image: str, max_size: int = 1024, floor: float = 0.05) -> str:
|
|
| 189 |
@app.api(name="get_pca")
|
| 190 |
def get_pca(image: str, max_size: int = 1024) -> str:
|
| 191 |
"""Return base64-encoded PNG of PCA patch-feature visualisation."""
|
| 192 |
-
|
|
|
|
| 193 |
pv = _preprocess(img, max_size)
|
| 194 |
pca_img = _gpu_pca(pv) # GPU: backbone + PCA
|
| 195 |
return _pil_to_base64(pca_img) # CPU: encode → base64 str
|
|
|
|
| 7 |
import json
|
| 8 |
import os
|
| 9 |
from pathlib import Path
|
| 10 |
+
from typing import Any
|
| 11 |
|
| 12 |
import spaces
|
| 13 |
import torchvision.transforms.v2 as v2
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
# ---------------------------------------------------------------------------
|
| 68 |
+
# Helpers
|
| 69 |
# ---------------------------------------------------------------------------
|
| 70 |
|
| 71 |
|
| 72 |
+
def _resolve_image_source(image: Any) -> str:
|
| 73 |
+
"""Normalise the image argument from Gradio.
|
| 74 |
+
|
| 75 |
+
Gradio passes uploaded files as a dict:
|
| 76 |
+
{"path": "/tmp/gradio/...", "orig_name": "...", "url": "...", ...}
|
| 77 |
+
URL strings and local paths are passed as plain str.
|
| 78 |
+
Returns a str (filepath or URL) that _open_image can consume.
|
| 79 |
+
"""
|
| 80 |
+
if isinstance(image, dict):
|
| 81 |
+
return image.get("path") or image.get("url") or image["orig_name"]
|
| 82 |
+
return str(image)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
def _preprocess(img: Image.Image, max_size: int) -> torch.Tensor:
|
| 86 |
"""Resize + ImageNet-normalise → [1, 3, H, W] float32 CPU tensor."""
|
| 87 |
w, h = img.size
|
|
|
|
| 194 |
def get_tags(image: str, max_size: int = 1024, floor: float = 0.05) -> str:
|
| 195 |
"""Tag an image. image: local filepath (uploaded) or http(s) URL.
|
| 196 |
Returns JSON string: {tags, categories, count}."""
|
| 197 |
+
src = _resolve_image_source(image) # normalise str or dict
|
| 198 |
+
img = _open_image(src) # CPU: load + decode
|
| 199 |
pv = _preprocess(img, max_size) # CPU: resize + normalise
|
| 200 |
logits = _gpu_infer(pv) # GPU: model forward
|
| 201 |
return _postprocess(logits, floor) # CPU: sort + bucketise → JSON str
|
|
|
|
| 204 |
@app.api(name="get_pca")
|
| 205 |
def get_pca(image: str, max_size: int = 1024) -> str:
|
| 206 |
"""Return base64-encoded PNG of PCA patch-feature visualisation."""
|
| 207 |
+
src = _resolve_image_source(image)
|
| 208 |
+
img = _open_image(src)
|
| 209 |
pv = _preprocess(img, max_size)
|
| 210 |
pca_img = _gpu_pca(pv) # GPU: backbone + PCA
|
| 211 |
return _pil_to_base64(pca_img) # CPU: encode → base64 str
|
tagger_ui/templates/index.html
CHANGED
|
@@ -94,7 +94,7 @@
|
|
| 94 |
/* preview — full width on top */
|
| 95 |
.preview-wrap {
|
| 96 |
display: flex; align-items: flex-start; gap: 1rem;
|
| 97 |
-
margin-bottom: 1.2rem;
|
| 98 |
}
|
| 99 |
.preview-wrap img {
|
| 100 |
border-radius: var(--radius); max-height: 420px;
|
|
@@ -204,6 +204,7 @@
|
|
| 204 |
.tag-pill.hidden { display: none; }
|
| 205 |
|
| 206 |
/* ---- PCA panel ---- */
|
|
|
|
| 207 |
.preview-col { flex: 1 1 0; min-width: 0; }
|
| 208 |
.pca-col {
|
| 209 |
flex: 1 1 0; min-width: 0;
|
|
@@ -242,11 +243,11 @@
|
|
| 242 |
|
| 243 |
<div class="input-row">
|
| 244 |
<input type="text" id="url-input" placeholder="Paste image URL or drop a file below…" />
|
| 245 |
-
<button class="btn" id="tag-btn" onclick="
|
| 246 |
</div>
|
| 247 |
|
| 248 |
<div id="drop-zone" onclick="document.getElementById('file-input').click()">
|
| 249 |
-
<input type="file" id="file-input" accept="image/*" onchange="
|
| 250 |
Drop image here or <strong>click to browse</strong>
|
| 251 |
</div>
|
| 252 |
|
|
@@ -255,10 +256,10 @@
|
|
| 255 |
<label>
|
| 256 |
Min score
|
| 257 |
<input type="range" id="global-thresh" min="0" max="99" step="1" value="0"
|
| 258 |
-
oninput="
|
| 259 |
<input type="number" id="global-thresh-num" min="0" max="99" step="1" value="0"
|
| 260 |
style="width:52px"
|
| 261 |
-
oninput="
|
| 262 |
<span id="global-thresh-val" style="display:none">0%</span>
|
| 263 |
</label>
|
| 264 |
</div>
|
|
@@ -283,13 +284,13 @@
|
|
| 283 |
|
| 284 |
<!-- PCA toggle -->
|
| 285 |
<div style="display:flex;justify-content:flex-end;margin-bottom:.6rem">
|
| 286 |
-
<button class="pca-toggle" id="pca-toggle" onclick="
|
| 287 |
</div>
|
| 288 |
|
| 289 |
<!-- global copy bar -->
|
| 290 |
<div class="copy-bar">
|
| 291 |
-
<div class="tag-string" id="tag-string" onclick="
|
| 292 |
-
<button class="copy-btn" id="copy-btn-all" onclick="
|
| 293 |
</div>
|
| 294 |
|
| 295 |
<!-- category sections -->
|
|
@@ -299,10 +300,7 @@
|
|
| 299 |
|
| 300 |
</div>
|
| 301 |
|
| 302 |
-
<script
|
| 303 |
-
import { client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
|
| 304 |
-
const gradioApp = await client(location.origin);
|
| 305 |
-
|
| 306 |
// ---- category metadata from server ----
|
| 307 |
const CAT_META = {
|
| 308 |
{% for cat_id, meta in category_meta.items() %}
|
|
@@ -310,13 +308,9 @@
|
|
| 310 |
{% endfor %}
|
| 311 |
};
|
| 312 |
|
| 313 |
-
// Per-category filter state
|
| 314 |
const catState = {};
|
| 315 |
-
let globalFloor = 0.0;
|
| 316 |
-
|
| 317 |
-
// ---- PCA state ----
|
| 318 |
-
let _pcaEnabled = false;
|
| 319 |
-
let _lastPcaRequest = null;
|
| 320 |
|
| 321 |
function getCatState(catId) {
|
| 322 |
if (!catState[catId]) catState[catId] = { mode: 'threshold', topk: 20, threshold: 0.85 };
|
|
@@ -328,6 +322,7 @@
|
|
| 328 |
globalFloor = v / 100;
|
| 329 |
document.getElementById('global-thresh').value = v;
|
| 330 |
document.getElementById('global-thresh-num').value = v;
|
|
|
|
| 331 |
document.querySelectorAll('.cat-section').forEach(sec => {
|
| 332 |
applyFilter(parseInt(sec.dataset.catId));
|
| 333 |
});
|
|
@@ -338,12 +333,15 @@
|
|
| 338 |
const el = document.getElementById(`thnum-${catId}`);
|
| 339 |
if (el) el.value = pct;
|
| 340 |
}
|
| 341 |
-
|
| 342 |
function syncCatThreshRange(catId, pct) {
|
| 343 |
const el = document.getElementById(`range-${catId}`);
|
| 344 |
if (el) el.value = Math.max(1, Math.min(99, parseInt(pct) || 1));
|
| 345 |
}
|
| 346 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
function togglePca() {
|
| 348 |
_pcaEnabled = !_pcaEnabled;
|
| 349 |
const btn = document.getElementById('pca-toggle');
|
|
@@ -353,80 +351,14 @@
|
|
| 353 |
if (_pcaEnabled && _lastPcaRequest) runPca(_lastPcaRequest);
|
| 354 |
}
|
| 355 |
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
try {
|
| 363 |
-
const imageArg = req.type === 'url' ? req.url : await handle_file(req.file);
|
| 364 |
-
const res = await gradioApp.predict("/get_pca", { image: imageArg, max_size: maxSize });
|
| 365 |
-
img.src = "data:image/png;base64," + res.data[0];
|
| 366 |
-
img.style.display = 'block';
|
| 367 |
-
} catch (err) {
|
| 368 |
-
console.error("PCA error:", err);
|
| 369 |
-
img.style.display = 'none';
|
| 370 |
-
} finally {
|
| 371 |
-
spinner.style.display = 'none';
|
| 372 |
-
}
|
| 373 |
-
}
|
| 374 |
|
| 375 |
-
|
| 376 |
-
const dz = document.getElementById('drop-zone');
|
| 377 |
-
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag-over'); });
|
| 378 |
-
dz.addEventListener('dragleave', () => dz.classList.remove('drag-over'));
|
| 379 |
-
dz.addEventListener('drop', e => {
|
| 380 |
-
e.preventDefault(); dz.classList.remove('drag-over');
|
| 381 |
-
const file = e.dataTransfer.files[0];
|
| 382 |
-
if (file) submitFile(file);
|
| 383 |
-
});
|
| 384 |
-
|
| 385 |
-
function runFromFile(input) {
|
| 386 |
-
if (input.files[0]) submitFile(input.files[0]);
|
| 387 |
-
}
|
| 388 |
-
|
| 389 |
-
async function runFromUrl() {
|
| 390 |
-
const url = document.getElementById('url-input').value.trim();
|
| 391 |
-
if (!url) return;
|
| 392 |
-
setPreview(url, url);
|
| 393 |
-
_lastPcaRequest = { type: 'url', url };
|
| 394 |
-
if (_pcaEnabled) runPca(_lastPcaRequest);
|
| 395 |
-
setLoading(true);
|
| 396 |
-
try {
|
| 397 |
-
const res = await gradioApp.predict("/get_tags", {
|
| 398 |
-
image: url,
|
| 399 |
-
max_size: +document.getElementById('maxsize-input').value,
|
| 400 |
-
floor: globalFloor,
|
| 401 |
-
});
|
| 402 |
-
renderResults(JSON.parse(res.data[0]));
|
| 403 |
-
} catch (err) {
|
| 404 |
-
showError(String(err));
|
| 405 |
-
} finally {
|
| 406 |
-
setLoading(false);
|
| 407 |
-
}
|
| 408 |
-
}
|
| 409 |
-
|
| 410 |
-
async function submitFile(file) {
|
| 411 |
-
const reader = new FileReader();
|
| 412 |
-
reader.onload = e => setPreview(e.target.result, file.name);
|
| 413 |
-
reader.readAsDataURL(file);
|
| 414 |
-
_lastPcaRequest = { type: 'file', file };
|
| 415 |
-
if (_pcaEnabled) runPca(_lastPcaRequest);
|
| 416 |
-
setLoading(true);
|
| 417 |
-
try {
|
| 418 |
-
const res = await gradioApp.predict("/get_tags", {
|
| 419 |
-
image: await handle_file(file),
|
| 420 |
-
max_size: +document.getElementById('maxsize-input').value,
|
| 421 |
-
floor: globalFloor,
|
| 422 |
-
});
|
| 423 |
-
renderResults(JSON.parse(res.data[0]));
|
| 424 |
-
} catch (err) {
|
| 425 |
-
showError(String(err));
|
| 426 |
-
} finally {
|
| 427 |
-
setLoading(false);
|
| 428 |
-
}
|
| 429 |
-
}
|
| 430 |
|
| 431 |
function setPreview(src, label) {
|
| 432 |
document.getElementById('preview-img').src = src;
|
|
@@ -436,8 +368,12 @@
|
|
| 436 |
|
| 437 |
// ---- rendering ----
|
| 438 |
|
|
|
|
|
|
|
|
|
|
| 439 |
function renderResults(data) {
|
| 440 |
hideError();
|
|
|
|
| 441 |
|
| 442 |
const container = document.getElementById('categories');
|
| 443 |
container.innerHTML = '';
|
|
@@ -465,31 +401,31 @@
|
|
| 465 |
<span class="mode-mini">
|
| 466 |
<button id="btn-topk-${cat.id}"
|
| 467 |
class="${state.mode==='topk'?'active':''}"
|
| 468 |
-
onclick="
|
| 469 |
<button id="btn-thresh-${cat.id}"
|
| 470 |
class="${state.mode==='threshold'?'active':''}"
|
| 471 |
-
onclick="
|
| 472 |
</span>
|
| 473 |
|
| 474 |
<span id="ctrl-topk-${cat.id}" style="display:${state.mode==='topk'?'flex':'none'};align-items:center;gap:.3rem">
|
| 475 |
<input type="number" id="topk-${cat.id}" value="${state.topk}" min="1" max="500"
|
| 476 |
style="width:52px"
|
| 477 |
-
oninput="
|
| 478 |
</span>
|
| 479 |
|
| 480 |
<span id="ctrl-thresh-${cat.id}" style="display:${state.mode==='threshold'?'flex':'none'};align-items:center;gap:.3rem">
|
| 481 |
<input type="range" id="range-${cat.id}" min="1" max="99" step="1"
|
| 482 |
value="${Math.round(state.threshold*100)}"
|
| 483 |
-
oninput="
|
| 484 |
<input type="number" class="cat-thresh-num" id="thnum-${cat.id}"
|
| 485 |
min="1" max="99" step="1" value="${Math.round(state.threshold*100)}"
|
| 486 |
-
oninput="
|
| 487 |
<span class="cat-thresh-val" style="display:none" id="thval-${cat.id}">${state.threshold.toFixed(2)}</span>
|
| 488 |
</span>
|
| 489 |
</span>
|
| 490 |
|
| 491 |
<span class="cat-count" id="count-${cat.id}">0</span>
|
| 492 |
-
<span class="cat-chevron">
|
| 493 |
`;
|
| 494 |
hdr.addEventListener('click', () => sec.classList.toggle('collapsed'));
|
| 495 |
|
|
@@ -500,13 +436,14 @@
|
|
| 500 |
body.innerHTML = `
|
| 501 |
<div class="cat-copy-bar">
|
| 502 |
<div class="cat-tag-string" id="cat-copy-${cat.id}"
|
| 503 |
-
onclick="
|
| 504 |
<button class="cat-copy-btn" id="cat-btn-${cat.id}"
|
| 505 |
-
onclick="
|
| 506 |
</div>
|
| 507 |
<div class="tag-list" id="cat-tags-${cat.id}"></div>
|
| 508 |
`;
|
| 509 |
|
|
|
|
| 510 |
const pillContainer = body.querySelector(`#cat-tags-${cat.id}`);
|
| 511 |
for (const t of cat.tags) {
|
| 512 |
const pill = document.createElement('span');
|
|
@@ -532,6 +469,7 @@
|
|
| 532 |
updateGlobalCopyBar();
|
| 533 |
}
|
| 534 |
|
|
|
|
| 535 |
function applyFilter(catId) {
|
| 536 |
const state = getCatState(catId);
|
| 537 |
const pills = document.querySelectorAll(`#cat-tags-${catId} .tag-pill`);
|
|
@@ -546,14 +484,17 @@
|
|
| 546 |
} else {
|
| 547 |
show = score >= state.threshold;
|
| 548 |
}
|
|
|
|
| 549 |
if (score < globalFloor) show = false;
|
| 550 |
pill.classList.toggle('hidden', !show);
|
| 551 |
if (show) visible++;
|
| 552 |
});
|
| 553 |
|
|
|
|
| 554 |
const countEl = document.getElementById(`count-${catId}`);
|
| 555 |
if (countEl) countEl.textContent = visible;
|
| 556 |
|
|
|
|
| 557 |
const visibleTags = [...pills]
|
| 558 |
.filter(p => !p.classList.contains('hidden'))
|
| 559 |
.map(p => p.dataset.tag);
|
|
@@ -588,6 +529,7 @@
|
|
| 588 |
}
|
| 589 |
|
| 590 |
function updateGlobalCopyBar() {
|
|
|
|
| 591 |
const allVisible = [];
|
| 592 |
document.querySelectorAll('.cat-section').forEach(sec => {
|
| 593 |
sec.querySelectorAll('.tag-pill:not(.hidden)').forEach(p => {
|
|
@@ -625,21 +567,93 @@
|
|
| 625 |
const el = document.getElementById('error-msg');
|
| 626 |
el.textContent = msg; el.style.display = 'block';
|
| 627 |
}
|
| 628 |
-
|
| 629 |
-
function hideError() {
|
| 630 |
-
document.getElementById('error-msg').style.display = 'none';
|
| 631 |
-
}
|
| 632 |
|
| 633 |
document.getElementById('url-input').addEventListener('keydown', e => {
|
| 634 |
if (e.key === 'Enter') runFromUrl();
|
| 635 |
});
|
| 636 |
|
| 637 |
-
//
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
};
|
| 644 |
</script>
|
| 645 |
</body>
|
|
|
|
| 94 |
/* preview — full width on top */
|
| 95 |
.preview-wrap {
|
| 96 |
display: flex; align-items: flex-start; gap: 1rem;
|
| 97 |
+
margin-bottom: 1.2rem;
|
| 98 |
}
|
| 99 |
.preview-wrap img {
|
| 100 |
border-radius: var(--radius); max-height: 420px;
|
|
|
|
| 204 |
.tag-pill.hidden { display: none; }
|
| 205 |
|
| 206 |
/* ---- PCA panel ---- */
|
| 207 |
+
.preview-wrap { flex-wrap: wrap; }
|
| 208 |
.preview-col { flex: 1 1 0; min-width: 0; }
|
| 209 |
.pca-col {
|
| 210 |
flex: 1 1 0; min-width: 0;
|
|
|
|
| 243 |
|
| 244 |
<div class="input-row">
|
| 245 |
<input type="text" id="url-input" placeholder="Paste image URL or drop a file below…" />
|
| 246 |
+
<button class="btn" id="tag-btn" onclick="runFromUrl()">Tag</button>
|
| 247 |
</div>
|
| 248 |
|
| 249 |
<div id="drop-zone" onclick="document.getElementById('file-input').click()">
|
| 250 |
+
<input type="file" id="file-input" accept="image/*" onchange="runFromFile(this)" />
|
| 251 |
Drop image here or <strong>click to browse</strong>
|
| 252 |
</div>
|
| 253 |
|
|
|
|
| 256 |
<label>
|
| 257 |
Min score
|
| 258 |
<input type="range" id="global-thresh" min="0" max="99" step="1" value="0"
|
| 259 |
+
oninput="setGlobalThreshold(this.value)" />
|
| 260 |
<input type="number" id="global-thresh-num" min="0" max="99" step="1" value="0"
|
| 261 |
style="width:52px"
|
| 262 |
+
oninput="setGlobalThreshold(this.value)" />
|
| 263 |
<span id="global-thresh-val" style="display:none">0%</span>
|
| 264 |
</label>
|
| 265 |
</div>
|
|
|
|
| 284 |
|
| 285 |
<!-- PCA toggle -->
|
| 286 |
<div style="display:flex;justify-content:flex-end;margin-bottom:.6rem">
|
| 287 |
+
<button class="pca-toggle" id="pca-toggle" onclick="togglePca()">Show PCA</button>
|
| 288 |
</div>
|
| 289 |
|
| 290 |
<!-- global copy bar -->
|
| 291 |
<div class="copy-bar">
|
| 292 |
+
<div class="tag-string" id="tag-string" onclick="copyAll()" title="Click to copy all visible tags"></div>
|
| 293 |
+
<button class="copy-btn" id="copy-btn-all" onclick="copyAll()">Copy all</button>
|
| 294 |
</div>
|
| 295 |
|
| 296 |
<!-- category sections -->
|
|
|
|
| 300 |
|
| 301 |
</div>
|
| 302 |
|
| 303 |
+
<script>
|
|
|
|
|
|
|
|
|
|
| 304 |
// ---- category metadata from server ----
|
| 305 |
const CAT_META = {
|
| 306 |
{% for cat_id, meta in category_meta.items() %}
|
|
|
|
| 308 |
{% endfor %}
|
| 309 |
};
|
| 310 |
|
| 311 |
+
// Per-category filter state: { [catId]: { mode: 'topk'|'threshold', topk: int, threshold: float } }
|
| 312 |
const catState = {};
|
| 313 |
+
let globalFloor = 0.0; // global minimum score (0–1), applied on top of per-category filters
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
function getCatState(catId) {
|
| 316 |
if (!catState[catId]) catState[catId] = { mode: 'threshold', topk: 20, threshold: 0.85 };
|
|
|
|
| 322 |
globalFloor = v / 100;
|
| 323 |
document.getElementById('global-thresh').value = v;
|
| 324 |
document.getElementById('global-thresh-num').value = v;
|
| 325 |
+
// re-apply filter to every rendered category
|
| 326 |
document.querySelectorAll('.cat-section').forEach(sec => {
|
| 327 |
applyFilter(parseInt(sec.dataset.catId));
|
| 328 |
});
|
|
|
|
| 333 |
const el = document.getElementById(`thnum-${catId}`);
|
| 334 |
if (el) el.value = pct;
|
| 335 |
}
|
|
|
|
| 336 |
function syncCatThreshRange(catId, pct) {
|
| 337 |
const el = document.getElementById(`range-${catId}`);
|
| 338 |
if (el) el.value = Math.max(1, Math.min(99, parseInt(pct) || 1));
|
| 339 |
}
|
| 340 |
|
| 341 |
+
// ---- PCA state ----
|
| 342 |
+
let _pcaEnabled = false;
|
| 343 |
+
let _lastPcaRequest = null; // { type: 'url'|'file', url?: string, file?: File }
|
| 344 |
+
|
| 345 |
function togglePca() {
|
| 346 |
_pcaEnabled = !_pcaEnabled;
|
| 347 |
const btn = document.getElementById('pca-toggle');
|
|
|
|
| 351 |
if (_pcaEnabled && _lastPcaRequest) runPca(_lastPcaRequest);
|
| 352 |
}
|
| 353 |
|
| 354 |
+
// runPca / runFromUrl / submitFile / submitFetch are replaced below by the
|
| 355 |
+
// @gradio/client module script once it loads. Stub versions here ensure
|
| 356 |
+
// the page does not error if the module hasn't finished loading yet.
|
| 357 |
+
function runPca(req) {}
|
| 358 |
+
function runFromUrl() {}
|
| 359 |
+
function submitFile(file) {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
+
function runFromFile(input) { if (input.files[0]) submitFile(input.files[0]); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
function setPreview(src, label) {
|
| 364 |
document.getElementById('preview-img').src = src;
|
|
|
|
| 368 |
|
| 369 |
// ---- rendering ----
|
| 370 |
|
| 371 |
+
// All tags from the last response, grouped by category
|
| 372 |
+
let _lastData = null;
|
| 373 |
+
|
| 374 |
function renderResults(data) {
|
| 375 |
hideError();
|
| 376 |
+
_lastData = data;
|
| 377 |
|
| 378 |
const container = document.getElementById('categories');
|
| 379 |
container.innerHTML = '';
|
|
|
|
| 401 |
<span class="mode-mini">
|
| 402 |
<button id="btn-topk-${cat.id}"
|
| 403 |
class="${state.mode==='topk'?'active':''}"
|
| 404 |
+
onclick="setCatMode(${cat.id},'topk')">top-k</button>
|
| 405 |
<button id="btn-thresh-${cat.id}"
|
| 406 |
class="${state.mode==='threshold'?'active':''}"
|
| 407 |
+
onclick="setCatMode(${cat.id},'threshold')">≥</button>
|
| 408 |
</span>
|
| 409 |
|
| 410 |
<span id="ctrl-topk-${cat.id}" style="display:${state.mode==='topk'?'flex':'none'};align-items:center;gap:.3rem">
|
| 411 |
<input type="number" id="topk-${cat.id}" value="${state.topk}" min="1" max="500"
|
| 412 |
style="width:52px"
|
| 413 |
+
oninput="setCatTopk(${cat.id},this.value)" />
|
| 414 |
</span>
|
| 415 |
|
| 416 |
<span id="ctrl-thresh-${cat.id}" style="display:${state.mode==='threshold'?'flex':'none'};align-items:center;gap:.3rem">
|
| 417 |
<input type="range" id="range-${cat.id}" min="1" max="99" step="1"
|
| 418 |
value="${Math.round(state.threshold*100)}"
|
| 419 |
+
oninput="setCatThreshold(${cat.id},this.value/100);syncCatThreshNum(${cat.id},this.value)" />
|
| 420 |
<input type="number" class="cat-thresh-num" id="thnum-${cat.id}"
|
| 421 |
min="1" max="99" step="1" value="${Math.round(state.threshold*100)}"
|
| 422 |
+
oninput="setCatThreshold(${cat.id},this.value/100);syncCatThreshRange(${cat.id},this.value)" />
|
| 423 |
<span class="cat-thresh-val" style="display:none" id="thval-${cat.id}">${state.threshold.toFixed(2)}</span>
|
| 424 |
</span>
|
| 425 |
</span>
|
| 426 |
|
| 427 |
<span class="cat-count" id="count-${cat.id}">0</span>
|
| 428 |
+
<span class="cat-chevron">▾</span>
|
| 429 |
`;
|
| 430 |
hdr.addEventListener('click', () => sec.classList.toggle('collapsed'));
|
| 431 |
|
|
|
|
| 436 |
body.innerHTML = `
|
| 437 |
<div class="cat-copy-bar">
|
| 438 |
<div class="cat-tag-string" id="cat-copy-${cat.id}"
|
| 439 |
+
onclick="copyCat(${cat.id})" title="Click to copy"></div>
|
| 440 |
<button class="cat-copy-btn" id="cat-btn-${cat.id}"
|
| 441 |
+
onclick="copyCat(${cat.id})">Copy</button>
|
| 442 |
</div>
|
| 443 |
<div class="tag-list" id="cat-tags-${cat.id}"></div>
|
| 444 |
`;
|
| 445 |
|
| 446 |
+
// build all pills (hidden ones will be toggled by applyFilter)
|
| 447 |
const pillContainer = body.querySelector(`#cat-tags-${cat.id}`);
|
| 448 |
for (const t of cat.tags) {
|
| 449 |
const pill = document.createElement('span');
|
|
|
|
| 469 |
updateGlobalCopyBar();
|
| 470 |
}
|
| 471 |
|
| 472 |
+
// Apply topk or threshold filter to a category's pills
|
| 473 |
function applyFilter(catId) {
|
| 474 |
const state = getCatState(catId);
|
| 475 |
const pills = document.querySelectorAll(`#cat-tags-${catId} .tag-pill`);
|
|
|
|
| 484 |
} else {
|
| 485 |
show = score >= state.threshold;
|
| 486 |
}
|
| 487 |
+
// global floor is always applied on top
|
| 488 |
if (score < globalFloor) show = false;
|
| 489 |
pill.classList.toggle('hidden', !show);
|
| 490 |
if (show) visible++;
|
| 491 |
});
|
| 492 |
|
| 493 |
+
// update count badge
|
| 494 |
const countEl = document.getElementById(`count-${catId}`);
|
| 495 |
if (countEl) countEl.textContent = visible;
|
| 496 |
|
| 497 |
+
// update copy bar text
|
| 498 |
const visibleTags = [...pills]
|
| 499 |
.filter(p => !p.classList.contains('hidden'))
|
| 500 |
.map(p => p.dataset.tag);
|
|
|
|
| 529 |
}
|
| 530 |
|
| 531 |
function updateGlobalCopyBar() {
|
| 532 |
+
// collect all visible tags across all categories, in category order
|
| 533 |
const allVisible = [];
|
| 534 |
document.querySelectorAll('.cat-section').forEach(sec => {
|
| 535 |
sec.querySelectorAll('.tag-pill:not(.hidden)').forEach(p => {
|
|
|
|
| 567 |
const el = document.getElementById('error-msg');
|
| 568 |
el.textContent = msg; el.style.display = 'block';
|
| 569 |
}
|
| 570 |
+
function hideError() { document.getElementById('error-msg').style.display = 'none'; }
|
|
|
|
|
|
|
|
|
|
| 571 |
|
| 572 |
document.getElementById('url-input').addEventListener('keydown', e => {
|
| 573 |
if (e.key === 'Enter') runFromUrl();
|
| 574 |
});
|
| 575 |
|
| 576 |
+
// drag & drop wired here so it can call submitFile after the module patches it
|
| 577 |
+
const dz = document.getElementById('drop-zone');
|
| 578 |
+
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag-over'); });
|
| 579 |
+
dz.addEventListener('dragleave', () => dz.classList.remove('drag-over'));
|
| 580 |
+
dz.addEventListener('drop', e => {
|
| 581 |
+
e.preventDefault(); dz.classList.remove('drag-over');
|
| 582 |
+
const file = e.dataTransfer.files[0];
|
| 583 |
+
if (file) submitFile(file);
|
| 584 |
+
});
|
| 585 |
+
</script>
|
| 586 |
+
|
| 587 |
+
<!-- @gradio/client module: patches runFromUrl / submitFile / runPca on window
|
| 588 |
+
to route through the Gradio queue (required for ZeroGPU on HF Spaces).
|
| 589 |
+
All rendering / UI logic above is untouched. -->
|
| 590 |
+
<script type="module">
|
| 591 |
+
import { client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
|
| 592 |
+
const gradioApp = await client(location.origin);
|
| 593 |
+
|
| 594 |
+
// ---- tag submission ----
|
| 595 |
+
|
| 596 |
+
window.runFromUrl = async function() {
|
| 597 |
+
const url = document.getElementById('url-input').value.trim();
|
| 598 |
+
if (!url) return;
|
| 599 |
+
setPreview(url, url);
|
| 600 |
+
_lastPcaRequest = { type: 'url', url };
|
| 601 |
+
if (_pcaEnabled) runPca(_lastPcaRequest);
|
| 602 |
+
setLoading(true);
|
| 603 |
+
try {
|
| 604 |
+
const res = await gradioApp.predict("/get_tags", {
|
| 605 |
+
image: url,
|
| 606 |
+
max_size: +document.getElementById('maxsize-input').value,
|
| 607 |
+
floor: globalFloor,
|
| 608 |
+
});
|
| 609 |
+
renderResults(JSON.parse(res.data[0]));
|
| 610 |
+
} catch (err) {
|
| 611 |
+
showError(String(err));
|
| 612 |
+
} finally {
|
| 613 |
+
setLoading(false);
|
| 614 |
+
}
|
| 615 |
+
};
|
| 616 |
+
|
| 617 |
+
window.submitFile = async function(file) {
|
| 618 |
+
const reader = new FileReader();
|
| 619 |
+
reader.onload = e => setPreview(e.target.result, file.name);
|
| 620 |
+
reader.readAsDataURL(file);
|
| 621 |
+
_lastPcaRequest = { type: 'file', file };
|
| 622 |
+
if (_pcaEnabled) runPca(_lastPcaRequest);
|
| 623 |
+
setLoading(true);
|
| 624 |
+
try {
|
| 625 |
+
const res = await gradioApp.predict("/get_tags", {
|
| 626 |
+
image: await handle_file(file),
|
| 627 |
+
max_size: +document.getElementById('maxsize-input').value,
|
| 628 |
+
floor: globalFloor,
|
| 629 |
+
});
|
| 630 |
+
renderResults(JSON.parse(res.data[0]));
|
| 631 |
+
} catch (err) {
|
| 632 |
+
showError(String(err));
|
| 633 |
+
} finally {
|
| 634 |
+
setLoading(false);
|
| 635 |
+
}
|
| 636 |
+
};
|
| 637 |
+
|
| 638 |
+
// ---- PCA ----
|
| 639 |
+
|
| 640 |
+
window.runPca = async function(req) {
|
| 641 |
+
const spinner = document.getElementById('pca-spinner');
|
| 642 |
+
const img = document.getElementById('pca-img');
|
| 643 |
+
spinner.style.display = 'block';
|
| 644 |
+
img.style.display = 'none';
|
| 645 |
+
const maxSize = +document.getElementById('maxsize-input').value;
|
| 646 |
+
try {
|
| 647 |
+
const imageArg = req.type === 'url' ? req.url : await handle_file(req.file);
|
| 648 |
+
const res = await gradioApp.predict("/get_pca", { image: imageArg, max_size: maxSize });
|
| 649 |
+
img.src = "data:image/png;base64," + res.data[0];
|
| 650 |
+
img.style.display = 'block';
|
| 651 |
+
} catch (err) {
|
| 652 |
+
console.error("PCA error:", err);
|
| 653 |
+
img.style.display = 'none';
|
| 654 |
+
} finally {
|
| 655 |
+
spinner.style.display = 'none';
|
| 656 |
+
}
|
| 657 |
};
|
| 658 |
</script>
|
| 659 |
</body>
|