Commit a4e82556f579e3f7e121bfaf2f2431b52966c138

Authored by yibo
1 parent ad566a41

h3-console/raylight: 支持竖屏视频生成(宽高互换,非独立参数)

MiniMax-H3 竖屏走的还是 width/height 机制,没有单独的 orientation 字段。给4个已实测
横屏档位(1344×768/832×480/448×256/1024×576)各配一个宽高互换的竖屏选项,帧数上限
沿用对应横屏档位的实测值(同总像素数,显存/耗时特征预期一致,但没有专门重跑过竖屏
OOM 边界测试)。前端 RESOLUTIONS 和后端 RESOLUTION_CEILINGS 保持同步。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhWwz7mLf5PLBn6noJoAcG
h3-console/static/index.html
@@ -172,8 +172,10 @@ @@ -172,8 +172,10 @@
172 <textarea id="fl2va-prompt" rows="10" placeholder="..."></textarea> 172 <textarea id="fl2va-prompt" rows="10" placeholder="..."></textarea>
173 <label>首帧图</label> 173 <label>首帧图</label>
174 <input id="fl2va-first" type="file" accept="image/*" onchange="uploadTo('fl2va-first')"> 174 <input id="fl2va-first" type="file" accept="image/*" onchange="uploadTo('fl2va-first')">
  175 + <input id="fl2va-first-url" type="url" placeholder="或粘贴图片 URL,回车添加" onkeydown="if(event.key==='Enter'){event.preventDefault();addSingleUrl('fl2va-first','fl2va-first-url',renderFl2vaThumbs,'fl2va-err')}">
175 <label>尾帧图(可选)</label> 176 <label>尾帧图(可选)</label>
176 <input id="fl2va-last" type="file" accept="image/*" onchange="uploadTo('fl2va-last')"> 177 <input id="fl2va-last" type="file" accept="image/*" onchange="uploadTo('fl2va-last')">
  178 + <input id="fl2va-last-url" type="url" placeholder="或粘贴图片 URL,回车添加" onkeydown="if(event.key==='Enter'){event.preventDefault();addSingleUrl('fl2va-last','fl2va-last-url',renderFl2vaThumbs,'fl2va-err')}">
177 <div class="thumbrow" id="fl2va-thumbs"></div> 179 <div class="thumbrow" id="fl2va-thumbs"></div>
178 <div class="row"> 180 <div class="row">
179 <div><label>分辨率</label><select id="fl2va-res" class="res-select"></select></div> 181 <div><label>分辨率</label><select id="fl2va-res" class="res-select"></select></div>
@@ -210,12 +212,15 @@ @@ -210,12 +212,15 @@
210 <textarea id="ref2va-prompt" rows="10" placeholder="..."></textarea> 212 <textarea id="ref2va-prompt" rows="10" placeholder="..."></textarea>
211 <label>参考图(可多选,没有张数上限,越多序列越长越慢)</label> 213 <label>参考图(可多选,没有张数上限,越多序列越长越慢)</label>
212 <input id="ref2va-refs" type="file" accept="image/*" multiple onchange="uploadMulti()"> 214 <input id="ref2va-refs" type="file" accept="image/*" multiple onchange="uploadMulti()">
  215 + <input id="ref2va-refs-url" type="url" placeholder="或粘贴图片 URL,回车添加(可加多个)" onkeydown="if(event.key==='Enter'){event.preventDefault();addArrayUrl('ref2va-refs','ref2va-refs-url',renderRefThumbs,'ref2va-err')}">
