File size: 9,037 Bytes
5e05e17 8bf3976 ee7cfca 8bf3976 5e05e17 249e1f3 5e05e17 4e019a3 5e05e17 4e019a3 5e05e17 4e019a3 5e05e17 adbdec4 5e05e17 b94bfb3 4e019a3 5e05e17 4e019a3 5e05e17 4e019a3 5e05e17 4e019a3 5e05e17 249e1f3 5e05e17 249e1f3 5e05e17 249e1f3 5e05e17 6058ec5 b94bfb3 5e05e17 ee7cfca 5e05e17 c8f7229 5e05e17 249e1f3 5e05e17 4e019a3 249e1f3 5e05e17 4e019a3 5e05e17 c8f7229 | 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 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | """DuplexJev on ZeroGPU: ten voice-agent decisions from raw speech in one forward pass."""
import os
import time
import gradio as gr
import numpy as np
import spaces
from duplexjev import Question, default_table
from duplexjev.table import LABELS
MODEL_ID = os.environ.get("DUPLEXJEV_MODEL", "adventists-ai/DuplexJev-32B-Para")
N_PERM = int(os.environ.get("DUPLEXJEV_NPERM", "1")) # 2 = average over option orders (same answers in our tests, 1.5x slower)
GPU_SIZE = os.environ.get("DUPLEXJEV_GPU", "xlarge") # 32B in bf16 needs the full 96 GB card
MOCK = os.environ.get("DUPLEXJEV_MOCK") == "1" # local UI test without a GPU
if MOCK:
class _Mock:
def decide(self, audio, qs):
rng = np.random.default_rng(len(qs))
out = {}
for q in qs:
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
DECIDER = _Mock()
else:
from duplexjev import Decider
DECIDER = Decider.from_pretrained(MODEL_ID, device="cuda") # placed on cuda at import time, as ZeroGPU expects
SR = 16000
# Chinese option / label -> English gloss, so the page reads in English whatever the speech language
from duplexjev.table import _EN, _ZH # same ids and option order, except gender
ZH2EN = {}
for (_i, _q, _eo), (_j, _zq, _zo) in zip(_EN, _ZH):
if _i == "gender":
ZH2EN.update({"男性": "male", "女性": "female"})
else:
ZH2EN.update(dict(zip(_zo, _eo)))
ZH2EN.update({"开口回应": "reply", "不出声,继续等(这不是对助手说的)": "stay silent (not addressed to the assistant)"})
LABEL2EN = {LABELS["zh"][k]: LABELS["en"][k] for k in LABELS["zh"]}
LABEL2EN["是不是对助手说"] = "addressed to the assistant?"
def _en(x, table):
return f"{table[x]} · {x}" if x in table else x
def _to16k(audio):
"""Gradio gives (sr, int16 array); resample with a polyphase filter (plain interpolation aliases and makes
speech sound harsher, which skews emotion and speaking-style answers)."""
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 * 20] # at most 20 s
def table_text(lang):
rows = []
for q in default_table(lang):
rows.append(f"{LABELS[lang].get(q.id, q.id)} | {q.text} | " + "; ".join(q.options))
if lang == "zh":
rows.append("是不是对助手说 | 助手已经讲完,正在等用户。听到这段声音,助手该怎么做? | 开口回应; 不出声,继续等(这不是对助手说的)")
return "\n".join(rows)
def parse_table(text, lang):
qs = []
for i, 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 {i + 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 {i + 1}: give at least two options, separated by ';'")
qs.append(Question(parts[0] or f"q{i + 1}", parts[1], opts, lang=lang))
if not qs:
raise gr.Error("Add at least one question.")
if len(qs) > 16:
raise gr.Error("At most 16 questions per clip in this demo.")
return qs
@spaces.GPU(duration=6, size=GPU_SIZE) # a call takes ~0.3 s; a short reservation leaves visitors more free runs
def _run(x, qs, n_perm):
t = time.perf_counter()
res = DECIDER.decide(x, qs) if MOCK else DECIDER.decide(x, qs, n_perm=n_perm)
return res, (time.perf_counter() - t) * 1000
def decide(audio, lang, text, n_perm=None):
if audio is None:
raise gr.Error("Record or upload a clip first (or pick an example).")
x = _to16k(audio)
if len(x) < SR * 0.2:
raise gr.Error("The clip is too short.")
qs = parse_table(text, lang)
res, ms = _run(x, qs, int(n_perm or N_PERM))
rows = []
for q in qs:
r = res[q.id]
tr = (lambda o: ZH2EN.get(o, o)) if lang == "zh" else (lambda o: o)
others = " · ".join(f"{tr(o)} {p:.0%}" for o, p in sorted(r["probs"].items(), key=lambda kv: -kv[1]) if o != r["answer"])
name = _en(q.id, LABEL2EN) if lang == "zh" else q.id
ans = _en(r["answer"], ZH2EN) if lang == "zh" else r["answer"]
rows.append([name, ans, f"{r['confidence']:.0%}", others])
head = f"### {len(qs)} decisions · 1 forward pass · 0 decode steps · **{ms:.0f} ms** on the GPU"
if lang == "zh":
head += f"\n<sub>Chinese speech: the questions use the Chinese wording the model was trained with; answers shown in English · 中文。{len(qs)} 个判断,GPU 上 {ms:.0f} 毫秒。</sub>"
return head, rows
def set_lang(lang):
return table_text(lang)
EX_EN = [
["examples/en_barge_in.wav", "en"],
["examples/en_unfinished.wav", "en"],
["examples/en_backchannel.wav", "en"],
["examples/laughter.wav", "en"],
]
EX_ZH = [
["examples/zh_wait.wav", "zh"],
["examples/zh_side_talk.wav", "zh"],
["examples/zh_happy.wav", "zh"],
["examples/zh_complete.wav", "zh"],
]
INTRO = """
# 🎧 DuplexJev — stop transcribing to decide
**The decision layer for voice agents.** Give it a clip and the questions your agent needs answered — *has the user
finished? should I stop talking? are they angry?* Every answer comes from **one forward pass**: no ASR, no decoding.
10 decisions in 92 ms, 21× faster than ASR → LLM ([paper](https://arxiv.org/abs/2610.02638)).
⭐ [GitHub](https://github.com/adventists-ai/duplexjev) · 🤗 [Models](https://huggingface.co/adventists-ai) ·
🔌 Pipecat: `pip install pipecat-duplexjev` · LiveKit: `pip install livekit-plugins-duplexjev` ·
👁️ **New:** voice + picture together → [DuplexJev-Vision demo](https://huggingface.co/spaces/BitKnight/DuplexJev-Vision) · 🏢 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; after that each run takes about 0.3 s on the GPU.
Free GPU time per visitor is limited by Hugging Face — [log in](https://huggingface.co/login) for more runs.
首次运行需要几秒分配 GPU,之后每次约 0.3 秒;登录 Hugging Face 可获得更多免费次数。</sub>
"""
FOOT = """
<sub>Model: DuplexJev-32B-Para (weights CC BY-NC 4.0), one full ZeroGPU card. Audio is processed for this request only and not stored.
Edit the questions freely: one per line, `name | question | option 1; option 2; …`.
GPU time shown excludes the queue and network. 音频只用于本次判断,不保存。</sub>
"""
with gr.Blocks(title="DuplexJev — speech to decisions", theme=gr.themes.Soft(primary_hue="emerald")) as demo:
gr.Markdown(INTRO)
with gr.Row():
with gr.Column(scale=5):
audio = gr.Audio(sources=["microphone", "upload"], type="numpy", label="Speech clip (≤ 20 s)")
lang = gr.Radio([("English speech", "en"), ("Chinese speech · 中文语音", "zh")], value="en", label="Speech language (picks the question wording) / 语音语言")
with gr.Accordion("Questions (editable) / 判断题(可编辑)", open=False):
table = gr.Textbox(value=table_text("en"), lines=11, show_label=False)
btn = gr.Button("Decide ⚡", variant="primary")
nperm = gr.Number(value=N_PERM, visible=False)
gr.Examples(EX_EN, inputs=[audio, lang], label="🇬🇧 English speech examples",
example_labels=["angry barge-in: “Wait, wait — stop…”", "stops mid-request: “…and, um —”", "“mm-hmm, yeah” backchannel", "laughter"])
gr.Examples(EX_ZH, inputs=[audio, lang], label="🇨🇳 Chinese speech examples · 中文语音示例 (answers in English · 中文)",
example_labels=["“Wait, let me think” · 等一下我想想", "back-seat chat, not to the assistant · 后排闲聊", "happy thanks · 开心道谢", "navigation request · 导航请求"])
with gr.Column(scale=6):
head = gr.Markdown("### Results appear here")
out = gr.Dataframe(headers=["decision", "answer", "confidence", "other options"], wrap=True,
interactive=False, max_height=1000, column_widths=["18%", "34%", "13%", "35%"])
gr.Markdown(FOOT)
lang.change(set_lang, lang, table)
btn.click(decide, [audio, lang, table, nperm], [head, out], api_name="decide")
if __name__ == "__main__":
demo.queue(max_size=40).launch()
|