wxDai commited on
Commit
ccf6aed
·
verified ·
1 Parent(s): 1d73070

sync to e756766+9ee7f46: shared gate detection, fps-scaled gate timings, session hardening UI; adapt engine to new gate API

Browse files
.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

  • SHA256: d1a609f8c0fc7db1c3798857a6d55615925305abcdc1b6fc876b0e3a0816a931
  • Pointer size: 131 Bytes
  • Size of remote file: 228 kB
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: "Change Outfit",
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: "像素游戏", 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." },
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: "给主角的���颌和脸颊添加浓密的深黄色络腮胡须,自然卷曲,覆盖下巴和两颊。", text_en: "Add a thick dark-blond full beard to the subject's jaw and cheeks, naturally curly, covering the chin and both cheeks." },
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: "变成机械生化体", 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." },
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: "Swap Identity",
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
- const bv = b.getAttribute("data-upq");
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(upAuto ? "auto" : (qualityLegacyEl ? qualityLegacyEl.value : "0.4"));
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 (v === "auto") {
2575
- upAuto = true;
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
- def _face_present(image: Image.Image, *, onnx_path: str, score_thresh: float, min_ratio: float = 0.0, edge_margin: float = 0.0) -> bool:
 
268
  det = _get_face_detector(onnx_path, score_thresh)
269
  if det is None:
270
- return True
 
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 False
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 f in faces:
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, "passthrough": False,
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 websocket.send_json(payload)
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 websocket.send_json({
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 websocket.send_json(
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 websocket.send_json(
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 websocket.send_bytes(wire)
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 websocket.send_json(_chunk_done_msg)
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
- session_ref.close()
 
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
- face_gate_pending = bool(payload.get("gate_enabled", True))
 
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
- presence_monitor = bool(args.online_gate) and bool(
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 = True
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 = float(args.face_gate_move_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 (presence_monitor or count_monitor) and not face_gate_pending:
1683
- if presence_monitor:
1684
- stride = max(1, int(args.person_check_stride))
1685
- _tick = gate_state.get("person_check_i", 0)
1686
- if _tick == 0 or gate_state.get("absent_hold"):
1687
- gate_state["person_last"] = _person_present(
1688
- frame, onnx_path=args.person_detector_onnx, conf=float(args.person_gate_conf))
1689
-
1690
- if face_required and gate_state["person_last"]:
1691
- gate_state["face_last"] = _face_present(
1692
- frame, onnx_path=args.face_detector_onnx, score_thresh=fg_score,
1693
- min_ratio=float(args.face_present_min_ratio),
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["present"] = 0
1720
- gate_state["absent"] += 1
1721
-
1722
- if _reason_now == "no_person" and gate_state.get("body_miss", 0) < body_flip:
1723
- _reason_now = "no_face" if face_required else ""
1724
- gate_state["hold_reason"] = _reason_now or gate_state.get("hold_reason") or "no_person"
1725
- if not gate_state.get("absent_hold") and gate_state["absent"] >= fg_absent:
1726
- gate_state["absent_hold"] = True
1727
-
1728
- try:
1729
- if session is not None:
1730
- session.pending_frames.clear()
1731
- session.pending_metas.clear()
1732
- except Exception:
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
- return ("__no_person__", gate_state.get("hold_reason", "no_person"))
1737
-
1738
- if count_monitor:
1739
- _reason_c, _, _n_faces = _check_face_gate(
1740
- frame, onnx_path=args.face_detector_onnx,
1741
- score_thresh=fg_score, min_below_ratio=fg_min_below,
1742
- count_min_ratio=float(args.count_face_min_ratio))
1743
- _n = _n_faces
1744
- if gate_state["subject_count"] is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1745
  gate_state["subject_count"] = _n
1746
  gate_state["cand"] = None
1747
  gate_state["cand_n"] = 0
1748
- elif _n > gate_state["subject_count"]:
1749
- if _n == gate_state["cand"]:
1750
- gate_state["cand_n"] += 1
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.37, so 0.15 has a wide margin.")
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 (must back up more).")
 
 
 
 
1980
 
1981
- parser.add_argument("--face-gate-center-margin", type=float, default=0.28, 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.")
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-editing (reset chunk0) so people who enter later get edited. Debounce vs transient miscounts (sway / motion blur / a background face flickering in). ~24fps, 24 ≈ 1s.")
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). Default 0.45.")
1990
- parser.add_argument("--person-detector-onnx", type=str, default=DEFAULT_PERSON_DETECTOR_ONNX, help="YOLOv8n ONNX (fixed 320) for mid-session person presence via cv2.dnn. Missing -> passthrough disabled (edits always run).")
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 = faster stop/resume detection, more CPU.")
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, _face_present, _person_present, _enhance_prompt_sync,
18
- _decode_image, _decode_ref_image, _optional_positive_int
 
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
- _face_present,
 
 
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
- presence_monitor = False
270
- face_required = False
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
- face_gate_pending = bool(payload.get("gate_enabled", True)) and bool(args.online_gate)
428
- presence_monitor = bool(args.online_gate) and bool(payload.get("no_person_blank", True))
429
- face_required = bool(args.online_gate) and bool(payload.get("require_face", True))
430
- count_monitor = bool(args.online_gate) and bool(payload.get("person_count_reedit", True))
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = True
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, presence_monitor=presence_monitor,
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, session=session,
 
 
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, presence_monitor, face_required,
618
- count_monitor, pe_defer, fg_score, fg_min_below, fg_stable, fg_absent,
619
- fg_return, session):
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 = float(args.face_gate_move_eps); cap = float(args.face_gate_settle_drift)
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 (presence_monitor or count_monitor) and not face_gate_pending:
652
- if presence_monitor:
653
- stride = max(1, int(args.person_check_stride))
654
- tick = gs.get("person_check_i", 0)
655
- if tick == 0 or gs.get("absent_hold"):
656
- gs["person_last"] = _person_present(
657
- frame, onnx_path=args.person_detector_onnx, conf=float(args.person_gate_conf))
658
- if face_required and gs["person_last"]:
659
- gs["face_last"] = _face_present(
660
- frame, onnx_path=args.face_detector_onnx, score_thresh=fg_score,
661
- min_ratio=float(args.face_present_min_ratio),
662
- edge_margin=float(args.face_present_edge_margin))
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["body_miss"] = gs.get("body_miss", 0) + 1
674
- if present:
675
- gs["absent"] = 0
676
- if gs.get("absent_hold"):
677
- gs["present"] = gs.get("present", 0) + 1
678
- if gs["present"] >= fg_return:
679
- gs["absent_hold"] = False; gs["present"] = 0
680
- return ("__person_returned__",)
681
- else:
682
- gs["present"] = 0; gs["absent"] += 1
683
- if reason_now == "no_person" and gs.get("body_miss", 0) < body_flip:
684
- reason_now = "no_face" if face_required else ""
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
- return ("__no_person__", gs.get("hold_reason", "no_person"))
695
-
696
- if count_monitor:
697
- reason_c, _, n_faces = _check_face_gate(
698
- frame, onnx_path=args.face_detector_onnx, score_thresh=fg_score,
699
- min_below_ratio=fg_min_below, count_min_ratio=float(args.count_face_min_ratio))
700
- n = n_faces
701
- if gs["subject_count"] is None:
702
- gs["subject_count"] = n; gs["cand"] = None; gs["cand_n"] = 0
703
- elif n != gs["subject_count"]:
704
- if n == gs["cand"]:
705
- gs["cand_n"] += 1
706
- else:
707
- gs["cand"] = n; gs["cand_n"] = 1
708
- if gs["cand_n"] >= int(args.person_count_change_frames):
709
- if n > gs["subject_count"]:
710
- gs["recount"] = True
711
- gs["subject_count"] = n; gs["cand"] = None; gs["cand_n"] = 0
 
 
 
 
 
 
 
 
712
  else:
713
- gs["cand"] = None; gs["cand_n"] = 0
 
 
 
 
 
 
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