213 <div class="thumbrow" id="ref2va-thumbs"></div> 216 <div class="thumbrow" id="ref2va-thumbs"></div>
214 <label>参考视频(可选,最多3段,2-15秒,音轨会自动带上)</label> 217 <label>参考视频(可选,最多3段,2-15秒,音轨会自动带上)</label>
215 <input id="ref2va-videos-input" type="file" accept="video/*" multiple onchange="uploadMultiTagged('ref2va-videos-input','ref2va-videos','ref2va-video-tags')"> 218 <input id="ref2va-videos-input" type="file" accept="video/*" multiple onchange="uploadMultiTagged('ref2va-videos-input','ref2va-videos','ref2va-video-tags')">
  219 + <input id="ref2va-videos-url" type="url" placeholder="或粘贴视频 URL,回车添加(最多3个)" onkeydown="if(event.key==='Enter'){event.preventDefault();addArrayUrl('ref2va-videos','ref2va-videos-url',()=>renderTags('ref2va-video-tags','ref2va-videos'),'ref2va-err',3)}">
216 <div class="thumbrow" id="ref2va-video-tags"></div> 220 <div class="thumbrow" id="ref2va-video-tags"></div>
217 <label>独立音频参考(可选,最多3个,不跟参考视频绑定)</label> 221 <label>独立音频参考(可选,最多3个,不跟参考视频绑定)</label>
218 <input id="ref2va-audios-input" type="file" accept="audio/*" multiple onchange="uploadMultiTagged('ref2va-audios-input','ref2va-audios','ref2va-audio-tags')"> 222 <input id="ref2va-audios-input" type="file" accept="audio/*" multiple onchange="uploadMultiTagged('ref2va-audios-input','ref2va-audios','ref2va-audio-tags')">
  223 + <input id="ref2va-audios-url" type="url" placeholder="或粘贴音频 URL,回车添加(最多3个)" onkeydown="if(event.key==='Enter'){event.preventDefault();addArrayUrl('ref2va-audios','ref2va-audios-url',()=>renderTags('ref2va-audio-tags','ref2va-audios'),'ref2va-err',3)}">
