Spaces:
Running on Zero
Running on Zero
Download app.py from BitKnight/DuplexJev-Vision: direct link, hf CLI and curl.
- Browser
- Download file 8.68 kB
-
https://huggingface.co/spaces/BitKnight/DuplexJev-Vision/resolve/main/app.py
- Command line
-
hf download hf://spaces/BitKnight/DuplexJev-Vision/app.py
-
curl -L -o app.py https://huggingface.co/spaces/BitKnight/DuplexJev-Vision/resolve/main/app.py
8.68 kB
| """DuplexJev-32B-Vision on ZeroGPU: judge what it hears against what it sees, in one forward pass.""" | |
| import os | |
| import time | |
| import gradio as gr | |
| import numpy as np | |
| import spaces | |
| MODEL_ID = os.environ.get("DUPLEXJEV_VISION_MODEL", "adventists-ai/DuplexJev-32B-Vision") | |
| N_PERM = int(os.environ.get("DUPLEXJEV_NPERM", "2")) # answers averaged over both option orders, as on the model card | |
| MOCK = os.environ.get("DUPLEXJEV_MOCK") == "1" | |
| SR = 16000 | |
| if MOCK: | |
| class _Mock: | |
| def decide(self, audio=None, image=None, questions=None, n_perm=2): | |
| rng = np.random.default_rng(len(questions)) | |
| out = {} | |
| for q in questions: | |
| p = rng.dirichlet(np.ones(len(q["options"]))) | |
| out[q["id"]] = {"answer": q["options"][int(p.argmax())], "confidence": float(p.max()), | |
| "probs": dict(zip(q["options"], map(float, p)))} | |
| return out | |
| VD = _Mock() | |
| else: | |
| from duplexjev.vision import VisionDecider | |
| VD = VisionDecider.from_pretrained(MODEL_ID, device="cuda") # on cuda at import time, as ZeroGPU expects | |
| PRESETS = { | |
| "Face ↔ voice · 表情和语气": [ | |
| ("face matches voice", "Does the face in the picture show the same emotion as the speaker's voice?", ["Yes", "No"]), | |
| ("face emotion", "What emotion does the face in the picture show?", ["happy", "angry", "sad"]), | |
| ("voice emotion", "What is the speaker's emotional state?", ["neutral", "happy", "angry", "sad"]), | |
| ("speaker gender", "What is the perceived gender of the speaker?", ["female", "male"]), | |
| ], | |
| "Red word ↔ stress · 标红词和重读": [ | |
| ("red word stressed", "Is the word in red in the picture the word the speaker stresses?", ["Yes", "No"]), | |
| ], | |
| "Caption ↔ sound · 文字说明和声音": [ | |
| ("caption matches", "Does the caption in the picture describe the sound you hear?", ["Yes", "No"]), | |
| ("sound", "Besides speech, which sound can be heard in this audio?", | |
| ["laughter", "breathing", "coughing", "a sigh", "None of these"]), | |
| ], | |
| } | |
| def preset_text(name): | |
| return "\n".join(f"{i} | {q} | " + "; ".join(o) for i, q, o in PRESETS[name]) | |
| def parse(text): | |
| qs = [] | |
| for n, line in enumerate(l for l in text.splitlines() if l.strip()): | |
| parts = [p.strip() for p in line.split("|")] | |
| if len(parts) < 3: | |
| raise gr.Error(f"Line {n + 1}: use name | question | option 1; option 2; …") | |
| opts = [o.strip() for o in parts[2].split(";") if o.strip()] | |
| if len(opts) < 2: | |
| raise gr.Error(f"Line {n + 1}: give at least two options, separated by ';'") | |
| qs.append({"id": parts[0] or f"q{n + 1}", "text": parts[1], "options": opts}) | |
| if not qs: | |
| raise gr.Error("Add at least one question.") | |
| if len(qs) > 8: | |
| raise gr.Error("At most 8 questions per request in this demo.") | |
| return qs | |
| def _to16k(audio): | |
| from math import gcd | |
| from scipy.signal import resample_poly | |
| sr, x = audio | |
| x = np.asarray(x) | |
| if x.dtype.kind == "i": | |
| x = x.astype(np.float32) / float(np.iinfo(x.dtype).max + 1) | |
| x = x.astype(np.float32) | |
| if x.ndim == 2: | |
| x = x.mean(axis=1) | |
| if sr != SR: | |
| g = gcd(int(sr), SR) | |
| x = resample_poly(x, SR // g, int(sr) // g).astype(np.float32) | |
| return x[: SR * 15] | |
| # 33 B in bf16 needs the full 96 GB card; a short reservation keeps free runs | |
| def _run(x, img, qs): | |
| t = time.perf_counter() | |
| res = VD.decide(audio=x, image=img, questions=qs) if MOCK else VD.decide(audio=x, image=img, questions=qs, n_perm=N_PERM) | |
| return res, (time.perf_counter() - t) * 1000 | |
| def decide(image, audio, text): | |
| if image is None and audio is None: | |
| raise gr.Error("Add a picture and/or a voice clip (or pick an example).") | |
| x = _to16k(audio) if audio is not None else None | |
| qs = parse(text) | |
| res, ms = _run(x, image, qs) | |
| rows = [] | |
| for q in qs: | |
| r = res[q["id"]] | |
| others = " · ".join(f"{o} {p:.0%}" for o, p in sorted(r["probs"].items(), key=lambda kv: -kv[1]) if o != r["answer"]) | |
| rows.append([q["id"], r["answer"], f"{r['confidence']:.0%}", others]) | |
| head = f"### {len(qs)} decisions about the voice + picture · 0 decode steps · **{ms:.0f} ms** on the GPU" | |
| return head, rows | |
| R = "examples/" | |
| EXAMPLES = [ | |
| [R + "rp_1072_TSI_HAP_XX.jpg", R + "rp_1072_TSI_HAP_XX.wav", "Face ↔ voice · 表情和语气"], | |
| [R + "rp_1072_TSI_SAD_XX.jpg", R + "rp_1072_TSI_HAP_XX.wav", "Face ↔ voice · 表情和语气"], | |
| [R + "rp_1072_TSI_ANG_XX.jpg", R + "rp_1072_TSI_ANG_XX.wav", "Face ↔ voice · 表情和语气"], | |
| [R + "rp_1085_WSI_HAP_XX.jpg", R + "rp_1085_WSI_ANG_XX.wav", "Face ↔ voice · 表情和语气"], | |
| [R + "rp_1074_MTI_SAD_XX.jpg", R + "rp_1074_MTI_SAD_XX.wav", "Face ↔ voice · 表情和语气"], | |
| [R + "rp_1078_ITS_HAP_XX.jpg", R + "rp_1078_ITS_SAD_XX.wav", "Face ↔ voice · 表情和语气"], | |
| [R + "v11_st_red_he.png", R + "v11_st_he.wav", "Red word ↔ stress · 标红词和重读"], | |
| [R + "v11_st_red_money.png", R + "v11_st_he.wav", "Red word ↔ stress · 标红词和重读"], | |
| [R + "v11_cap_laugh.png", R + "v11_laugh.wav", "Caption ↔ sound · 文字说明和声音"], | |
| [R + "v11_cap_laugh.png", R + "v11_cough.wav", "Caption ↔ sound · 文字说明和声音"], | |
| ] | |
| LABELS = ["real photo · happy face + happy voice", "same person, sad face + happy voice", "real photo · angry face + angry voice", | |
| "happy face + angry voice", "real photo · sad face + sad voice", "happy face + sad voice", | |
| "“I didn't say HE…” · red word: he", "“I didn't say HE…” · red word: money", "caption “laughter” + laughter", | |
| "caption “laughter” + a cough"] | |
| INTRO = """ | |
| # 👁️🎧 DuplexJev-32B-Vision — it hears, and now it sees | |
| **One forward pass over a voice and a picture.** Ask multiple-choice questions that need both: *does the face match the | |
| voice? is the red word the one they stress? does the caption describe the sound?* Each answer is one constrained token — | |
| nothing is decoded. Research preview · [model card](https://huggingface.co/adventists-ai/DuplexJev-32B-Vision) · | |
| ⭐ [GitHub](https://github.com/adventists-ai/duplexjev) · speech-only demo: [DuplexJev](https://huggingface.co/spaces/BitKnight/DuplexJev) · | |
| 🏢 commercial licence & hosted API: **jiejin@adventists.ai** | |
| <sub>⏱️ The first run after the Space has been idle takes a few seconds while Hugging Face attaches a GPU. Free GPU time per visitor | |
| is limited by Hugging Face — [log in](https://huggingface.co/login) for more runs. 首次运行需要几秒分配 GPU;登录 Hugging Face 可获得更多免费次数。</sub> | |
| """ | |
| FOOT = """ | |
| <sub>Model: DuplexJev-32B-Vision (CC BY-NC 4.0, research preview); answers averaged over both option orders. Questions work best in English; | |
| speech can be English or Chinese. One picture per request. Real-photo clips: CREMA-D (ODbL / DbCL), actors not seen in training. | |
| Pictures and audio are processed for this request only and not stored. Do not use the outputs to make decisions about individuals.</sub> | |
| """ | |
| with gr.Blocks(title="DuplexJev-32B-Vision — voice + picture decisions", theme=gr.themes.Soft(primary_hue="emerald")) as demo: | |
| gr.Markdown(INTRO) | |
| with gr.Row(): | |
| with gr.Column(scale=5): | |
| with gr.Row(): | |
| image = gr.Image(type="pil", sources=["upload", "webcam", "clipboard"], label="Picture", height=260) | |
| audio = gr.Audio(sources=["microphone", "upload"], type="numpy", label="Voice (≤ 15 s)") | |
| preset = gr.Radio(list(PRESETS), value="Face ↔ voice · 表情和语气", label="Question set") | |
| with gr.Accordion("Questions (editable): name | question | option 1; option 2; …", open=False): | |
| table = gr.Textbox(value=preset_text("Face ↔ voice · 表情和语气"), lines=6, show_label=False) | |
| btn = gr.Button("Decide ⚡", variant="primary") | |
| gr.Examples(EXAMPLES, inputs=[image, audio, preset], example_labels=LABELS, label="Examples") | |
| with gr.Column(scale=5): | |
| head = gr.Markdown("### Results appear here") | |
| out = gr.Dataframe(headers=["decision", "answer", "confidence", "other options"], wrap=True, | |
| interactive=False, column_widths=["24%", "26%", "14%", "36%"]) | |
| gr.Markdown(FOOT) | |
| preset.change(preset_text, preset, table) | |
| btn.click(decide, [image, audio, table], [head, out], api_name="decide") | |
| if __name__ == "__main__": | |
| demo.queue(max_size=40).launch() | |