Commit a4e82556f579e3f7e121bfaf2f2431b52966c138
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
Showing
2 changed files
with
55 additions
and
4 deletions
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" |