219 <div class="thumbrow" id="ref2va-audio-tags"></div> 224 <div class="thumbrow" id="ref2va-audio-tags"></div>
220 <div class="row"> 225 <div class="row">
221 <div><label>分辨率</label><select id="ref2va-res" class="res-select"></select></div> 226 <div><label>分辨率</label><select id="ref2va-res" class="res-select"></select></div>
@@ -368,11 +373,18 @@ function showCreate(pushUrl = true) { @@ -368,11 +373,18 @@ function showCreate(pushUrl = true) {
368 // 数字改动两边一起改。832×480/1024×576/1344×768 三个是真实CUDA OOM边界;448×256 技术上测到 373 // 数字改动两边一起改。832×480/1024×576/1344×768 三个是真实CUDA OOM边界;448×256 技术上测到
369 // 3000帧都没OOM,1200帧(50秒)是画质边界(用户看过实际输出判定50秒往后不能看),不是显存边界。 374 // 3000帧都没OOM,1200帧(50秒)是画质边界(用户看过实际输出判定50秒往后不能看),不是显存边界。
370 // 1344×768(官方默认分辨率)排第一,作为新建任务的默认选项——2026-08-23 用户要求。 375 // 1344×768(官方默认分辨率)排第一,作为新建任务的默认选项——2026-08-23 用户要求。
  376 +// 2026-09-01:竖屏是宽高互换实现的,不是独立的横竖屏参数(生产已有 720×1280 提交案例,见
  377 +// docs/todo/h3-showcase-cases.md)。maxFrames 沿用对应横屏档位的实测值——宽高互换总像素数不变,
  378 +// 显存/耗时特征预期一致,但没有单独针对竖屏重新跑过 OOM 边界测试,如果发现竖屏更早 OOM 要单独下调。
371 const RESOLUTIONS = [ 379 const RESOLUTIONS = [
372 - { value: '1344x768', width: 1344, height: 768, label: '1344×768(官方默认分辨率)', maxFrames: 412 },  
373 - { value: '832x480', width: 832, height: 480, label: '832×480(标准档)', maxFrames: 1125 },  
374 - { value: '448x256', width: 448, height: 256, label: '448×256(长时长档)', maxFrames: 1200 },  
375 - { value: '1024x576', width: 1024, height: 576, label: '1024×576(高清档)', maxFrames: 743 }, 380 + { value: '1344x768', width: 1344, height: 768, label: '1344×768(官方默认分辨率,横屏)', maxFrames: 412 },
  381 + { value: '768x1344', width: 768, height: 1344, label: '768×1344(官方默认分辨率,竖屏)', maxFrames: 412 },
  382 + { value: '832x480', width: 832, height: 480, label: '832×480(标准档,横屏)', maxFrames: 1125 },
  383 + { value: '480x832', width: 480, height: 832, label: '480×832(标准档,竖屏)', maxFrames: 1125 },
  384 + { value: '448x256', width: 448, height: 256, label: '448×256(长时长档,横屏)', maxFrames: 1200 },
  385 + { value: '256x448', width: 256, height: 448, label: '256×448(长时长档,竖屏)', maxFrames: 1200 },
  386 + { value: '1024x576', width: 1024, height: 576, label: '1024×576(高清档,横屏)', maxFrames: 743 },
  387 + { value: '576x1024', width: 576, height: 1024, label: '576×1024(高清档,竖屏)', maxFrames: 743 },
376 ]; 388 ];
377 const DURATIONS = [ 389 const DURATIONS = [
378 { value: '124', frames: 124, label: '5s' }, 390 { value: '124', frames: 124, label: '5s' },
@@ -441,6 +453,34 @@ async function uploadFile(file) { @@ -441,6 +453,34 @@ async function uploadFile(file) {
441 return await r.json(); 453 return await r.json();
442 } 454 }
443 455
  456 +// 外部 URL 不经过 /v1/assets 转存,直接把地址原样交给后端——raylight 拿到 reference_image_urls
  457 +// 等字段里的 URL 就是 urllib.request.urlopen() 下载,本机 /v1/assets 生成的 URL 和外部 URL
  458 +// 对它没有区别。所以这里跳过上传,直接把用户粘贴的地址塞进 uploaded 状态。
  459 +// ponytail: 不校验 URL 是否可达/是图片——排队执行时后端下载失败才会报错,不在提交阶段拦截。
  460 +function addSingleUrl(key, inputId, renderFn, errBoxId) {
  461 + const input = document.getElementById(inputId);
  462 + const url = input.value.trim();
  463 + if (!url) return;
  464 + uploaded[key] = url;
  465 + input.value = '';
  466 + document.getElementById(errBoxId).textContent = '';
  467 + renderFn();
  468 +}
  469 +
  470 +function addArrayUrl(key, inputId, renderFn, errBoxId, cap) {
  471 + const input = document.getElementById(inputId);
  472 + const url = input.value.trim();
  473 + if (!url) return;
  474 + if (cap && uploaded[key].length >= cap) {
  475 + document.getElementById(errBoxId).textContent = `最多${cap}个,已达上限`;
  476 + return;
  477 + }
  478 + uploaded[key].push(url);
  479 + input.value = '';
  480 + document.getElementById(errBoxId).textContent = '';
  481 + renderFn();
  482 +}
  483 +
444 // server.py 把原始文件名塞进了存盘文件名里(<aid>__<原名>.ext),这里直接从 URL 反解出来 484 // server.py 把原始文件名塞进了存盘文件名里(<aid>__<原名>.ext),这里直接从 URL 反解出来
445 // 显示——不用单独维护一份 filename 状态,旧任务(改动前上传的,URL 里没有 __ 分段)会 485 // 显示——不用单独维护一份 filename 状态,旧任务(改动前上传的,URL 里没有 __ 分段)会
446 // 退化成显示 uuid 文件名,不算错误,只是没有原名可用。 486 // 退化成显示 uuid 文件名,不算错误,只是没有原名可用。
@@ -629,12 +669,17 @@ function clearForm(kind) { @@ -629,12 +669,17 @@ function clearForm(kind) {
629 uploaded['fl2va-first'] = null; uploaded['fl2va-last'] = null; 669 uploaded['fl2va-first'] = null; uploaded['fl2va-last'] = null;
630 document.getElementById('fl2va-first').value = ''; 670 document.getElementById('fl2va-first').value = '';
631 document.getElementById('fl2va-last').value = ''; 671 document.getElementById('fl2va-last').value = '';
  672 + document.getElementById('fl2va-first-url').value = '';
  673 + document.getElementById('fl2va-last-url').value = '';
632 renderFl2vaThumbs(); 674 renderFl2vaThumbs();
633 } else if (kind === 'ref2va') { 675 } else if (kind === 'ref2va') {
634 uploaded['ref2va-refs'] = []; uploaded['ref2va-videos'] = []; uploaded['ref2va-audios'] = []; 676 uploaded['ref2va-refs'] = []; uploaded['ref2va-videos'] = []; uploaded['ref2va-audios'] = [];
635 document.getElementById('ref2va-refs').value = ''; 677 document.getElementById('ref2va-refs').value = '';
636 document.getElementById('ref2va-videos-input').value = ''; 678 document.getElementById('ref2va-videos-input').value = '';
637 document.getElementById('ref2va-audios-input').value = ''; 679 document.getElementById('ref2va-audios-input').value = '';
  680 + document.getElementById('ref2va-refs-url').value = '';
  681 + document.getElementById('ref2va-videos-url').value = '';
  682 + document.getElementById('ref2va-audios-url').value = '';
638 renderRefThumbs(); 683 renderRefThumbs();
639 renderTags('ref2va-video-tags', 'ref2va-videos'); 684 renderTags('ref2va-video-tags', 'ref2va-videos');
640 renderTags('ref2va-audio-tags', 'ref2va-audios'); 685 renderTags('ref2va-audio-tags', 'ref2va-audios');
raylight/scripts/server.py
@@ -96,11 +96,17 @@ RAY_INIT_CFG = { @@ -96,11 +96,17 @@ RAY_INIT_CFG = {
96 # 或画质边界(448×256,见notes.md——技术上测到3000帧都没OOM,是用户看过实际输出判定50秒往后 96 # 或画质边界(448×256,见notes.md——技术上测到3000帧都没OOM,是用户看过实际输出判定50秒往后
97 # 画质不能看,不是显存拦的)。跟 h3-console 前端 RESOLUTIONS 里的 maxFrames 保持一致—— 97 # 画质不能看,不是显存拦的)。跟 h3-console 前端 RESOLUTIONS 里的 maxFrames 保持一致——
98 # 两边算法对不上就会出现"前端放行、后端拒绝"或反过来,数字改动两边一起改。 98 # 两边算法对不上就会出现"前端放行、后端拒绝"或反过来,数字改动两边一起改。
  99 +# 2026-09-01:竖屏(宽高互换)条目沿用对应横屏档位的实测上限——总像素数不变,显存/耗时特征预期
  100 +# 一致,但没有专门为竖屏重跑过 OOM 边界测试,如果实测发现竖屏更早 OOM 要单独下调,不要假设两者永远相等。
99 RESOLUTION_CEILINGS = { 101 RESOLUTION_CEILINGS = {
100 (832, 480): 1125, 102 (832, 480): 1125,
  103 + (480, 832): 1125,
101 (448, 256): 1200, 104 (448, 256): 1200,
  105 + (256, 448): 1200,
102 (1024, 576): 743, 106 (1024, 576): 743,
  107 + (576, 1024): 743,
103 (1344, 768): 412, 108 (1344, 768): 412,
  109 + (768, 1344): 412,
104 } 110 }
105 CLIP_NAME = "qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors" 111 CLIP_NAME = "qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors"
106 VAE_VIDEO = "minimax_h3_video_vae_fp16.safetensors" 112 VAE_VIDEO = "minimax_h3_video_vae_fp16.safetensors"