Spaces:
Running on Zero
Running on Zero
File size: 8,680 Bytes
413215d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | """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]
@spaces.GPU(duration=10, size="xlarge") # 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()
|