Spaces:
Sleeping
Sleeping
sync to e756766+9ee7f46: shared gate detection, fps-scaled gate timings, session hardening UI; adapt engine to new gate API
Browse files- .gitattributes +1 -0
- rv2v_reference/nailong.png +3 -0
- static/index.html +18 -52
- xvideo/models/vae/vae.py +4 -1
- xvideo/serving/serve_joyomni_streaming.py +169 -128
- xvideo/serving/zerogpu_engine.py +95 -78
.gitattributes
CHANGED
|
@@ -40,3 +40,4 @@ rv2v_reference/5e178546-3ebf-40df-bb86-01613dd96c3b.png filter=lfs diff=lfs merg
|
|
| 40 |
wheels/joyomni_ops-0.1.0-cp310-cp310-linux_x86_64.whl filter=lfs diff=lfs merge=lfs -text
|
| 41 |
wheels/sageattention-2.2.0-cp310-cp310-linux_x86_64.whl filter=lfs diff=lfs merge=lfs -text
|
| 42 |
libs/libstdc++.so.6 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 40 |
wheels/joyomni_ops-0.1.0-cp310-cp310-linux_x86_64.whl filter=lfs diff=lfs merge=lfs -text
|
| 41 |
wheels/sageattention-2.2.0-cp310-cp310-linux_x86_64.whl filter=lfs diff=lfs merge=lfs -text
|
| 42 |
libs/libstdc++.so.6 filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
rv2v_reference/nailong.png filter=lfs diff=lfs merge=lfs -text
|
rv2v_reference/nailong.png
ADDED
|
Git LFS Details
|
static/index.html
CHANGED
|
@@ -243,8 +243,7 @@
|
|
| 243 |
<span id="upqTier" class="qtier">
|
| 244 |
<button type="button" data-upq="0.2" data-i18n="q_low">低</button>
|
| 245 |
<button type="button" data-upq="0.4" data-i18n="q_mid">中</button>
|
| 246 |
-
<button type="button" data-upq="0.6" data-i18n="q_high">高</button>
|
| 247 |
-
<button type="button" data-upq="auto" class="on" data-i18n="q_auto">自动</button>
|
| 248 |
</span>
|
| 249 |
</span>
|
| 250 |
<span class="kvreset-field keep-min">
|
|
@@ -699,7 +698,6 @@ let downDelayMs = null;
|
|
| 699 |
let downSkewMin = null;
|
| 700 |
let downSkewAt = 0;
|
| 701 |
let upClamped = false;
|
| 702 |
-
let upAuto = true;
|
| 703 |
let upClearStreak = 0;
|
| 704 |
let upClearNeed = 4;
|
| 705 |
let upDrainFactor = 1;
|
|
@@ -765,7 +763,6 @@ function applyOrientation(o) {
|
|
| 765 |
const dims = orientDims(o);
|
| 766 |
document.getElementById("width").value = dims.width;
|
| 767 |
document.getElementById("height").value = dims.height;
|
| 768 |
-
autoUpQuality();
|
| 769 |
updateCropMask();
|
| 770 |
updateMetrics();
|
| 771 |
}
|
|
@@ -972,7 +969,6 @@ function updateMetrics() {
|
|
| 972 |
netReadoutEl.textContent = t("net_label") + " " + fmt(_rtt, "ms") + _tail;
|
| 973 |
netReadoutEl.classList.toggle("net-warn", _hi || _fd || _uc);
|
| 974 |
}
|
| 975 |
-
if (upAuto) highlightUpqTier("auto");
|
| 976 |
if (dispLatencyEl) {
|
| 977 |
dispLatencyEl.textContent = t("disp_label") + " " + fmt(editLatencyLastMs, "ms");
|
| 978 |
}
|
|
@@ -1662,18 +1658,13 @@ async function beginSession() {
|
|
| 1662 |
seed: SERVER_DEFAULTS.seed,
|
| 1663 |
kv_reset_frames: Math.max(0, Math.floor(Number(document.getElementById("kvResetFrames").value) || 0)),
|
| 1664 |
output_quality: Number(downQualityUi.value),
|
|
|
|
| 1665 |
output_codec: h264DecodeOk ? "h264" : "mjpeg",
|
| 1666 |
-
input_codec: upCodecH264 ? "h264" : "mjpeg"
|
| 1667 |
-
no_person_blank: faceGateDebugEnabled(),
|
| 1668 |
-
require_face: faceGateDebugEnabled(),
|
| 1669 |
-
person_count_reedit: faceGateDebugEnabled()
|
| 1670 |
};
|
| 1671 |
if (usingVideoFile) {
|
| 1672 |
startPayload.source = "file";
|
| 1673 |
startPayload.gate_enabled = false;
|
| 1674 |
-
startPayload.no_person_blank = false;
|
| 1675 |
-
startPayload.require_face = false;
|
| 1676 |
-
startPayload.person_count_reedit = false;
|
| 1677 |
}
|
| 1678 |
if (cachedEnhancedPrompt) startPayload.enhanced_prompt = cachedEnhancedPrompt;
|
| 1679 |
if (refImage) startPayload.ref_image = refImage;
|
|
@@ -1921,6 +1912,7 @@ async function start() {
|
|
| 1921 |
}
|
| 1922 |
return;
|
| 1923 |
}
|
|
|
|
| 1924 |
const meta = pendingOutputMeta;
|
| 1925 |
pendingOutputMeta = null;
|
| 1926 |
receivedFrames += 1;
|
|
@@ -2010,6 +2002,7 @@ async function send() {
|
|
| 2010 |
try { if (usingVideoFile) camera.pause(); } catch (err) {}
|
| 2011 |
clearResultVideo();
|
| 2012 |
clearOutputQueue();
|
|
|
|
| 2013 |
const ok = await beginSession();
|
| 2014 |
if (ok) return;
|
| 2015 |
startingRun = false;
|
|
@@ -2024,13 +2017,13 @@ sendBtn.onclick = () => { suppressPeThisSend = false; send(); };
|
|
| 2024 |
const PROMPT_GROUPS = [
|
| 2025 |
{
|
| 2026 |
key: "change_clothes",
|
| 2027 |
-
label: "实时换装", label_en: "
|
| 2028 |
cases: [
|
| 2029 |
{ title: "白T恤", title_en: "White Tee", desc: "上衣换成白色圆领纯棉T恤", desc_en: "Top → white crew-neck cotton tee", text: "将主角的上衣换成白色圆领纯棉 T 恤。", text_en: "Replace the subject's top with a white crew-neck cotton T-shirt." },
|
| 2030 |
{ title: "皮夹克", title_en: "Leather Jacket", desc: "外套换成哑光黑色皮质机车夹克", desc_en: "Jacket → matte black leather biker jacket", text: "将主角的外套换成哑光黑色皮质机车夹克。", text_en: "Replace the subject's jacket with a matte black leather biker jacket." },
|
| 2031 |
{ title: "汉服", title_en: "Hanfu", desc: "换成传统汉服交领右衽上衣", desc_en: "Traditional cross-collar Hanfu top", text: "将主角的服装换成传统汉服交领右衽上衣。", text_en: "Replace the subject's outfit with a traditional cross-collar (right-lapel) Hanfu top." },
|
| 2032 |
{ title: "学院风衬衫", title_en: "Preppy Shirt", desc: "白衬衫配藏青V领马甲加酒红领带", desc_en: "White shirt, navy V-vest, wine tie", text: "将主角的上衣换成白衬衫搭配藏青色 V 领马甲和酒红色领带。", text_en: "Replace the subject's top with a white shirt paired with a navy V-neck vest and a wine-red tie." },
|
| 2033 |
-
{ title: "京东定制帽 + 墨镜", title_en: "JD Cap + Shades", desc: "戴上“京东”的红色鸭舌帽和墨镜", desc_en: "Red cap reading “京东” plus sunglasses", text: "给主角戴上写着“京东”两个汉字的红色鸭舌帽和墨镜。", text_en: "Put a red cap printed with the Chinese characters \"京东\" and a pair of sunglasses on the subject." },
|
| 2034 |
],
|
| 2035 |
},
|
| 2036 |
{
|
|
@@ -2041,6 +2034,7 @@ const PROMPT_GROUPS = [
|
|
| 2041 |
{ title: "红色围巾", title_en: "Red Scarf", desc: "围上参考图里的红色流苏围巾", desc_en: "Red tasseled scarf from the reference image", ref: "scarf", text: "将参考图中的红色流苏围巾围到主角的颈部。", text_en: "Wrap the red tasseled scarf from the reference image around the subject's neck." },
|
| 2042 |
{ title: "粉色T恤", title_en: "Pink Tee", desc: "换上参考图里的粉色短袖 T 恤", desc_en: "Pink short-sleeve tee from the reference image", ref: "pink_tee", text: "将主角的衣服换成参考图里的粉色短袖 T 恤。", text_en: "Replace the subject's clothing with the pink short-sleeve T-shirt from the reference image." },
|
| 2043 |
{ title: "橙色眼镜", title_en: "Orange Glasses", desc: "戴上参考图里的橙色镜片眼镜", desc_en: "Orange-tinted glasses from the reference image", ref: "orange_glasses", text: "给主角戴上参考图里的橙色镜片眼镜。", text_en: "Put the orange-tinted glasses from the reference image on the subject." },
|
|
|
|
| 2044 |
],
|
| 2045 |
},
|
| 2046 |
{
|
|
@@ -2049,7 +2043,7 @@ const PROMPT_GROUPS = [
|
|
| 2049 |
cases: [
|
| 2050 |
{ title: "3D动画", title_en: "3D Animation", desc: "渲染为3D动画电影风格", desc_en: "Render as 3D animated-film style", text: "将整个画面渲染为3D动画电影风格。应用平滑的三维建模质感、柔和的全局光照和景深,以及皮克斯风格的角色与场景渲染。", text_en: "Render the entire scene in a 3D animated-film style. Apply smooth 3D-modeled surfaces, soft global illumination and depth of field, and Pixar-style character and scene rendering." },
|
| 2051 |
{ title: "油画质感", title_en: "Oil Painting", desc: "转成梵高厚涂油画风格", desc_en: "Van Gogh impasto oil-painting style", text: "将整个画面转换为梵高的画的风格。应用厚重、富有表现力的笔触,鲜艳且强烈的色彩,天空中和风景中动态旋转的纹理,以及梵高画作中特有的粗放油彩(厚涂法)效果。", text_en: "Convert the entire scene into the style of a Van Gogh painting. Apply thick, expressive brushstrokes, vivid and intense colors, dynamic swirling textures in the sky and landscape, and the coarse impasto oil effect characteristic of Van Gogh's work." },
|
| 2052 |
-
{ title: "像素
|
| 2053 |
{ title: "日漫赛璐璐", title_en: "Anime Cel", desc: "转成日式动漫赛璐璐风格", desc_en: "Japanese anime cel-shaded style", text: "将整个画面转换为日式动漫赛璐璐风格:干净的黑色描边、大面积平涂的纯色色块、明确的明暗二分硬边阴影和简洁的高光。", text_en: "Convert the entire scene into a Japanese anime cel-shaded style: clean black outlines, large flat areas of solid color, clear hard-edged two-tone shading, and simple highlights." },
|
| 2054 |
],
|
| 2055 |
},
|
|
@@ -2057,10 +2051,10 @@ const PROMPT_GROUPS = [
|
|
| 2057 |
key: "face_edit",
|
| 2058 |
label: "外貌改造", label_en: "Look Edit",
|
| 2059 |
cases: [
|
| 2060 |
-
{ title: "戴上墨镜", title_en: "Sunglasses", desc: "戴一副粉色飞行员墨镜", desc_en: "Add pink aviator sunglasses", text: "给主角添加一副粉色飞行员墨镜架在鼻梁上。", text_en: "Add a pair of pink aviator sunglasses resting on the subject's nose." },
|
| 2061 |
{ title: "戴口罩", title_en: "Face Mask", desc: "脸上戴一个医用口罩", desc_en: "Add a medical face mask", text: "给主角的脸上戴一个医用口罩。", text_en: "Put a medical face mask on the subject's face." },
|
| 2062 |
{ title: "加耳环", title_en: "Earrings", desc: "双耳加一对金色圆环耳环", desc_en: "Add a pair of gold hoop earrings", text: "给主角的双耳耳垂添加一对抛光金色圆环耳环。", text_en: "Add a pair of polished gold hoop earrings to the subject's earlobes." },
|
| 2063 |
-
{ title: "加络腮胡", title_en: "Beard", desc: "下颌脸颊加浓密络腮胡", desc_en: "Add a thick full beard", text: "给主角的
|
| 2064 |
],
|
| 2065 |
},
|
| 2066 |
{
|
|
@@ -2069,9 +2063,8 @@ const PROMPT_GROUPS = [
|
|
| 2069 |
cases: [
|
| 2070 |
{ title: "大理石雕塑", title_en: "Marble Statue", desc: "变成白色大理石雕塑", desc_en: "Turn into a white marble statue", text: "将主角整体转化为白色大理石雕塑:可见石材纹理和细微灰色脉络、表面呈抛光光泽、光线在曲面形成柔和高光,眼睛和头发同为石质,保持原有动作但带有雕塑般的质感。", text_en: "Transform the subject entirely into a white marble statue: visible stone texture and fine gray veining, a polished glossy surface, soft highlights on the curved surfaces, eyes and hair also made of stone, keeping the original motion but with a sculptural quality." },
|
| 2071 |
{ title: "乐高小人", title_en: "LEGO Figure", desc: "变成乐高积木小人", desc_en: "Turn into a LEGO minifigure", text: "将主角转化为乐高积木小人风格:黄色圆柱形头部、表面为塑料亚光质感,五官简化为印刷贴纸式的黑色圆点眼睛和弧线微笑。", text_en: "Transform the subject into a LEGO minifigure style: a yellow cylindrical head with a matte plastic surface, facial features simplified into printed-sticker-style black dot eyes and a curved smile." },
|
| 2072 |
-
{ title: "赛博机械体", title_en: "Cyborg", desc: "变成
|
| 2073 |
{ title: "黏土动画", title_en: "Claymation", desc: "变成定格黏土动画角色", desc_en: "Turn into a stop-motion clay character", text: "将主角转化为定格黏土动画角色:表面可见手工捏制的指纹压痕和不规则质感、色彩饱和但略带哑光、比例微微卡通化(头部略大)。", text_en: "Transform the subject into a stop-motion claymation character: visible hand-molded fingerprint dents and irregular texture, saturated but slightly matte colors, and mildly cartoonish proportions (a slightly larger head)." },
|
| 2074 |
-
{ title: "机器人", title_en: "Robot", desc: "变成机器人", desc_en: "Turn into a robot", text: "将主角变成一个机器人。", text_en: "Turn the subject into a robot." },
|
| 2075 |
],
|
| 2076 |
},
|
| 2077 |
{
|
|
@@ -2085,12 +2078,12 @@ const PROMPT_GROUPS = [
|
|
| 2085 |
},
|
| 2086 |
{
|
| 2087 |
key: "swap_hero",
|
| 2088 |
-
label: "主角变身", label_en: "
|
| 2089 |
cases: [
|
| 2090 |
{ title: "梅西", title_en: "Messi", desc: "主角变成球星梅西", desc_en: "Turn the subject into Lionel Messi", text: "将主角变成足球明星梅西(Lionel Messi)的样貌。", text_en: "Change the subject's appearance into that of the football star Lionel Messi." },
|
| 2091 |
{ title: "詹姆斯", title_en: "LeBron", desc: "主角变成球星詹姆斯", desc_en: "Turn the subject into LeBron James", text: "将主角变成篮球明星勒布朗·詹姆斯(LeBron James)的样貌。", text_en: "Change the subject's appearance into that of the basketball star LeBron James." },
|
| 2092 |
{ title: "特朗普", title_en: "Trump", desc: "主角变成特朗普", desc_en: "Turn the subject into Donald Trump", text: "将主角变成美国总统特朗普(Donald Trump)的样貌。", text_en: "Change the subject's appearance into that of U.S. President Donald Trump." },
|
| 2093 |
-
{ title: "奥巴马", title_en: "Obama", desc: "主角变成奥巴马", desc_en: "Turn the subject into Barack Obama", text: "将主角变成美国总统奥巴马(Barack Obama)的样貌。", text_en: "Change the subject's appearance into that of U.S. President Barack Obama." },
|
| 2094 |
{ title: "奥特曼", title_en: "Ultraman", desc: "主角变成奥特曼", desc_en: "Turn the subject into Ultraman", text: "将主角的脸和身体变成奥特曼。", text_en: "Change the subject's face and body into Ultraman." },
|
| 2095 |
{ title: "拉布拉多", title_en: "Labrador", desc: "主角变成拉布拉多", desc_en: "Turn the subject into a Labrador", text: "将主角的脸和身体变成一只拉布拉多犬。", text_en: "Change the subject's face and body into a Labrador dog." },
|
| 2096 |
],
|
|
@@ -2542,43 +2535,18 @@ const qualityLegacyEl = document.getElementById("quality");
|
|
| 2542 |
function highlightUpqTier(v) {
|
| 2543 |
if (!upqTierEl) return;
|
| 2544 |
for (const b of upqTierEl.querySelectorAll("button")) {
|
| 2545 |
-
|
| 2546 |
-
b.classList.toggle("on", bv === String(v) || (v !== "auto" && Number(bv) === Number(v)));
|
| 2547 |
-
b.classList.remove("auto-active");
|
| 2548 |
-
}
|
| 2549 |
-
if (v === "auto") {
|
| 2550 |
-
const cur = effectiveUpQuality();
|
| 2551 |
-
for (const b of upqTierEl.querySelectorAll("button")) {
|
| 2552 |
-
if (Number(b.getAttribute("data-upq")) === Number(cur)) b.classList.add("auto-active");
|
| 2553 |
-
}
|
| 2554 |
}
|
| 2555 |
}
|
| 2556 |
function syncUpQualityUi() {
|
| 2557 |
-
highlightUpqTier(
|
| 2558 |
-
}
|
| 2559 |
-
function autoUpQuality() {
|
| 2560 |
-
if (!upAuto) return;
|
| 2561 |
-
const qEl = document.getElementById("quality");
|
| 2562 |
-
if (!qEl) return;
|
| 2563 |
-
qEl.value = 0.6;
|
| 2564 |
-
highlightUpqTier("auto");
|
| 2565 |
-
}
|
| 2566 |
-
for (const id of ["width", "height", "fps"]) {
|
| 2567 |
-
const el = document.getElementById(id);
|
| 2568 |
-
if (el) el.addEventListener("change", autoUpQuality);
|
| 2569 |
}
|
| 2570 |
if (upqTierEl) {
|
| 2571 |
for (const b of upqTierEl.querySelectorAll("button")) {
|
| 2572 |
b.addEventListener("click", () => {
|
| 2573 |
const v = b.getAttribute("data-upq");
|
| 2574 |
-
if (
|
| 2575 |
-
|
| 2576 |
-
autoUpQuality();
|
| 2577 |
-
} else {
|
| 2578 |
-
upAuto = false;
|
| 2579 |
-
if (qualityLegacyEl) qualityLegacyEl.value = v;
|
| 2580 |
-
highlightUpqTier(v);
|
| 2581 |
-
}
|
| 2582 |
resetUplinkEncoder();
|
| 2583 |
updateMetrics();
|
| 2584 |
});
|
|
@@ -2639,7 +2607,6 @@ if (fpsTierEl) {
|
|
| 2639 |
if (fpsInputEl) fpsInputEl.value = v;
|
| 2640 |
for (const x of fpsTierEl.querySelectorAll("button")) x.classList.toggle("on", x === b);
|
| 2641 |
if (timer) { clearInterval(timer); timer = setInterval(tick, Math.floor(1000 / sendTargetFps())); }
|
| 2642 |
-
autoUpQuality();
|
| 2643 |
updateMetrics();
|
| 2644 |
});
|
| 2645 |
}
|
|
@@ -2688,7 +2655,6 @@ function applyServerDefaults() {
|
|
| 2688 |
b.classList.toggle("on", Number(b.getAttribute("data-fps")) === Number(d.fps));
|
| 2689 |
}
|
| 2690 |
}
|
| 2691 |
-
autoUpQuality();
|
| 2692 |
const boolMap = {
|
| 2693 |
freezeKvOnStatic: "freeze_kv_on_static",
|
| 2694 |
usePe: "use_pe",
|
|
|
|
| 243 |
<span id="upqTier" class="qtier">
|
| 244 |
<button type="button" data-upq="0.2" data-i18n="q_low">低</button>
|
| 245 |
<button type="button" data-upq="0.4" data-i18n="q_mid">中</button>
|
| 246 |
+
<button type="button" data-upq="0.6" class="on" data-i18n="q_high">高</button>
|
|
|
|
| 247 |
</span>
|
| 248 |
</span>
|
| 249 |
<span class="kvreset-field keep-min">
|
|
|
|
| 698 |
let downSkewMin = null;
|
| 699 |
let downSkewAt = 0;
|
| 700 |
let upClamped = false;
|
|
|
|
| 701 |
let upClearStreak = 0;
|
| 702 |
let upClearNeed = 4;
|
| 703 |
let upDrainFactor = 1;
|
|
|
|
| 763 |
const dims = orientDims(o);
|
| 764 |
document.getElementById("width").value = dims.width;
|
| 765 |
document.getElementById("height").value = dims.height;
|
|
|
|
| 766 |
updateCropMask();
|
| 767 |
updateMetrics();
|
| 768 |
}
|
|
|
|
| 969 |
netReadoutEl.textContent = t("net_label") + " " + fmt(_rtt, "ms") + _tail;
|
| 970 |
netReadoutEl.classList.toggle("net-warn", _hi || _fd || _uc);
|
| 971 |
}
|
|
|
|
| 972 |
if (dispLatencyEl) {
|
| 973 |
dispLatencyEl.textContent = t("disp_label") + " " + fmt(editLatencyLastMs, "ms");
|
| 974 |
}
|
|
|
|
| 1658 |
seed: SERVER_DEFAULTS.seed,
|
| 1659 |
kv_reset_frames: Math.max(0, Math.floor(Number(document.getElementById("kvResetFrames").value) || 0)),
|
| 1660 |
output_quality: Number(downQualityUi.value),
|
| 1661 |
+
fps: sendTargetFps(),
|
| 1662 |
output_codec: h264DecodeOk ? "h264" : "mjpeg",
|
| 1663 |
+
input_codec: upCodecH264 ? "h264" : "mjpeg"
|
|
|
|
|
|
|
|
|
|
| 1664 |
};
|
| 1665 |
if (usingVideoFile) {
|
| 1666 |
startPayload.source = "file";
|
| 1667 |
startPayload.gate_enabled = false;
|
|
|
|
|
|
|
|
|
|
| 1668 |
}
|
| 1669 |
if (cachedEnhancedPrompt) startPayload.enhanced_prompt = cachedEnhancedPrompt;
|
| 1670 |
if (refImage) startPayload.ref_image = refImage;
|
|
|
|
| 1912 |
}
|
| 1913 |
return;
|
| 1914 |
}
|
| 1915 |
+
if (startingRun) return;
|
| 1916 |
const meta = pendingOutputMeta;
|
| 1917 |
pendingOutputMeta = null;
|
| 1918 |
receivedFrames += 1;
|
|
|
|
| 2002 |
try { if (usingVideoFile) camera.pause(); } catch (err) {}
|
| 2003 |
clearResultVideo();
|
| 2004 |
clearOutputQueue();
|
| 2005 |
+
showOutputStart(true, t("busy_resetting"));
|
| 2006 |
const ok = await beginSession();
|
| 2007 |
if (ok) return;
|
| 2008 |
startingRun = false;
|
|
|
|
| 2017 |
const PROMPT_GROUPS = [
|
| 2018 |
{
|
| 2019 |
key: "change_clothes",
|
| 2020 |
+
label: "实时换装", label_en: "Real-Time Outfit Change",
|
| 2021 |
cases: [
|
| 2022 |
{ title: "白T恤", title_en: "White Tee", desc: "上衣换成白色圆领纯棉T恤", desc_en: "Top → white crew-neck cotton tee", text: "将主角的上衣换成白色圆领纯棉 T 恤。", text_en: "Replace the subject's top with a white crew-neck cotton T-shirt." },
|
| 2023 |
{ title: "皮夹克", title_en: "Leather Jacket", desc: "外套换成哑光黑色皮质机车夹克", desc_en: "Jacket → matte black leather biker jacket", text: "将主角的外套换成哑光黑色皮质机车夹克。", text_en: "Replace the subject's jacket with a matte black leather biker jacket." },
|
| 2024 |
{ title: "汉服", title_en: "Hanfu", desc: "换成传统汉服交领右衽上衣", desc_en: "Traditional cross-collar Hanfu top", text: "将主角的服装换成传统汉服交领右衽上衣。", text_en: "Replace the subject's outfit with a traditional cross-collar (right-lapel) Hanfu top." },
|
| 2025 |
{ title: "学院风衬衫", title_en: "Preppy Shirt", desc: "白衬衫配藏青V领马甲加酒红领带", desc_en: "White shirt, navy V-vest, wine tie", text: "将主角的上衣换成白衬衫搭配藏青色 V 领马甲和酒红色领带。", text_en: "Replace the subject's top with a white shirt paired with a navy V-neck vest and a wine-red tie." },
|
| 2026 |
+
{ title: "京东定制帽 + 墨镜", title_en: "Custom JD Cap + Shades", desc: "戴上“京东”的红色鸭舌帽和墨镜", desc_en: "Red cap reading “京东” plus sunglasses", text: "给主角戴上写着“京东”两个汉字的红色鸭舌帽和墨镜。", text_en: "Put a red cap printed with the Chinese characters \"京东\" and a pair of sunglasses on the subject." },
|
| 2027 |
],
|
| 2028 |
},
|
| 2029 |
{
|
|
|
|
| 2034 |
{ title: "红色围巾", title_en: "Red Scarf", desc: "围上参考图里的红色流苏围巾", desc_en: "Red tasseled scarf from the reference image", ref: "scarf", text: "将参考图中的红色流苏围巾围到主角的颈部。", text_en: "Wrap the red tasseled scarf from the reference image around the subject's neck." },
|
| 2035 |
{ title: "粉色T恤", title_en: "Pink Tee", desc: "换上参考图里的粉色短袖 T 恤", desc_en: "Pink short-sleeve tee from the reference image", ref: "pink_tee", text: "将主角的衣服换成参考图里的粉色短袖 T 恤。", text_en: "Replace the subject's clothing with the pink short-sleeve T-shirt from the reference image." },
|
| 2036 |
{ title: "橙色眼镜", title_en: "Orange Glasses", desc: "戴上参考图里的橙色镜片眼镜", desc_en: "Orange-tinted glasses from the reference image", ref: "orange_glasses", text: "给主角戴上参考图里的橙色镜片眼镜。", text_en: "Put the orange-tinted glasses from the reference image on the subject." },
|
| 2037 |
+
{ title: "奶龙", title_en: "Nailong", desc: "主角换成参考图里的黄色玩偶", desc_en: "Subject → yellow toy from the reference image", ref: "nailong", text: "将视频中的主角替换为参考图中的黄色玩偶。", text_en: "Replace the main subject in the video with the yellow toy from the reference image." },
|
| 2038 |
],
|
| 2039 |
},
|
| 2040 |
{
|
|
|
|
| 2043 |
cases: [
|
| 2044 |
{ title: "3D动画", title_en: "3D Animation", desc: "渲染为3D动画电影风格", desc_en: "Render as 3D animated-film style", text: "将整个画面渲染为3D动画电影风格。应用平滑的三维建模质感、柔和的全局光照和景深,以及皮克斯风格的角色与场景渲染。", text_en: "Render the entire scene in a 3D animated-film style. Apply smooth 3D-modeled surfaces, soft global illumination and depth of field, and Pixar-style character and scene rendering." },
|
| 2045 |
{ title: "油画质感", title_en: "Oil Painting", desc: "转成梵高厚涂油画风格", desc_en: "Van Gogh impasto oil-painting style", text: "将整个画面转换为梵高的画的风格。应用厚重、富有表现力的笔触,鲜艳且强烈的色彩,天空中和风景中动态旋转的纹理,以及梵高画作中特有的粗放油彩(厚涂法)效果。", text_en: "Convert the entire scene into the style of a Van Gogh painting. Apply thick, expressive brushstrokes, vivid and intense colors, dynamic swirling textures in the sky and landscape, and the coarse impasto oil effect characteristic of Van Gogh's work." },
|
| 2046 |
+
{ title: "像素画", title_en: "Pixel Art", desc: "渲染为16色像素画风格", desc_en: "Render as 16-color pixel art", text: "将整个画面渲染为16色像素画风格。", text_en: "Render the entire scene in a 16-color pixel-art style." },
|
| 2047 |
{ title: "日漫赛璐璐", title_en: "Anime Cel", desc: "转成日式动漫赛璐璐风格", desc_en: "Japanese anime cel-shaded style", text: "将整个画面转换为日式动漫赛璐璐风格:干净的黑色描边、大面积平涂的纯色色块、明确的明暗二分硬边阴影和简洁的高光。", text_en: "Convert the entire scene into a Japanese anime cel-shaded style: clean black outlines, large flat areas of solid color, clear hard-edged two-tone shading, and simple highlights." },
|
| 2048 |
],
|
| 2049 |
},
|
|
|
|
| 2051 |
key: "face_edit",
|
| 2052 |
label: "外貌改造", label_en: "Look Edit",
|
| 2053 |
cases: [
|
| 2054 |
+
{ title: "戴上墨镜", title_en: "Sunglasses", desc: "戴一副粉色飞行员墨镜", desc_en: "Add pink aviator sunglasses", text: "给主角添加一副粉色飞行员墨镜架在鼻梁上。", text_en: "Add a pair of pink aviator sunglasses resting on the bridge of the subject's nose." },
|
| 2055 |
{ title: "戴口罩", title_en: "Face Mask", desc: "脸上戴一个医用口罩", desc_en: "Add a medical face mask", text: "给主角的脸上戴一个医用口罩。", text_en: "Put a medical face mask on the subject's face." },
|
| 2056 |
{ title: "加耳环", title_en: "Earrings", desc: "双耳加一对金色圆环耳环", desc_en: "Add a pair of gold hoop earrings", text: "给主角的双耳耳垂添加一对抛光金色圆环耳环。", text_en: "Add a pair of polished gold hoop earrings to the subject's earlobes." },
|
| 2057 |
+
{ title: "加络腮胡", title_en: "Beard", desc: "下颌脸颊加浓密络腮胡", desc_en: "Add a thick full beard", text: "给主角的下颌和脸颊添加浓密的深黄色络腮胡须,自然卷曲,覆盖下巴和两颊。", text_en: "Add a thick dark-yellow full beard to the subject's jaw and cheeks, naturally curly, covering the chin and both cheeks." },
|
| 2058 |
],
|
| 2059 |
},
|
| 2060 |
{
|
|
|
|
| 2063 |
cases: [
|
| 2064 |
{ title: "大理石雕塑", title_en: "Marble Statue", desc: "变成白色大理石雕塑", desc_en: "Turn into a white marble statue", text: "将主角整体转化为白色大理石雕塑:可见石材纹理和细微灰色脉络、表面呈抛光光泽、光线在曲面形成柔和高光,眼睛和头发同为石质,保持原有动作但带有雕塑般的质感。", text_en: "Transform the subject entirely into a white marble statue: visible stone texture and fine gray veining, a polished glossy surface, soft highlights on the curved surfaces, eyes and hair also made of stone, keeping the original motion but with a sculptural quality." },
|
| 2065 |
{ title: "乐高小人", title_en: "LEGO Figure", desc: "变成乐高积木小人", desc_en: "Turn into a LEGO minifigure", text: "将主角转化为乐高积木小人风格:黄色圆柱形头部、表面为塑料亚光质感,五官简化为印刷贴纸式的黑色圆点眼睛和弧线微笑。", text_en: "Transform the subject into a LEGO minifigure style: a yellow cylindrical head with a matte plastic surface, facial features simplified into printed-sticker-style black dot eyes and a curved smile." },
|
| 2066 |
+
{ title: "赛博机械体", title_en: "Cyborg", desc: "变成赛博机械体", desc_en: "Turn into a cyborg", text: "将主角转化为赛博机械体:面部露出银色金属骨架和精密齿轮、眼睛发出冷蓝光、皮肤接缝处可见荧光线路板纹路,关节处有可见的液压结构。", text_en: "Transform the subject into a cyborg: a silver metal skeleton and precision gears exposed on the face, eyes glowing cold blue, glowing circuit-board traces along the skin seams, and visible hydraulic structures at the joints." },
|
| 2067 |
{ title: "黏土动画", title_en: "Claymation", desc: "变成定格黏土动画角色", desc_en: "Turn into a stop-motion clay character", text: "将主角转化为定格黏土动画角色:表面可见手工捏制的指纹压痕和不规则质感、色彩饱和但略带哑光、比例微微卡通化(头部略大)。", text_en: "Transform the subject into a stop-motion claymation character: visible hand-molded fingerprint dents and irregular texture, saturated but slightly matte colors, and mildly cartoonish proportions (a slightly larger head)." },
|
|
|
|
| 2068 |
],
|
| 2069 |
},
|
| 2070 |
{
|
|
|
|
| 2078 |
},
|
| 2079 |
{
|
| 2080 |
key: "swap_hero",
|
| 2081 |
+
label: "主角变身", label_en: "Transform Subject",
|
| 2082 |
cases: [
|
| 2083 |
{ title: "梅西", title_en: "Messi", desc: "主角变成球星梅西", desc_en: "Turn the subject into Lionel Messi", text: "将主角变成足球明星梅西(Lionel Messi)的样貌。", text_en: "Change the subject's appearance into that of the football star Lionel Messi." },
|
| 2084 |
{ title: "詹姆斯", title_en: "LeBron", desc: "主角变成球星詹姆斯", desc_en: "Turn the subject into LeBron James", text: "将主角变成篮球明星勒布朗·詹姆斯(LeBron James)的样貌。", text_en: "Change the subject's appearance into that of the basketball star LeBron James." },
|
| 2085 |
{ title: "特朗普", title_en: "Trump", desc: "主角变成特朗普", desc_en: "Turn the subject into Donald Trump", text: "将主角变成美国总统特朗普(Donald Trump)的样貌。", text_en: "Change the subject's appearance into that of U.S. President Donald Trump." },
|
| 2086 |
+
{ title: "奥巴马", title_en: "Obama", desc: "主角变成奥巴马", desc_en: "Turn the subject into Barack Obama", text: "将主角变成美国前总统奥巴马(Barack Obama)的样貌。", text_en: "Change the subject's appearance into that of former U.S. President Barack Obama." },
|
| 2087 |
{ title: "奥特曼", title_en: "Ultraman", desc: "主角变成奥特曼", desc_en: "Turn the subject into Ultraman", text: "将主角的脸和身体变成奥特曼。", text_en: "Change the subject's face and body into Ultraman." },
|
| 2088 |
{ title: "拉布拉多", title_en: "Labrador", desc: "主角变成拉布拉多", desc_en: "Turn the subject into a Labrador", text: "将主角的脸和身体变成一只拉布拉多犬。", text_en: "Change the subject's face and body into a Labrador dog." },
|
| 2089 |
],
|
|
|
|
| 2535 |
function highlightUpqTier(v) {
|
| 2536 |
if (!upqTierEl) return;
|
| 2537 |
for (const b of upqTierEl.querySelectorAll("button")) {
|
| 2538 |
+
b.classList.toggle("on", Number(b.getAttribute("data-upq")) === Number(v));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2539 |
}
|
| 2540 |
}
|
| 2541 |
function syncUpQualityUi() {
|
| 2542 |
+
highlightUpqTier(qualityLegacyEl ? qualityLegacyEl.value : "0.6");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2543 |
}
|
| 2544 |
if (upqTierEl) {
|
| 2545 |
for (const b of upqTierEl.querySelectorAll("button")) {
|
| 2546 |
b.addEventListener("click", () => {
|
| 2547 |
const v = b.getAttribute("data-upq");
|
| 2548 |
+
if (qualityLegacyEl) qualityLegacyEl.value = v;
|
| 2549 |
+
highlightUpqTier(v);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2550 |
resetUplinkEncoder();
|
| 2551 |
updateMetrics();
|
| 2552 |
});
|
|
|
|
| 2607 |
if (fpsInputEl) fpsInputEl.value = v;
|
| 2608 |
for (const x of fpsTierEl.querySelectorAll("button")) x.classList.toggle("on", x === b);
|
| 2609 |
if (timer) { clearInterval(timer); timer = setInterval(tick, Math.floor(1000 / sendTargetFps())); }
|
|
|
|
| 2610 |
updateMetrics();
|
| 2611 |
});
|
| 2612 |
}
|
|
|
|
| 2655 |
b.classList.toggle("on", Number(b.getAttribute("data-fps")) === Number(d.fps));
|
| 2656 |
}
|
| 2657 |
}
|
|
|
|
| 2658 |
const boolMap = {
|
| 2659 |
freezeKvOnStatic: "freeze_kv_on_static",
|
| 2660 |
usePe: "use_pe",
|
xvideo/models/vae/vae.py
CHANGED
|
@@ -525,7 +525,10 @@ class Head(nn.Module):
|
|
| 525 |
|
| 526 |
|
| 527 |
class XVAEChunkCausal(ModelMixin, ConfigMixin):
|
| 528 |
-
|
|
|
|
|
|
|
|
|
|
| 529 |
@register_to_config
|
| 530 |
def __init__(
|
| 531 |
self,
|
|
|
|
| 525 |
|
| 526 |
|
| 527 |
class XVAEChunkCausal(ModelMixin, ConfigMixin):
|
| 528 |
+
"""For more technical details on high-resolution causal VAE decoding, see:
|
| 529 |
+
https://github.com/xin1u/UltraFlash
|
| 530 |
+
"""
|
| 531 |
+
|
| 532 |
@register_to_config
|
| 533 |
def __init__(
|
| 534 |
self,
|
xvideo/serving/serve_joyomni_streaming.py
CHANGED
|
@@ -72,12 +72,15 @@ class SessionGate:
|
|
| 72 |
ev.set()
|
| 73 |
|
| 74 |
|
|
|
|
|
|
|
| 75 |
REF_IMAGE_DIR = REPO_ROOT / "rv2v_reference"
|
| 76 |
REF_IMAGE_FILES = {
|
| 77 |
"hat": "4e481f7a-2443-4935-a841-af6113cc4236.png",
|
| 78 |
"scarf": "5e178546-3ebf-40df-bb86-01613dd96c3b.png",
|
| 79 |
"pink_tee": "1c182f2f-32cf-4825-904e-64c69aed2e31.png",
|
| 80 |
"orange_glasses": "486b9561-e73d-45ca-bb2d-2a47998a0a73.png",
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
def _load_ref_images() -> dict[str, str]:
|
|
@@ -125,6 +128,7 @@ def _snap_to_align(value: int, align: int) -> int:
|
|
| 125 |
|
| 126 |
class _H264Stream:
|
| 127 |
def __init__(self, quality: int) -> None:
|
|
|
|
| 128 |
self._enc = None
|
| 129 |
self._size: tuple[int, int] | None = None
|
| 130 |
self._crf = self.crf_for_quality(quality)
|
|
@@ -142,6 +146,10 @@ class _H264Stream:
|
|
| 142 |
self._want_reset = True
|
| 143 |
|
| 144 |
def encode(self, frames: list) -> list[tuple[bytes, bool]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
import av
|
| 146 |
import cv2
|
| 147 |
|
|
@@ -264,25 +272,37 @@ def _check_face_gate(image: Image.Image, *, onnx_path: str,
|
|
| 264 |
return ("too_close", None, n_faces)
|
| 265 |
return (None, (cx, cy, min(fw, fh) / frame_min), n_faces)
|
| 266 |
|
| 267 |
-
|
|
|
|
| 268 |
det = _get_face_detector(onnx_path, score_thresh)
|
| 269 |
if det is None:
|
| 270 |
-
return
|
|
|
|
| 271 |
import numpy as np
|
| 272 |
rgb = np.asarray(image if image.mode == "RGB" else image.convert("RGB"))
|
| 273 |
bgr = np.ascontiguousarray(rgb[:, :, ::-1])
|
| 274 |
h, w = bgr.shape[:2]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
det.setInputSize((w, h))
|
| 276 |
_, faces = det.detect(bgr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
if faces is None:
|
| 278 |
-
return
|
| 279 |
_min_side = float(min_ratio) * float(min(w, h))
|
| 280 |
_mx = float(edge_margin) * float(w)
|
| 281 |
_my = float(edge_margin) * float(h)
|
| 282 |
-
for
|
| 283 |
-
if float(f[-1]) < float(score_thresh):
|
| 284 |
-
continue
|
| 285 |
-
fx, fy, fw, fh = float(f[0]), float(f[1]), float(f[2]), float(f[3])
|
| 286 |
if _min_side > 0.0 and min(fw, fh) < _min_side:
|
| 287 |
continue
|
| 288 |
if edge_margin > 0.0 and (fx < _mx or fy < _my or fx + fw > w - _mx or fy + fh > h - _my):
|
|
@@ -291,6 +311,22 @@ def _face_present(image: Image.Image, *, onnx_path: str, score_thresh: float, mi
|
|
| 291 |
return False
|
| 292 |
|
| 293 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
_PERSON_NET: dict[str, Any] = {}
|
| 295 |
|
| 296 |
def _get_person_net(onnx_path: str):
|
|
@@ -744,16 +780,8 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 744 |
pe_defer = False
|
| 745 |
pe_task: asyncio.Task | None = None
|
| 746 |
session_max_inflight = max(0, int(args.max_inflight_chunks or 0))
|
| 747 |
-
presence_monitor = False
|
| 748 |
-
face_required = False
|
| 749 |
-
count_monitor = False
|
| 750 |
-
|
| 751 |
-
fg_score = float(args.face_gate_score)
|
| 752 |
-
fg_min_below = float(args.face_gate_min_below_ratio)
|
| 753 |
-
fg_stable = int(args.face_gate_stable_frames)
|
| 754 |
-
fg_absent = int(args.presence_absent_frames)
|
| 755 |
|
| 756 |
-
gate_state = {"count": 0, "cx": None, "cy": None, "absent": 0,
|
| 757 |
"absent_hold": False, "present": 0, "person_check_i": 0, "person_last": True,
|
| 758 |
"subject_count": None, "cand": None, "cand_n": 0, "recount": False}
|
| 759 |
kv_reset_frames = max(0, int(args.kv_reset_frames or 0))
|
|
@@ -803,10 +831,22 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 803 |
"send_state": "idle",
|
| 804 |
}
|
| 805 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 806 |
async def _send_json(payload: dict[str, Any]) -> None:
|
| 807 |
async with send_lock:
|
| 808 |
ws_debug["send_state"] = f"json:{payload.get('type')}"
|
| 809 |
-
await
|
| 810 |
ws_debug["last_send_at"] = time.time()
|
| 811 |
ws_debug["send_state"] = "idle"
|
| 812 |
|
|
@@ -887,7 +927,7 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 887 |
ws_debug["rec_out_written"] = _rec_o.frames_written
|
| 888 |
ws_debug["rec_out_dropped"] = _rec_o.frames_dropped_recording
|
| 889 |
async with send_lock:
|
| 890 |
-
await
|
| 891 |
"type": "flow_drop",
|
| 892 |
"count": count,
|
| 893 |
"outstanding": _outstanding,
|
|
@@ -918,7 +958,7 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 918 |
|
| 919 |
async with send_lock:
|
| 920 |
ws_debug["send_state"] = f"chunk_start:{profile.get('chunk_idx')}"
|
| 921 |
-
await
|
| 922 |
{
|
| 923 |
"type": "chunk_start",
|
| 924 |
"count": count,
|
|
@@ -948,7 +988,7 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 948 |
wire, is_key = encoded, False
|
| 949 |
async with send_lock:
|
| 950 |
ws_debug["send_state"] = f"chunk_frame:{profile.get('chunk_idx')}:{idx}"
|
| 951 |
-
await
|
| 952 |
{
|
| 953 |
"type": "output_frame",
|
| 954 |
"index": idx,
|
|
@@ -961,7 +1001,7 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 961 |
"key": is_key,
|
| 962 |
}
|
| 963 |
)
|
| 964 |
-
await
|
| 965 |
frames_out += 1
|
| 966 |
ws_debug["frames_out"] = frames_out
|
| 967 |
ws_debug["output_bytes"] = int(ws_debug.get("output_bytes", 0)) + len(wire)
|
|
@@ -1002,7 +1042,7 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 1002 |
_chunk_done_msg["chunk_idx"] = profile.get("chunk_idx")
|
| 1003 |
async with send_lock:
|
| 1004 |
ws_debug["send_state"] = f"chunk_done:{profile.get('chunk_idx')}"
|
| 1005 |
-
await
|
| 1006 |
ws_debug["chunk_results_sent"] = int(ws_debug.get("chunk_results_sent", 0)) + 1
|
| 1007 |
ws_debug["last_send_at"] = time.time()
|
| 1008 |
ws_debug["send_state"] = "idle"
|
|
@@ -1109,7 +1149,8 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 1109 |
return None
|
| 1110 |
|
| 1111 |
def _close_session_sync(session_ref) -> None:
|
| 1112 |
-
|
|
|
|
| 1113 |
|
| 1114 |
async def _close_session_safely(session_ref, reason: str) -> None:
|
| 1115 |
try:
|
|
@@ -1219,6 +1260,7 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 1219 |
nonlocal session, frames_since_session_reset, reset_count
|
| 1220 |
if session is None:
|
| 1221 |
return
|
|
|
|
| 1222 |
|
| 1223 |
await _stop_output_task()
|
| 1224 |
await _close_session_safely(session, "kv_reset")
|
|
@@ -1261,7 +1303,7 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 1261 |
last_activity = time.monotonic()
|
| 1262 |
last_frames_out = frames_out
|
| 1263 |
while True:
|
| 1264 |
-
if frames_out != last_frames_out:
|
| 1265 |
last_frames_out = frames_out
|
| 1266 |
last_activity = time.monotonic()
|
| 1267 |
if time.monotonic() - last_activity >= HOLDER_IDLE_TIMEOUT_S:
|
|
@@ -1336,7 +1378,8 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 1336 |
))
|
| 1337 |
use_pe = bool(payload.get("use_pe", args.use_pe)) and bool(os.environ.get("OPENAI_API_KEY"))
|
| 1338 |
|
| 1339 |
-
|
|
|
|
| 1340 |
flow["recv"] = None
|
| 1341 |
flow["at"] = 0.0
|
| 1342 |
flow["congested"] = False
|
|
@@ -1345,24 +1388,26 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 1345 |
flow["consec"] = 0
|
| 1346 |
flow["base"] = frames_out
|
| 1347 |
|
| 1348 |
-
|
| 1349 |
-
payload.get("no_person_blank", True)
|
| 1350 |
-
)
|
| 1351 |
-
|
| 1352 |
-
face_required = bool(args.online_gate) and bool(payload.get("require_face", True))
|
| 1353 |
-
|
| 1354 |
-
count_monitor = bool(args.online_gate) and bool(payload.get("person_count_reedit", True))
|
| 1355 |
|
| 1356 |
fg_score = float(payload.get("fg_score", args.face_gate_score))
|
| 1357 |
fg_min_below = float(payload.get("fg_min_below_ratio", args.face_gate_min_below_ratio))
|
| 1358 |
fg_stable = max(1, int(payload.get("fg_stable_frames", args.face_gate_stable_frames)))
|
| 1359 |
fg_absent = max(1, int(args.presence_absent_frames))
|
| 1360 |
fg_return = max(1, int(args.presence_return_frames))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1361 |
gate_state["count"] = 0
|
| 1362 |
gate_state["cx"] = None
|
| 1363 |
gate_state["cy"] = None
|
| 1364 |
gate_state["absent"] = 0
|
| 1365 |
-
gate_state["passthrough"] = False
|
| 1366 |
gate_state["absent_hold"] = False
|
| 1367 |
gate_state["present"] = 0
|
| 1368 |
gate_state["person_check_i"] = 0
|
|
@@ -1600,7 +1645,7 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 1600 |
continue
|
| 1601 |
|
| 1602 |
if kv_reset_frames > 0 and frames_since_session_reset >= kv_reset_frames:
|
| 1603 |
-
face_gate_pending =
|
| 1604 |
gate_state["count"] = 0
|
| 1605 |
gate_state["cx"] = None
|
| 1606 |
gate_state["cy"] = None
|
|
@@ -1653,7 +1698,7 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 1653 |
gate_state["csz"] = _csz
|
| 1654 |
return "off_center"
|
| 1655 |
_pcx, _pcy, _pcsz = gate_state["cx"], gate_state["cy"], gate_state.get("csz")
|
| 1656 |
-
_eps =
|
| 1657 |
_cap = float(args.face_gate_settle_drift)
|
| 1658 |
|
| 1659 |
_szeps = _eps * 0.5
|
|
@@ -1679,96 +1724,92 @@ def create_app(args: argparse.Namespace) -> FastAPI:
|
|
| 1679 |
gate_state["pe_anchor"] = frame
|
| 1680 |
return "__gate_pe__"
|
| 1681 |
|
| 1682 |
-
if
|
| 1683 |
-
|
| 1684 |
-
|
| 1685 |
-
|
| 1686 |
-
|
| 1687 |
-
|
| 1688 |
-
|
| 1689 |
-
|
| 1690 |
-
|
| 1691 |
-
|
| 1692 |
-
|
| 1693 |
-
|
| 1694 |
-
edge_margin=float(args.face_present_edge_margin))
|
| 1695 |
-
else:
|
| 1696 |
-
gate_state["face_last"] = True
|
| 1697 |
-
gate_state["person_check_i"] = (_tick + 1) % stride
|
| 1698 |
-
_body_here = bool(gate_state["person_last"])
|
| 1699 |
-
_face_here = bool(gate_state.get("face_last", True))
|
| 1700 |
-
_present = _body_here and _face_here
|
| 1701 |
-
_reason_now = "no_person" if not _body_here else ("no_face" if not _face_here else "")
|
| 1702 |
-
|
| 1703 |
-
body_flip = max(1, int(args.person_body_flip_frames))
|
| 1704 |
-
if _body_here:
|
| 1705 |
-
gate_state["body_miss"] = 0
|
| 1706 |
-
else:
|
| 1707 |
-
gate_state["body_miss"] = gate_state.get("body_miss", 0) + 1
|
| 1708 |
-
if _present:
|
| 1709 |
-
gate_state["absent"] = 0
|
| 1710 |
-
if gate_state.get("absent_hold"):
|
| 1711 |
-
gate_state["present"] = gate_state.get("present", 0) + 1
|
| 1712 |
-
if gate_state["present"] >= fg_return:
|
| 1713 |
-
gate_state["absent_hold"] = False
|
| 1714 |
-
gate_state["present"] = 0
|
| 1715 |
-
print("#####[PERSON-GATE] subject returned (stable) -> re-run startup gate (reset)", flush=True)
|
| 1716 |
-
return ("__person_returned__",)
|
| 1717 |
-
|
| 1718 |
else:
|
| 1719 |
-
gate_state["
|
| 1720 |
-
|
| 1721 |
-
|
| 1722 |
-
|
| 1723 |
-
|
| 1724 |
-
|
| 1725 |
-
|
| 1726 |
-
|
| 1727 |
-
|
| 1728 |
-
|
| 1729 |
-
|
| 1730 |
-
|
| 1731 |
-
|
| 1732 |
-
|
| 1733 |
-
pass
|
| 1734 |
-
print(f"#####[PERSON-GATE] {gate_state['hold_reason']} for {gate_state['absent']} frames -> black-hold", flush=True)
|
| 1735 |
if gate_state.get("absent_hold"):
|
| 1736 |
-
|
| 1737 |
-
|
| 1738 |
-
|
| 1739 |
-
|
| 1740 |
-
|
| 1741 |
-
|
| 1742 |
-
|
| 1743 |
-
|
| 1744 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1745 |
gate_state["subject_count"] = _n
|
| 1746 |
gate_state["cand"] = None
|
| 1747 |
gate_state["cand_n"] = 0
|
| 1748 |
-
|
| 1749 |
-
|
| 1750 |
-
|
| 1751 |
-
else:
|
| 1752 |
-
gate_state["cand"] = _n
|
| 1753 |
-
gate_state["cand_n"] = 1
|
| 1754 |
-
if gate_state["cand_n"] >= int(args.person_count_change_frames):
|
| 1755 |
-
gate_state["recount"] = True
|
| 1756 |
-
gate_state["subject_count"] = _n
|
| 1757 |
-
gate_state["cand"] = None
|
| 1758 |
-
gate_state["cand_n"] = 0
|
| 1759 |
-
elif _n < gate_state["subject_count"]:
|
| 1760 |
-
if _n == gate_state["cand"]:
|
| 1761 |
-
gate_state["cand_n"] += 1
|
| 1762 |
-
else:
|
| 1763 |
-
gate_state["cand"] = _n
|
| 1764 |
-
gate_state["cand_n"] = 1
|
| 1765 |
-
if gate_state["cand_n"] >= int(args.person_count_change_frames):
|
| 1766 |
-
gate_state["subject_count"] = _n
|
| 1767 |
-
gate_state["cand"] = None
|
| 1768 |
-
gate_state["cand_n"] = 0
|
| 1769 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1770 |
gate_state["cand"] = None
|
| 1771 |
gate_state["cand_n"] = 0
|
|
|
|
|
|
|
|
|
|
| 1772 |
|
| 1773 |
if pe_defer and not face_gate_pending:
|
| 1774 |
gate_state["pe_anchor"] = frame
|
|
@@ -1974,22 +2015,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
| 1974 |
|
| 1975 |
parser.add_argument("--face-detector-onnx", type=str, default=DEFAULT_FACE_DETECTOR_ONNX, help="YuNet ONNX weight for the face-presence gate. Missing -> gate disabled (edits run unconditionally).")
|
| 1976 |
parser.add_argument("--face-gate-score", type=float, default=0.35, help="Min YuNet confidence to count as a face. Lower = detects motion-blurred faces (fewer transient drops), but more false positives.")
|
| 1977 |
-
parser.add_argument("--face-present-min-ratio", type=float, default=0.15, help="Mid-session presence: a detected face counts as 'present' only if its short side is >= this fraction of the frame short side. A too-small/partial face (subject sat down so only the top of the head shows) counts as no-face -> black-hold, instead of letting the model t2v-hallucinate a person. 0 = any face counts. Higher = stricter (black out sooner when the face gets small/far). Normal editing faces measure ~0.
|
| 1978 |
parser.add_argument("--face-present-edge-margin", type=float, default=0.0, help="Mid-session presence: a face whose box comes within this fraction of ANY frame border counts as a HALF/partial face (turned/leaned out) -> no-face -> black-hold, so the model never edits a half-face frame (which it fills in as a t2v hallucination). 0 = no edge check (default: disabled -- the face-box edge check false-blacked too eagerly when a face merely neared a border). Set e.g. 0.02 to re-enable a lenient check.")
|
| 1979 |
-
parser.add_argument("--face-gate-min-below-ratio", type=float, default=0.20, help="Min fraction of frame HEIGHT that must be below the face (torso room, for garment try-on). Bigger -> stricter
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1980 |
|
| 1981 |
-
parser.add_argument("--
|
| 1982 |
-
parser.add_argument("--face-gate-move-eps", type=float, default=0.02, help="Max per-frame face-center movement (fraction of frame) to count as 'still'. Bigger -> tolerates more motion. Pairs with --face-gate-settle-drift (cumulative) so a slow glide can't creep through frame-by-frame.")
|
| 1983 |
-
parser.add_argument("--face-gate-settle-drift", type=float, default=0.05, help="Max CUMULATIVE face-center wander (fraction of frame) allowed across the whole settle streak. Closes the 'slow continuous glide' hole where every per-frame step is < move-eps but they sum to a big slide (swing-into-frame motion baked into chunk0 -> ghost/duplicate person). Smaller = must hold more still. Complements --face-gate-move-eps (per-frame) + --face-gate-stable-frames (streak length).")
|
| 1984 |
-
parser.add_argument("--face-gate-stable-frames", type=int, default=12, help="Consecutive centered+still frames required before editing starts (~24fps send rate, so 12 ≈ 0.5s).")
|
| 1985 |
-
parser.add_argument("--online-gate", action=argparse.BooleanOptionalAction, default=True, help="Master switch for MID-SESSION behavior (no-person black-hold + person-count re-edit). On (default) = presence/count monitoring runs for ALL sessions once editing begins. --no-online-gate to disable and make the inference path identical to the base commit.")
|
| 1986 |
parser.add_argument("--presence-absent-frames", type=int, default=12, help="Consecutive not-present frames (body missing, OR face too small / half-out per --face-present-*) before the output goes black. Small = stop FAST (less T2V leak on a quick sit-down / turn-away); larger = tolerate a brief occlusion / head-turn without black-holding. ~24fps, 12 ≈ 0.5s.")
|
| 1987 |
parser.add_argument("--presence-return-frames", type=int, default=24, help="Consecutive present (body+face) frames required to LEAVE the black-hold and re-run the startup gate. Separate from --presence-absent-frames so entry stays fast (black out quickly) while exit is well de-bounced: a face flickering through finger gaps while hands cover the face won't bounce no_face<->settling. ~24fps, 24 ≈ 1s.")
|
| 1988 |
-
parser.add_argument("--person-count-change-frames", type=int, default=24, help="Consecutive frames a NEW face count must hold before re-
|
| 1989 |
-
parser.add_argument("--count-face-min-ratio", type=float, default=0.45, help="For person-count-change: a face counts as an additional subject only if its short side is >= this fraction of the MAIN (largest/foreground) face's short side. Excludes far-smaller BACKGROUND people (e.g. a coworker behind the subject) that otherwise flip the count and trigger spurious re-edits. Higher = stricter (ignore more background).
|
| 1990 |
-
parser.add_argument("--person-detector-onnx", type=str, default=DEFAULT_PERSON_DETECTOR_ONNX, help="YOLOv8n ONNX (fixed 320) for mid-session
|
| 1991 |
parser.add_argument("--person-gate-conf", type=float, default=0.4, help="Min YOLO person-class score to count the person as present.")
|
| 1992 |
-
parser.add_argument("--person-check-stride", type=int, default=2, help="Run the person detector every Nth frame during editing (YOLO ~27ms; stride amortizes the cost). Smaller =
|
| 1993 |
parser.add_argument("--person-body-flip-frames", type=int, default=6, help="Consecutive body-misses before the client reason flips to no_person. Below this, a lone YOLO dip (a hand/object over the face also clips the torso) keeps the current reason -- normally show_full_face -- so the hint doesn't strobe no_face<->no_person. Reason-only de-bounce; the black-hold timing (--presence-absent-frames) is unaffected. ~24fps, 6 ≈ 0.25s. Higher = more reluctant to ever show no_person; 1 = report no_person on the first miss (old behavior).")
|
| 1994 |
parser.add_argument("--output-quality", default="auto",
|
| 1995 |
help="Downlink preview quality: 'auto' (RTT-adaptive) or a fixed 1-100.")
|
|
|
|
| 72 |
ev.set()
|
| 73 |
|
| 74 |
|
| 75 |
+
WS_SEND_TIMEOUT_S = 10.0
|
| 76 |
+
|
| 77 |
REF_IMAGE_DIR = REPO_ROOT / "rv2v_reference"
|
| 78 |
REF_IMAGE_FILES = {
|
| 79 |
"hat": "4e481f7a-2443-4935-a841-af6113cc4236.png",
|
| 80 |
"scarf": "5e178546-3ebf-40df-bb86-01613dd96c3b.png",
|
| 81 |
"pink_tee": "1c182f2f-32cf-4825-904e-64c69aed2e31.png",
|
| 82 |
"orange_glasses": "486b9561-e73d-45ca-bb2d-2a47998a0a73.png",
|
| 83 |
+
"nailong": "nailong.png",
|
| 84 |
}
|
| 85 |
|
| 86 |
def _load_ref_images() -> dict[str, str]:
|
|
|
|
| 128 |
|
| 129 |
class _H264Stream:
|
| 130 |
def __init__(self, quality: int) -> None:
|
| 131 |
+
self._lock = threading.Lock()
|
| 132 |
self._enc = None
|
| 133 |
self._size: tuple[int, int] | None = None
|
| 134 |
self._crf = self.crf_for_quality(quality)
|
|
|
|
| 146 |
self._want_reset = True
|
| 147 |
|
| 148 |
def encode(self, frames: list) -> list[tuple[bytes, bool]]:
|
| 149 |
+
with self._lock:
|
| 150 |
+
return self._encode_locked(frames)
|
| 151 |
+
|
| 152 |
+
def _encode_locked(self, frames: list) -> list[tuple[bytes, bool]]:
|
| 153 |
import av
|
| 154 |
import cv2
|
| 155 |
|
|
|
|
| 272 |
return ("too_close", None, n_faces)
|
| 273 |
return (None, (cx, cy, min(fw, fh) / frame_min), n_faces)
|
| 274 |
|
| 275 |
+
|
| 276 |
+
def _detect_gate_faces(image: Image.Image, *, onnx_path: str, score_thresh: float):
|
| 277 |
det = _get_face_detector(onnx_path, score_thresh)
|
| 278 |
if det is None:
|
| 279 |
+
return None, 0.0, 0.0
|
| 280 |
+
import cv2
|
| 281 |
import numpy as np
|
| 282 |
rgb = np.asarray(image if image.mode == "RGB" else image.convert("RGB"))
|
| 283 |
bgr = np.ascontiguousarray(rgb[:, :, ::-1])
|
| 284 |
h, w = bgr.shape[:2]
|
| 285 |
+
if FACE_DETECTOR_DOWNSAMPLE > 1.0:
|
| 286 |
+
_s = 1.0 / FACE_DETECTOR_DOWNSAMPLE
|
| 287 |
+
bgr = cv2.resize(bgr, (max(2, int(round(w * _s))), max(2, int(round(h * _s)))), interpolation=cv2.INTER_AREA)
|
| 288 |
+
h, w = bgr.shape[:2]
|
| 289 |
+
det.setScoreThreshold(float(score_thresh))
|
| 290 |
det.setInputSize((w, h))
|
| 291 |
_, faces = det.detect(bgr)
|
| 292 |
+
out = []
|
| 293 |
+
if faces is not None:
|
| 294 |
+
for f in faces:
|
| 295 |
+
out.append((float(f[0]), float(f[1]), float(f[2]), float(f[3])))
|
| 296 |
+
return out, float(w), float(h)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _face_present_from(faces, w, h, *, min_ratio: float = 0.0, edge_margin: float = 0.0) -> bool:
|
| 300 |
if faces is None:
|
| 301 |
+
return True
|
| 302 |
_min_side = float(min_ratio) * float(min(w, h))
|
| 303 |
_mx = float(edge_margin) * float(w)
|
| 304 |
_my = float(edge_margin) * float(h)
|
| 305 |
+
for fx, fy, fw, fh in faces:
|
|
|
|
|
|
|
|
|
|
| 306 |
if _min_side > 0.0 and min(fw, fh) < _min_side:
|
| 307 |
continue
|
| 308 |
if edge_margin > 0.0 and (fx < _mx or fy < _my or fx + fw > w - _mx or fy + fh > h - _my):
|
|
|
|
| 311 |
return False
|
| 312 |
|
| 313 |
|
| 314 |
+
def _count_faces_from(faces, w, h, *, count_min_ratio: float) -> int:
|
| 315 |
+
if not faces:
|
| 316 |
+
return 0
|
| 317 |
+
frame_min = float(min(w, h))
|
| 318 |
+
best = None
|
| 319 |
+
best_area = -1.0
|
| 320 |
+
shorts = []
|
| 321 |
+
for fx, fy, fw, fh in faces:
|
| 322 |
+
shorts.append(min(fw, fh))
|
| 323 |
+
if fw * fh > best_area:
|
| 324 |
+
best_area = fw * fh
|
| 325 |
+
best = (fw, fh)
|
| 326 |
+
thr = max(0.05 * frame_min, float(count_min_ratio) * min(best))
|
| 327 |
+
return sum(1 for sh in shorts if sh >= thr)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
_PERSON_NET: dict[str, Any] = {}
|
| 331 |
|
| 332 |
def _get_person_net(onnx_path: str):
|
|
|
|
| 780 |
pe_defer = False
|
| 781 |
pe_task: asyncio.Task | None = None
|
| 782 |
session_max_inflight = max(0, int(args.max_inflight_chunks or 0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
|
| 784 |
+
gate_state = {"count": 0, "cx": None, "cy": None, "absent": 0,
|
| 785 |
"absent_hold": False, "present": 0, "person_check_i": 0, "person_last": True,
|
| 786 |
"subject_count": None, "cand": None, "cand_n": 0, "recount": False}
|
| 787 |
kv_reset_frames = max(0, int(args.kv_reset_frames or 0))
|
|
|
|
| 831 |
"send_state": "idle",
|
| 832 |
}
|
| 833 |
|
| 834 |
+
async def _ws_send_json(payload: dict[str, Any]) -> None:
|
| 835 |
+
try:
|
| 836 |
+
await asyncio.wait_for(websocket.send_json(payload), timeout=WS_SEND_TIMEOUT_S)
|
| 837 |
+
except asyncio.TimeoutError:
|
| 838 |
+
raise WebSocketDisconnect()
|
| 839 |
+
|
| 840 |
+
async def _ws_send_bytes(data: bytes) -> None:
|
| 841 |
+
try:
|
| 842 |
+
await asyncio.wait_for(websocket.send_bytes(data), timeout=WS_SEND_TIMEOUT_S)
|
| 843 |
+
except asyncio.TimeoutError:
|
| 844 |
+
raise WebSocketDisconnect()
|
| 845 |
+
|
| 846 |
async def _send_json(payload: dict[str, Any]) -> None:
|
| 847 |
async with send_lock:
|
| 848 |
ws_debug["send_state"] = f"json:{payload.get('type')}"
|
| 849 |
+
await _ws_send_json(payload)
|
| 850 |
ws_debug["last_send_at"] = time.time()
|
| 851 |
ws_debug["send_state"] = "idle"
|
| 852 |
|
|
|
|
| 927 |
ws_debug["rec_out_written"] = _rec_o.frames_written
|
| 928 |
ws_debug["rec_out_dropped"] = _rec_o.frames_dropped_recording
|
| 929 |
async with send_lock:
|
| 930 |
+
await _ws_send_json({
|
| 931 |
"type": "flow_drop",
|
| 932 |
"count": count,
|
| 933 |
"outstanding": _outstanding,
|
|
|
|
| 958 |
|
| 959 |
async with send_lock:
|
| 960 |
ws_debug["send_state"] = f"chunk_start:{profile.get('chunk_idx')}"
|
| 961 |
+
await _ws_send_json(
|
| 962 |
{
|
| 963 |
"type": "chunk_start",
|
| 964 |
"count": count,
|
|
|
|
| 988 |
wire, is_key = encoded, False
|
| 989 |
async with send_lock:
|
| 990 |
ws_debug["send_state"] = f"chunk_frame:{profile.get('chunk_idx')}:{idx}"
|
| 991 |
+
await _ws_send_json(
|
| 992 |
{
|
| 993 |
"type": "output_frame",
|
| 994 |
"index": idx,
|
|
|
|
| 1001 |
"key": is_key,
|
| 1002 |
}
|
| 1003 |
)
|
| 1004 |
+
await _ws_send_bytes(wire)
|
| 1005 |
frames_out += 1
|
| 1006 |
ws_debug["frames_out"] = frames_out
|
| 1007 |
ws_debug["output_bytes"] = int(ws_debug.get("output_bytes", 0)) + len(wire)
|
|
|
|
| 1042 |
_chunk_done_msg["chunk_idx"] = profile.get("chunk_idx")
|
| 1043 |
async with send_lock:
|
| 1044 |
ws_debug["send_state"] = f"chunk_done:{profile.get('chunk_idx')}"
|
| 1045 |
+
await _ws_send_json(_chunk_done_msg)
|
| 1046 |
ws_debug["chunk_results_sent"] = int(ws_debug.get("chunk_results_sent", 0)) + 1
|
| 1047 |
ws_debug["last_send_at"] = time.time()
|
| 1048 |
ws_debug["send_state"] = "idle"
|
|
|
|
| 1149 |
return None
|
| 1150 |
|
| 1151 |
def _close_session_sync(session_ref) -> None:
|
| 1152 |
+
with app.state.inference_lock:
|
| 1153 |
+
session_ref.close()
|
| 1154 |
|
| 1155 |
async def _close_session_safely(session_ref, reason: str) -> None:
|
| 1156 |
try:
|
|
|
|
| 1260 |
nonlocal session, frames_since_session_reset, reset_count
|
| 1261 |
if session is None:
|
| 1262 |
return
|
| 1263 |
+
print(f"#####[STREAM] session reset ({reason})", flush=True)
|
| 1264 |
|
| 1265 |
await _stop_output_task()
|
| 1266 |
await _close_session_safely(session, "kv_reset")
|
|
|
|
| 1303 |
last_activity = time.monotonic()
|
| 1304 |
last_frames_out = frames_out
|
| 1305 |
while True:
|
| 1306 |
+
if frames_out != last_frames_out or pe_task is not None:
|
| 1307 |
last_frames_out = frames_out
|
| 1308 |
last_activity = time.monotonic()
|
| 1309 |
if time.monotonic() - last_activity >= HOLDER_IDLE_TIMEOUT_S:
|
|
|
|
| 1378 |
))
|
| 1379 |
use_pe = bool(payload.get("use_pe", args.use_pe)) and bool(os.environ.get("OPENAI_API_KEY"))
|
| 1380 |
|
| 1381 |
+
entry_gate = bool(payload.get("gate_enabled", True))
|
| 1382 |
+
face_gate_pending = entry_gate
|
| 1383 |
flow["recv"] = None
|
| 1384 |
flow["at"] = 0.0
|
| 1385 |
flow["congested"] = False
|
|
|
|
| 1388 |
flow["consec"] = 0
|
| 1389 |
flow["base"] = frames_out
|
| 1390 |
|
| 1391 |
+
gate_on = bool(args.online_gate) and entry_gate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1392 |
|
| 1393 |
fg_score = float(payload.get("fg_score", args.face_gate_score))
|
| 1394 |
fg_min_below = float(payload.get("fg_min_below_ratio", args.face_gate_min_below_ratio))
|
| 1395 |
fg_stable = max(1, int(payload.get("fg_stable_frames", args.face_gate_stable_frames)))
|
| 1396 |
fg_absent = max(1, int(args.presence_absent_frames))
|
| 1397 |
fg_return = max(1, int(args.presence_return_frames))
|
| 1398 |
+
_gate_fps = float(payload.get("fps") or args.fps or 24.0)
|
| 1399 |
+
_fscale = _gate_fps / 24.0
|
| 1400 |
+
fg_stable = max(1, int(round(fg_stable * _fscale)))
|
| 1401 |
+
fg_absent = max(1, int(round(fg_absent * _fscale)))
|
| 1402 |
+
fg_return = max(1, int(round(fg_return * _fscale)))
|
| 1403 |
+
count_change_frames = max(1, int(round(int(args.person_count_change_frames) * _fscale)))
|
| 1404 |
+
body_flip_frames = max(1, int(round(int(args.person_body_flip_frames) * _fscale)))
|
| 1405 |
+
person_stride = max(1, int(round(int(args.person_check_stride) * _fscale)))
|
| 1406 |
+
gate_move_eps = float(args.face_gate_move_eps) / _fscale
|
| 1407 |
gate_state["count"] = 0
|
| 1408 |
gate_state["cx"] = None
|
| 1409 |
gate_state["cy"] = None
|
| 1410 |
gate_state["absent"] = 0
|
|
|
|
| 1411 |
gate_state["absent_hold"] = False
|
| 1412 |
gate_state["present"] = 0
|
| 1413 |
gate_state["person_check_i"] = 0
|
|
|
|
| 1645 |
continue
|
| 1646 |
|
| 1647 |
if kv_reset_frames > 0 and frames_since_session_reset >= kv_reset_frames:
|
| 1648 |
+
face_gate_pending = entry_gate
|
| 1649 |
gate_state["count"] = 0
|
| 1650 |
gate_state["cx"] = None
|
| 1651 |
gate_state["cy"] = None
|
|
|
|
| 1698 |
gate_state["csz"] = _csz
|
| 1699 |
return "off_center"
|
| 1700 |
_pcx, _pcy, _pcsz = gate_state["cx"], gate_state["cy"], gate_state.get("csz")
|
| 1701 |
+
_eps = gate_move_eps
|
| 1702 |
_cap = float(args.face_gate_settle_drift)
|
| 1703 |
|
| 1704 |
_szeps = _eps * 0.5
|
|
|
|
| 1724 |
gate_state["pe_anchor"] = frame
|
| 1725 |
return "__gate_pe__"
|
| 1726 |
|
| 1727 |
+
if gate_on and not face_gate_pending:
|
| 1728 |
+
_tick = gate_state.get("person_check_i", 0)
|
| 1729 |
+
_gfaces, _gfw, _gfh = _detect_gate_faces(frame, onnx_path=args.face_detector_onnx, score_thresh=fg_score)
|
| 1730 |
+
if _tick == 0 or gate_state.get("absent_hold"):
|
| 1731 |
+
gate_state["person_last"] = _person_present(
|
| 1732 |
+
frame, onnx_path=args.person_detector_onnx, conf=float(args.person_gate_conf))
|
| 1733 |
+
|
| 1734 |
+
if gate_state["person_last"]:
|
| 1735 |
+
gate_state["face_last"] = _face_present_from(
|
| 1736 |
+
_gfaces, _gfw, _gfh,
|
| 1737 |
+
min_ratio=float(args.face_present_min_ratio),
|
| 1738 |
+
edge_margin=float(args.face_present_edge_margin))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1739 |
else:
|
| 1740 |
+
gate_state["face_last"] = True
|
| 1741 |
+
gate_state["person_check_i"] = (_tick + 1) % person_stride
|
| 1742 |
+
_body_here = bool(gate_state["person_last"])
|
| 1743 |
+
_face_here = bool(gate_state.get("face_last", True))
|
| 1744 |
+
_present = _body_here and _face_here
|
| 1745 |
+
_reason_now = "no_person" if not _body_here else ("no_face" if not _face_here else "")
|
| 1746 |
+
|
| 1747 |
+
body_flip = body_flip_frames
|
| 1748 |
+
if _body_here:
|
| 1749 |
+
gate_state["body_miss"] = 0
|
| 1750 |
+
else:
|
| 1751 |
+
gate_state["body_miss"] = gate_state.get("body_miss", 0) + 1
|
| 1752 |
+
if _present:
|
| 1753 |
+
gate_state["absent"] = 0
|
|
|
|
|
|
|
| 1754 |
if gate_state.get("absent_hold"):
|
| 1755 |
+
gate_state["present"] = gate_state.get("present", 0) + 1
|
| 1756 |
+
if gate_state["present"] >= fg_return:
|
| 1757 |
+
gate_state["absent_hold"] = False
|
| 1758 |
+
gate_state["present"] = 0
|
| 1759 |
+
print("#####[PERSON-GATE] subject returned (stable) -> re-run startup gate (reset)", flush=True)
|
| 1760 |
+
return ("__person_returned__",)
|
| 1761 |
+
|
| 1762 |
+
else:
|
| 1763 |
+
gate_state["present"] = 0
|
| 1764 |
+
gate_state["absent"] += 1
|
| 1765 |
+
|
| 1766 |
+
if _reason_now == "no_person" and gate_state.get("body_miss", 0) < body_flip:
|
| 1767 |
+
_reason_now = "no_face"
|
| 1768 |
+
gate_state["hold_reason"] = _reason_now or gate_state.get("hold_reason") or "no_person"
|
| 1769 |
+
if not gate_state.get("absent_hold") and gate_state["absent"] >= fg_absent:
|
| 1770 |
+
gate_state["absent_hold"] = True
|
| 1771 |
+
|
| 1772 |
+
try:
|
| 1773 |
+
if session is not None:
|
| 1774 |
+
session.pending_frames.clear()
|
| 1775 |
+
session.pending_metas.clear()
|
| 1776 |
+
except Exception:
|
| 1777 |
+
pass
|
| 1778 |
+
print(f"#####[PERSON-GATE] {gate_state['hold_reason']} for {gate_state['absent']} frames -> black-hold", flush=True)
|
| 1779 |
+
if gate_state.get("absent_hold"):
|
| 1780 |
+
return ("__no_person__", gate_state.get("hold_reason", "no_person"))
|
| 1781 |
+
|
| 1782 |
+
_n = _count_faces_from(
|
| 1783 |
+
_gfaces, _gfw, _gfh,
|
| 1784 |
+
count_min_ratio=float(args.count_face_min_ratio))
|
| 1785 |
+
if gate_state["subject_count"] is None:
|
| 1786 |
+
gate_state["subject_count"] = _n
|
| 1787 |
+
gate_state["cand"] = None
|
| 1788 |
+
gate_state["cand_n"] = 0
|
| 1789 |
+
elif _n > gate_state["subject_count"]:
|
| 1790 |
+
if _n == gate_state["cand"]:
|
| 1791 |
+
gate_state["cand_n"] += 1
|
| 1792 |
+
else:
|
| 1793 |
+
gate_state["cand"] = _n
|
| 1794 |
+
gate_state["cand_n"] = 1
|
| 1795 |
+
if gate_state["cand_n"] >= count_change_frames:
|
| 1796 |
+
gate_state["recount"] = True
|
| 1797 |
gate_state["subject_count"] = _n
|
| 1798 |
gate_state["cand"] = None
|
| 1799 |
gate_state["cand_n"] = 0
|
| 1800 |
+
elif _n < gate_state["subject_count"]:
|
| 1801 |
+
if _n == gate_state["cand"]:
|
| 1802 |
+
gate_state["cand_n"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1803 |
else:
|
| 1804 |
+
gate_state["cand"] = _n
|
| 1805 |
+
gate_state["cand_n"] = 1
|
| 1806 |
+
if gate_state["cand_n"] >= count_change_frames:
|
| 1807 |
+
gate_state["subject_count"] = _n
|
| 1808 |
gate_state["cand"] = None
|
| 1809 |
gate_state["cand_n"] = 0
|
| 1810 |
+
else:
|
| 1811 |
+
gate_state["cand"] = None
|
| 1812 |
+
gate_state["cand_n"] = 0
|
| 1813 |
|
| 1814 |
if pe_defer and not face_gate_pending:
|
| 1815 |
gate_state["pe_anchor"] = frame
|
|
|
|
| 2015 |
|
| 2016 |
parser.add_argument("--face-detector-onnx", type=str, default=DEFAULT_FACE_DETECTOR_ONNX, help="YuNet ONNX weight for the face-presence gate. Missing -> gate disabled (edits run unconditionally).")
|
| 2017 |
parser.add_argument("--face-gate-score", type=float, default=0.35, help="Min YuNet confidence to count as a face. Lower = detects motion-blurred faces (fewer transient drops), but more false positives.")
|
| 2018 |
+
parser.add_argument("--face-present-min-ratio", type=float, default=0.15, help="Mid-session presence: a detected face counts as 'present' only if its short side is >= this fraction of the frame short side. A too-small/partial face (subject sat down so only the top of the head shows) counts as no-face -> black-hold, instead of letting the model t2v-hallucinate a person. 0 = any face counts. Higher = stricter (black out sooner when the face gets small/far). Normal editing faces measure ~0.35, so 0.15 has a wide margin.")
|
| 2019 |
parser.add_argument("--face-present-edge-margin", type=float, default=0.0, help="Mid-session presence: a face whose box comes within this fraction of ANY frame border counts as a HALF/partial face (turned/leaned out) -> no-face -> black-hold, so the model never edits a half-face frame (which it fills in as a t2v hallucination). 0 = no edge check (default: disabled -- the face-box edge check false-blacked too eagerly when a face merely neared a border). Set e.g. 0.02 to re-enable a lenient check.")
|
| 2020 |
+
parser.add_argument("--face-gate-min-below-ratio", type=float, default=0.20, help="Min fraction of frame HEIGHT that must be below the face (torso room, for garment try-on). Bigger -> stricter: the chin must sit higher in frame (back up, sit taller, or re-aim the camera).")
|
| 2021 |
+
parser.add_argument("--face-gate-center-margin", type=float, default=0.35, help="Max |face-center-x - 0.5| (fraction of width) for a SINGLE subject to count as centered. Bigger -> more lenient. SKIPPED entirely when 2+ comparable faces are present (side-by-side people can't be centered). Note: motion/stability is enforced separately by --face-gate-move-eps + --face-gate-settle-drift, so this does not affect the swing-into-frame ghost fix.")
|
| 2022 |
+
parser.add_argument("--face-gate-move-eps", type=float, default=0.02, help="Max per-frame face-center movement (fraction of frame) to count as 'still'. Bigger -> tolerates more motion. Pairs with --face-gate-settle-drift (net drift from anchor) so a slow glide can't creep through frame-by-frame. Also requires per-frame face-size change <= 0.5*eps.")
|
| 2023 |
+
parser.add_argument("--face-gate-settle-drift", type=float, default=0.05, help="Max net drift of the face center AND size from the settle-streak anchor (fraction of frame). Closes the 'slow continuous glide' hole where every per-frame step is < move-eps but they sum to a big slide (swing-into-frame motion baked into chunk0 -> ghost/duplicate person). Smaller = must hold more still. Complements --face-gate-move-eps (per-frame) + --face-gate-stable-frames (streak length).")
|
| 2024 |
+
parser.add_argument("--face-gate-stable-frames", type=int, default=24, help="Consecutive centered+still frames required before editing starts (~24fps send rate, so 24 ≈ 1s).")
|
| 2025 |
|
| 2026 |
+
parser.add_argument("--online-gate", action=argparse.BooleanOptionalAction, default=True, help="Server-wide master for MID-SESSION monitoring (no-person black-hold + person-count re-edit). A session runs it only when its gate_enabled is also true (browser checkbox / start field, default true); the ENTRY gate follows gate_enabled alone. Also seeds the UI checkbox default. --no-online-gate disables mid-session monitoring for all sessions.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2027 |
parser.add_argument("--presence-absent-frames", type=int, default=12, help="Consecutive not-present frames (body missing, OR face too small / half-out per --face-present-*) before the output goes black. Small = stop FAST (less T2V leak on a quick sit-down / turn-away); larger = tolerate a brief occlusion / head-turn without black-holding. ~24fps, 12 ≈ 0.5s.")
|
| 2028 |
parser.add_argument("--presence-return-frames", type=int, default=24, help="Consecutive present (body+face) frames required to LEAVE the black-hold and re-run the startup gate. Separate from --presence-absent-frames so entry stays fast (black out quickly) while exit is well de-bounced: a face flickering through finger gaps while hands cover the face won't bounce no_face<->settling. ~24fps, 24 ≈ 1s.")
|
| 2029 |
+
parser.add_argument("--person-count-change-frames", type=int, default=24, help="Consecutive frames a NEW face count must hold before it is accepted. An INCREASE then re-edits (reset chunk0) so people who enter later get edited; a decrease only lowers the baseline. Debounce vs transient miscounts (sway / motion blur / a background face flickering in). Keep this larger than --presence-absent-frames so a brief face loss black-holds instead of faking a 0->1 'new person' re-edit. ~24fps, 24 ≈ 1s.")
|
| 2030 |
+
parser.add_argument("--count-face-min-ratio", type=float, default=0.45, help="For person-count-change: a face counts as an additional subject only if its short side is >= this fraction of the MAIN (largest/foreground) face's short side; an absolute floor of 5% of the frame short side also applies. Excludes far-smaller BACKGROUND people (e.g. a coworker behind the subject) that otherwise flip the count and trigger spurious re-edits. Higher = stricter (ignore more background).")
|
| 2031 |
+
parser.add_argument("--person-detector-onnx", type=str, default=DEFAULT_PERSON_DETECTOR_ONNX, help="YOLOv8n ONNX (fixed 320 input) for mid-session body presence via cv2.dnn. Missing/unloadable -> the body check passes through (always 'present'); face-present rules still apply, so no_face black-holds can still trigger.")
|
| 2032 |
parser.add_argument("--person-gate-conf", type=float, default=0.4, help="Min YOLO person-class score to count the person as present.")
|
| 2033 |
+
parser.add_argument("--person-check-stride", type=int, default=2, help="Run the person detector every Nth frame during editing (YOLO ~27ms; stride amortizes the cost). Smaller = notices the subject LEAVING sooner, more CPU. During a black-hold the check runs every frame regardless, so return detection is unaffected by the stride.")
|
| 2034 |
parser.add_argument("--person-body-flip-frames", type=int, default=6, help="Consecutive body-misses before the client reason flips to no_person. Below this, a lone YOLO dip (a hand/object over the face also clips the torso) keeps the current reason -- normally show_full_face -- so the hint doesn't strobe no_face<->no_person. Reason-only de-bounce; the black-hold timing (--presence-absent-frames) is unaffected. ~24fps, 6 ≈ 0.25s. Higher = more reluctant to ever show no_person; 1 = report no_person on the first miss (old behavior).")
|
| 2035 |
parser.add_argument("--output-quality", default="auto",
|
| 2036 |
help="Downlink preview quality: 'auto' (RTT-adaptive) or a fixed 1-100.")
|
xvideo/serving/zerogpu_engine.py
CHANGED
|
@@ -14,8 +14,9 @@ is the transport: a `_QueueWebSocket` adapter presents the same
|
|
| 14 |
the in/out queues instead of a socket.
|
| 15 |
|
| 16 |
Reused verbatim (imported, not copied) from serve_joyomni_streaming:
|
| 17 |
-
_check_face_gate,
|
| 18 |
-
_decode_image, _decode_ref_image,
|
|
|
|
| 19 |
"""
|
| 20 |
from __future__ import annotations
|
| 21 |
|
|
@@ -38,7 +39,9 @@ from xvideo.serving.serve_joyomni_streaming import (
|
|
| 38 |
_decode_image,
|
| 39 |
_decode_ref_image,
|
| 40 |
_enhance_prompt_sync,
|
| 41 |
-
|
|
|
|
|
|
|
| 42 |
_optional_positive_int,
|
| 43 |
_person_present,
|
| 44 |
_H264Stream,
|
|
@@ -266,9 +269,8 @@ async def _session_loop(runtime, args, websocket: _QueueWebSocket) -> None:
|
|
| 266 |
face_gate_pending = False
|
| 267 |
pe_defer = False
|
| 268 |
session_max_inflight = max(0, int(args.max_inflight_chunks or 0))
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
count_monitor = False
|
| 272 |
pe_report = None
|
| 273 |
|
| 274 |
fg_score = float(args.face_gate_score)
|
|
@@ -276,6 +278,10 @@ async def _session_loop(runtime, args, websocket: _QueueWebSocket) -> None:
|
|
| 276 |
fg_stable = int(args.face_gate_stable_frames)
|
| 277 |
fg_absent = int(args.presence_absent_frames)
|
| 278 |
fg_return = int(args.presence_return_frames)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
output_quality = 60 if args.output_quality == "auto" else int(args.output_quality)
|
| 280 |
output_codec = "mjpeg"
|
| 281 |
h264_stream: _H264Stream | None = None
|
|
@@ -424,10 +430,23 @@ async def _session_loop(runtime, args, websocket: _QueueWebSocket) -> None:
|
|
| 424 |
session_max_inflight = max(0, int(
|
| 425 |
payload.get("max_inflight_chunks", args.max_inflight_chunks) or 0))
|
| 426 |
use_pe = bool(payload.get("use_pe", args.use_pe)) and bool(os.environ.get("OPENAI_API_KEY"))
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
_reset_gate_state(gate_state)
|
| 432 |
pe_report = None
|
| 433 |
pe_defer = False
|
|
@@ -500,7 +519,7 @@ async def _session_loop(runtime, args, websocket: _QueueWebSocket) -> None:
|
|
| 500 |
continue
|
| 501 |
|
| 502 |
if kv_reset_frames > 0 and frames_since_session_reset >= kv_reset_frames:
|
| 503 |
-
face_gate_pending =
|
| 504 |
_reset_gate_state(gate_state)
|
| 505 |
await _reset_session("kv_reset_frames")
|
| 506 |
continue
|
|
@@ -509,10 +528,11 @@ async def _session_loop(runtime, args, websocket: _QueueWebSocket) -> None:
|
|
| 509 |
frame = uplink_frame if uplink_frame is not None else _decode_image(frame_bytes)
|
| 510 |
sentinel = _apply_gate(
|
| 511 |
frame, gate_state, args,
|
| 512 |
-
face_gate_pending=face_gate_pending,
|
| 513 |
-
face_required=face_required, count_monitor=count_monitor, pe_defer=pe_defer,
|
| 514 |
fg_score=fg_score, fg_min_below=fg_min_below, fg_stable=fg_stable,
|
| 515 |
-
fg_absent=fg_absent, fg_return=fg_return,
|
|
|
|
|
|
|
| 516 |
)
|
| 517 |
if sentinel is not None:
|
| 518 |
return sentinel
|
|
@@ -614,9 +634,9 @@ def _reset_gate_state(gs: dict) -> None:
|
|
| 614 |
"settle_ay": None, "settle_asz": None, "pe_anchor": None})
|
| 615 |
|
| 616 |
|
| 617 |
-
def _apply_gate(frame, gs, args, *, face_gate_pending,
|
| 618 |
-
|
| 619 |
-
|
| 620 |
if face_gate_pending:
|
| 621 |
reason, center, nf = _check_face_gate(
|
| 622 |
frame, onnx_path=args.face_detector_onnx,
|
|
@@ -631,7 +651,7 @@ def _apply_gate(frame, gs, args, *, face_gate_pending, presence_monitor, face_re
|
|
| 631 |
gs["settle_asz"] = None; gs["cx"] = cx; gs["cy"] = cy; gs["csz"] = csz
|
| 632 |
return "off_center"
|
| 633 |
pcx, pcy, pcsz = gs.get("cx"), gs.get("cy"), gs.get("csz")
|
| 634 |
-
eps =
|
| 635 |
szeps = eps * 0.5
|
| 636 |
still = (pcx is not None and abs(cx - pcx) <= eps and abs(cy - pcy) <= eps
|
| 637 |
and pcsz is not None and abs(csz - pcsz) <= szeps)
|
|
@@ -648,69 +668,66 @@ def _apply_gate(frame, gs, args, *, face_gate_pending, presence_monitor, face_re
|
|
| 648 |
gs["pe_anchor"] = frame
|
| 649 |
return "__gate_pe__"
|
| 650 |
|
| 651 |
-
if
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
else:
|
| 664 |
-
gs["face_last"] = True
|
| 665 |
-
gs["person_check_i"] = (tick + 1) % stride
|
| 666 |
-
body_here = bool(gs["person_last"]); face_here = bool(gs.get("face_last", True))
|
| 667 |
-
present = body_here and face_here
|
| 668 |
-
reason_now = "no_person" if not body_here else ("no_face" if not face_here else "")
|
| 669 |
-
body_flip = max(1, int(args.person_body_flip_frames))
|
| 670 |
-
if body_here:
|
| 671 |
-
gs["body_miss"] = 0
|
| 672 |
else:
|
| 673 |
-
gs["
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
gs["hold_reason"] = reason_now or gs.get("hold_reason") or "no_person"
|
| 686 |
-
if not gs.get("absent_hold") and gs["absent"] >= fg_absent:
|
| 687 |
-
gs["absent_hold"] = True
|
| 688 |
-
try:
|
| 689 |
-
if session is not None:
|
| 690 |
-
session.pending_frames.clear(); session.pending_metas.clear()
|
| 691 |
-
except Exception: # noqa: BLE001
|
| 692 |
-
pass
|
| 693 |
if gs.get("absent_hold"):
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
else:
|
| 713 |
-
gs["cand"] =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
|
| 715 |
if pe_defer and not face_gate_pending:
|
| 716 |
gs["pe_anchor"] = frame
|
|
|
|
| 14 |
the in/out queues instead of a socket.
|
| 15 |
|
| 16 |
Reused verbatim (imported, not copied) from serve_joyomni_streaming:
|
| 17 |
+
_check_face_gate, _detect_gate_faces, _face_present_from, _count_faces_from,
|
| 18 |
+
_person_present, _enhance_prompt_sync, _decode_image, _decode_ref_image,
|
| 19 |
+
_optional_positive_int
|
| 20 |
"""
|
| 21 |
from __future__ import annotations
|
| 22 |
|
|
|
|
| 39 |
_decode_image,
|
| 40 |
_decode_ref_image,
|
| 41 |
_enhance_prompt_sync,
|
| 42 |
+
_count_faces_from,
|
| 43 |
+
_detect_gate_faces,
|
| 44 |
+
_face_present_from,
|
| 45 |
_optional_positive_int,
|
| 46 |
_person_present,
|
| 47 |
_H264Stream,
|
|
|
|
| 269 |
face_gate_pending = False
|
| 270 |
pe_defer = False
|
| 271 |
session_max_inflight = max(0, int(args.max_inflight_chunks or 0))
|
| 272 |
+
entry_gate = False
|
| 273 |
+
gate_on = False
|
|
|
|
| 274 |
pe_report = None
|
| 275 |
|
| 276 |
fg_score = float(args.face_gate_score)
|
|
|
|
| 278 |
fg_stable = int(args.face_gate_stable_frames)
|
| 279 |
fg_absent = int(args.presence_absent_frames)
|
| 280 |
fg_return = int(args.presence_return_frames)
|
| 281 |
+
count_change_frames = max(1, int(args.person_count_change_frames))
|
| 282 |
+
body_flip_frames = max(1, int(args.person_body_flip_frames))
|
| 283 |
+
person_stride = max(1, int(args.person_check_stride))
|
| 284 |
+
gate_move_eps = float(args.face_gate_move_eps)
|
| 285 |
output_quality = 60 if args.output_quality == "auto" else int(args.output_quality)
|
| 286 |
output_codec = "mjpeg"
|
| 287 |
h264_stream: _H264Stream | None = None
|
|
|
|
| 430 |
session_max_inflight = max(0, int(
|
| 431 |
payload.get("max_inflight_chunks", args.max_inflight_chunks) or 0))
|
| 432 |
use_pe = bool(payload.get("use_pe", args.use_pe)) and bool(os.environ.get("OPENAI_API_KEY"))
|
| 433 |
+
entry_gate = bool(payload.get("gate_enabled", True))
|
| 434 |
+
face_gate_pending = entry_gate
|
| 435 |
+
gate_on = bool(args.online_gate) and entry_gate
|
| 436 |
+
fg_score = float(payload.get("fg_score", args.face_gate_score))
|
| 437 |
+
fg_min_below = float(payload.get("fg_min_below_ratio", args.face_gate_min_below_ratio))
|
| 438 |
+
fg_stable = max(1, int(payload.get("fg_stable_frames", args.face_gate_stable_frames)))
|
| 439 |
+
fg_absent = max(1, int(args.presence_absent_frames))
|
| 440 |
+
fg_return = max(1, int(args.presence_return_frames))
|
| 441 |
+
_gate_fps = float(payload.get("fps") or args.fps or 24.0)
|
| 442 |
+
_fscale = _gate_fps / 24.0
|
| 443 |
+
fg_stable = max(1, int(round(fg_stable * _fscale)))
|
| 444 |
+
fg_absent = max(1, int(round(fg_absent * _fscale)))
|
| 445 |
+
fg_return = max(1, int(round(fg_return * _fscale)))
|
| 446 |
+
count_change_frames = max(1, int(round(int(args.person_count_change_frames) * _fscale)))
|
| 447 |
+
body_flip_frames = max(1, int(round(int(args.person_body_flip_frames) * _fscale)))
|
| 448 |
+
person_stride = max(1, int(round(int(args.person_check_stride) * _fscale)))
|
| 449 |
+
gate_move_eps = float(args.face_gate_move_eps) / _fscale
|
| 450 |
_reset_gate_state(gate_state)
|
| 451 |
pe_report = None
|
| 452 |
pe_defer = False
|
|
|
|
| 519 |
continue
|
| 520 |
|
| 521 |
if kv_reset_frames > 0 and frames_since_session_reset >= kv_reset_frames:
|
| 522 |
+
face_gate_pending = entry_gate
|
| 523 |
_reset_gate_state(gate_state)
|
| 524 |
await _reset_session("kv_reset_frames")
|
| 525 |
continue
|
|
|
|
| 528 |
frame = uplink_frame if uplink_frame is not None else _decode_image(frame_bytes)
|
| 529 |
sentinel = _apply_gate(
|
| 530 |
frame, gate_state, args,
|
| 531 |
+
face_gate_pending=face_gate_pending, gate_on=gate_on, pe_defer=pe_defer,
|
|
|
|
| 532 |
fg_score=fg_score, fg_min_below=fg_min_below, fg_stable=fg_stable,
|
| 533 |
+
fg_absent=fg_absent, fg_return=fg_return, person_stride=person_stride,
|
| 534 |
+
body_flip_frames=body_flip_frames, count_change_frames=count_change_frames,
|
| 535 |
+
gate_move_eps=gate_move_eps, session=session,
|
| 536 |
)
|
| 537 |
if sentinel is not None:
|
| 538 |
return sentinel
|
|
|
|
| 634 |
"settle_ay": None, "settle_asz": None, "pe_anchor": None})
|
| 635 |
|
| 636 |
|
| 637 |
+
def _apply_gate(frame, gs, args, *, face_gate_pending, gate_on, pe_defer, fg_score,
|
| 638 |
+
fg_min_below, fg_stable, fg_absent, fg_return, person_stride,
|
| 639 |
+
body_flip_frames, count_change_frames, gate_move_eps, session):
|
| 640 |
if face_gate_pending:
|
| 641 |
reason, center, nf = _check_face_gate(
|
| 642 |
frame, onnx_path=args.face_detector_onnx,
|
|
|
|
| 651 |
gs["settle_asz"] = None; gs["cx"] = cx; gs["cy"] = cy; gs["csz"] = csz
|
| 652 |
return "off_center"
|
| 653 |
pcx, pcy, pcsz = gs.get("cx"), gs.get("cy"), gs.get("csz")
|
| 654 |
+
eps = gate_move_eps; cap = float(args.face_gate_settle_drift)
|
| 655 |
szeps = eps * 0.5
|
| 656 |
still = (pcx is not None and abs(cx - pcx) <= eps and abs(cy - pcy) <= eps
|
| 657 |
and pcsz is not None and abs(csz - pcsz) <= szeps)
|
|
|
|
| 668 |
gs["pe_anchor"] = frame
|
| 669 |
return "__gate_pe__"
|
| 670 |
|
| 671 |
+
if gate_on and not face_gate_pending:
|
| 672 |
+
gfaces, gfw, gfh = _detect_gate_faces(
|
| 673 |
+
frame, onnx_path=args.face_detector_onnx, score_thresh=fg_score)
|
| 674 |
+
tick = gs.get("person_check_i", 0)
|
| 675 |
+
if tick == 0 or gs.get("absent_hold"):
|
| 676 |
+
gs["person_last"] = _person_present(
|
| 677 |
+
frame, onnx_path=args.person_detector_onnx, conf=float(args.person_gate_conf))
|
| 678 |
+
if gs["person_last"]:
|
| 679 |
+
gs["face_last"] = _face_present_from(
|
| 680 |
+
gfaces, gfw, gfh,
|
| 681 |
+
min_ratio=float(args.face_present_min_ratio),
|
| 682 |
+
edge_margin=float(args.face_present_edge_margin))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
else:
|
| 684 |
+
gs["face_last"] = True
|
| 685 |
+
gs["person_check_i"] = (tick + 1) % person_stride
|
| 686 |
+
body_here = bool(gs["person_last"]); face_here = bool(gs.get("face_last", True))
|
| 687 |
+
present = body_here and face_here
|
| 688 |
+
reason_now = "no_person" if not body_here else ("no_face" if not face_here else "")
|
| 689 |
+
body_flip = body_flip_frames
|
| 690 |
+
if body_here:
|
| 691 |
+
gs["body_miss"] = 0
|
| 692 |
+
else:
|
| 693 |
+
gs["body_miss"] = gs.get("body_miss", 0) + 1
|
| 694 |
+
if present:
|
| 695 |
+
gs["absent"] = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
if gs.get("absent_hold"):
|
| 697 |
+
gs["present"] = gs.get("present", 0) + 1
|
| 698 |
+
if gs["present"] >= fg_return:
|
| 699 |
+
gs["absent_hold"] = False; gs["present"] = 0
|
| 700 |
+
return ("__person_returned__",)
|
| 701 |
+
else:
|
| 702 |
+
gs["present"] = 0; gs["absent"] += 1
|
| 703 |
+
if reason_now == "no_person" and gs.get("body_miss", 0) < body_flip:
|
| 704 |
+
reason_now = "no_face"
|
| 705 |
+
gs["hold_reason"] = reason_now or gs.get("hold_reason") or "no_person"
|
| 706 |
+
if not gs.get("absent_hold") and gs["absent"] >= fg_absent:
|
| 707 |
+
gs["absent_hold"] = True
|
| 708 |
+
try:
|
| 709 |
+
if session is not None:
|
| 710 |
+
session.pending_frames.clear(); session.pending_metas.clear()
|
| 711 |
+
except Exception: # noqa: BLE001
|
| 712 |
+
pass
|
| 713 |
+
if gs.get("absent_hold"):
|
| 714 |
+
return ("__no_person__", gs.get("hold_reason", "no_person"))
|
| 715 |
+
|
| 716 |
+
n = _count_faces_from(
|
| 717 |
+
gfaces, gfw, gfh, count_min_ratio=float(args.count_face_min_ratio))
|
| 718 |
+
if gs["subject_count"] is None:
|
| 719 |
+
gs["subject_count"] = n; gs["cand"] = None; gs["cand_n"] = 0
|
| 720 |
+
elif n != gs["subject_count"]:
|
| 721 |
+
if n == gs["cand"]:
|
| 722 |
+
gs["cand_n"] += 1
|
| 723 |
else:
|
| 724 |
+
gs["cand"] = n; gs["cand_n"] = 1
|
| 725 |
+
if gs["cand_n"] >= count_change_frames:
|
| 726 |
+
if n > gs["subject_count"]:
|
| 727 |
+
gs["recount"] = True
|
| 728 |
+
gs["subject_count"] = n; gs["cand"] = None; gs["cand_n"] = 0
|
| 729 |
+
else:
|
| 730 |
+
gs["cand"] = None; gs["cand_n"] = 0
|
| 731 |
|
| 732 |
if pe_defer and not face_gate_pending:
|
| 733 |
gs["pe_anchor"] = frame
|