Delete LTX2.3-1.0.4-new
Browse files- LTX2.3-1.0.4-new/API issues-API问题办法.txt +0 -50
- LTX2.3-1.0.4-new/LTX_Shortcut/LTX Desktop.lnk +0 -0
- LTX2.3-1.0.4-new/UI/i18n.js +0 -646
- LTX2.3-1.0.4-new/UI/index.css +0 -982
- LTX2.3-1.0.4-new/UI/index.html +0 -604
- LTX2.3-1.0.4-new/UI/index.js +0 -0
- LTX2.3-1.0.4-new/main.py +0 -266
- LTX2.3-1.0.4-new/patches/API模式问题修复说明.md +0 -41
- LTX2.3-1.0.4-new/patches/__pycache__/api_types.cpython-313.pyc +0 -0
- LTX2.3-1.0.4-new/patches/__pycache__/app_factory.cpython-313.pyc +0 -3
- LTX2.3-1.0.4-new/patches/__pycache__/keep_models_runtime.cpython-313.pyc +0 -0
- LTX2.3-1.0.4-new/patches/__pycache__/lora_build_hook.cpython-313.pyc +0 -0
- LTX2.3-1.0.4-new/patches/__pycache__/lora_injection.cpython-313.pyc +0 -0
- LTX2.3-1.0.4-new/patches/__pycache__/low_vram_runtime.cpython-313.pyc +0 -0
- LTX2.3-1.0.4-new/patches/__pycache__/ltx_dev_video_pipeline.cpython-313.pyc +0 -0
- LTX2.3-1.0.4-new/patches/__pycache__/ltx_fp8_video_pipeline.cpython-313.pyc +0 -0
- LTX2.3-1.0.4-new/patches/__pycache__/tts_worker.cpython-313.pyc +0 -0
- LTX2.3-1.0.4-new/patches/api_types.py +0 -403
- LTX2.3-1.0.4-new/patches/app_factory.py +0 -0
- LTX2.3-1.0.4-new/patches/app_settings_patch.py +0 -22
- LTX2.3-1.0.4-new/patches/handlers/__pycache__/video_generation_handler.cpython-313.pyc +0 -0
- LTX2.3-1.0.4-new/patches/handlers/video_generation_handler.py +0 -882
- LTX2.3-1.0.4-new/patches/keep_models_runtime.py +0 -16
- LTX2.3-1.0.4-new/patches/launcher.py +0 -20
- LTX2.3-1.0.4-new/patches/lora_build_hook.py +0 -172
- LTX2.3-1.0.4-new/patches/lora_injection.py +0 -139
- LTX2.3-1.0.4-new/patches/low_vram_runtime.py +0 -264
- LTX2.3-1.0.4-new/patches/ltx_dev_video_pipeline.py +0 -156
- LTX2.3-1.0.4-new/patches/ltx_fp8_video_pipeline.py +0 -269
- LTX2.3-1.0.4-new/patches/runtime_policy.py +0 -21
- LTX2.3-1.0.4-new/patches/settings.json +0 -23
- LTX2.3-1.0.4-new/patches/tts_worker.py +0 -222
- LTX2.3-1.0.4-new/run.bat +0 -38
- LTX2.3-1.0.4-new/安装TTS环境.txt +0 -17
LTX2.3-1.0.4-new/API issues-API问题办法.txt
DELETED
|
@@ -1,50 +0,0 @@
|
|
| 1 |
-
1. 复制LTX桌面版的快捷方式到LTX_Shortcut
|
| 2 |
-
|
| 3 |
-
2. 运行run.bat
|
| 4 |
-
----
|
| 5 |
-
1. Copy the LTX desktop shortcut to LTX_Shortcut
|
| 6 |
-
|
| 7 |
-
2. Run run.bat
|
| 8 |
-
----
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
【问题描述 / Problem】
|
| 13 |
-
系统强制使用 FAL API 生成图片,即使本地有 GPU 可用。
|
| 14 |
-
System forces FAL API generation even when local GPU is available.
|
| 15 |
-
|
| 16 |
-
【原因 / Cause】
|
| 17 |
-
LTX 强制要求 GPU 有 31GB VRAM 才会使用本地显卡,低于此值会强制走 API 模式。
|
| 18 |
-
LTX requires 31GB VRAM to use local GPU. Below this, it forces API mode.
|
| 19 |
-
|
| 20 |
-
================================================================================
|
| 21 |
-
【修复方法 / Fix Method】
|
| 22 |
-
================================================================================
|
| 23 |
-
|
| 24 |
-
运行: API issues.bat.bat (以管理员身份)
|
| 25 |
-
Run: API issues.bat.bat (as Administrator)
|
| 26 |
-
|
| 27 |
-
================================================================================
|
| 28 |
-
================================================================================
|
| 29 |
-
|
| 30 |
-
【或者手动 / Or Manual】
|
| 31 |
-
|
| 32 |
-
1. 修改 VRAM 阈值 / Modify VRAM Threshold
|
| 33 |
-
文件路径 / File: C:\Program Files\LTX Desktop\resources\backend\runtime_config\runtime_policy.py
|
| 34 |
-
第16行 / Line 16:
|
| 35 |
-
原 / Original: return vram_gb < 31
|
| 36 |
-
改为 / Change: return vram_gb < 6
|
| 37 |
-
|
| 38 |
-
2. 清空 API Key / Clear API Key
|
| 39 |
-
文件路径 / File: C:\Users\<用户名>\AppData\Local\LTXDesktop\settings.json
|
| 40 |
-
原 / Original: "fal_api_key": "xxxxx"
|
| 41 |
-
改为 / Change: "fal_api_key": ""
|
| 42 |
-
|
| 43 |
-
【说明 / Note】
|
| 44 |
-
- VRAM 阈值改为 6GB,意味着 6GB 及以上显存都会使用本地显卡
|
| 45 |
-
- VRAM threshold set to 6GB means 6GB+ VRAM will use local GPU
|
| 46 |
-
- 清空 fal_api_key 避免系统误判为已配置 API
|
| 47 |
-
- Clear fal_api_key to avoid system thinking API is configured
|
| 48 |
-
- 修改后重启程序即可生效
|
| 49 |
-
- Restart LTX Desktop after changes
|
| 50 |
-
================================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/LTX_Shortcut/LTX Desktop.lnk
DELETED
|
Binary file (1.94 kB)
|
|
|
LTX2.3-1.0.4-new/UI/i18n.js
DELETED
|
@@ -1,646 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* LTX UI i18n — 与根目录「中英文.html」思路类似,但独立脚本、避免坏 DOM/错误路径。
|
| 3 |
-
* 仅维护文案映射;动态节点由 index.js 在语言切换后刷新。
|
| 4 |
-
*/
|
| 5 |
-
(function (global) {
|
| 6 |
-
const STORAGE_KEY = 'ltx_ui_lang';
|
| 7 |
-
|
| 8 |
-
const STR = {
|
| 9 |
-
zh: {
|
| 10 |
-
tabVideo: '视频生成',
|
| 11 |
-
tabBatch: '智能多帧',
|
| 12 |
-
tabMotion: '视频迁移',
|
| 13 |
-
tabImage: '图像生成',
|
| 14 |
-
promptLabel: '视觉描述词 (Prompt)',
|
| 15 |
-
promptPlaceholder: '在此输入视觉描述词 (Prompt)...',
|
| 16 |
-
seedLabel: '随机种子 (Seed)',
|
| 17 |
-
seedRandom: '随机',
|
| 18 |
-
seedFixed: '固定',
|
| 19 |
-
clearVram: '释放显存',
|
| 20 |
-
clearingVram: '清理中...',
|
| 21 |
-
settingsTitle: '系统高级设置',
|
| 22 |
-
langToggleAriaZh: '切换为 English',
|
| 23 |
-
langToggleAriaEn: 'Switch to 中文',
|
| 24 |
-
sysScanning: '正在扫描 GPU...',
|
| 25 |
-
sysBusy: '运算中...',
|
| 26 |
-
sysOnline: '在线 / 就绪',
|
| 27 |
-
sysStarting: '启动中...',
|
| 28 |
-
sysOffline: '未检测到后端 (Port 3000)',
|
| 29 |
-
advancedSettings: '高级设置',
|
| 30 |
-
deviceSelect: '工作设备选择',
|
| 31 |
-
gpuDetecting: '正在检测 GPU...',
|
| 32 |
-
outputPath: '输出与上传存储路径',
|
| 33 |
-
outputPathPh: '例如: D:\\LTX_outputs',
|
| 34 |
-
savePath: '保存路径',
|
| 35 |
-
outputPathHint:
|
| 36 |
-
'系统默认会在 C 盘保留输出文件。请输入新路径后点击保存按钮。',
|
| 37 |
-
lowVram: '低显存优化',
|
| 38 |
-
lowVramDesc:
|
| 39 |
-
'尽量关闭 fast 超分、在加载管线后尝试 CPU 分层卸载(仅当引擎提供 Diffusers 式 API 才可能生效)。每次生成结束会卸载管线。说明:整模型常驻 GPU 时占用仍可能接近满配(例如约 24GB),要明显降占用需更短时长/更低分辨率或 FP8 等小权重。',
|
| 40 |
-
vramLimitLabel: '可用最高显存上限 (GB, 0为全开优先显存)',
|
| 41 |
-
vramLimitPh: '例如: 12 (0表示无限制)',
|
| 42 |
-
saveLabel: '保存',
|
| 43 |
-
modelCheckpointLabel: '视频模型(蒸馏版)',
|
| 44 |
-
modelCheckpointDefault: '默认官方蒸馏模型',
|
| 45 |
-
modelCheckpointHint: '推荐使用 distilled-fp8;仅显示 LTX 2.3 22B 蒸馏模型,避开 dev 模型。',
|
| 46 |
-
modelCheckpointSaved: '已选择模型',
|
| 47 |
-
modelCheckpointNone: '未找到可切换的蒸馏模型',
|
| 48 |
-
modelCheckpointLoadFail: '模型列表加载失败',
|
| 49 |
-
modelLoraSettings: '模型与LoRA设置',
|
| 50 |
-
modelFolder: '模型文件夹',
|
| 51 |
-
modelFolderPh: '当前 LTX 模型目录',
|
| 52 |
-
loraFolder: 'LoRA文件夹',
|
| 53 |
-
loraFolderPh: '模型目录\\loras',
|
| 54 |
-
loraFolderPath: 'LoRA 文件夹路径(可选)',
|
| 55 |
-
loraFolderPathPlaceholder: '留空使用 模型目录\\loras',
|
| 56 |
-
saveScan: '保存并扫描',
|
| 57 |
-
loraPlacementHint: '将 LoRA 文件放到当前模型目录下的 <code>loras</code> 文件夹。',
|
| 58 |
-
loraPlacementHintWithDir:
|
| 59 |
-
'将 LoRA 文件放到当前模型目录: <code>{dir}</code>\\loras',
|
| 60 |
-
basicEngine: '基础画面 / Basic EngineSpecs',
|
| 61 |
-
qualityLevel: '清晰度级别',
|
| 62 |
-
aspectRatio: '画幅比例',
|
| 63 |
-
ratio169: '16:9 电影宽幅',
|
| 64 |
-
ratio916: '9:16 移动竖屏',
|
| 65 |
-
ratio11: '1:1 方形',
|
| 66 |
-
ratio43: '4:3 经典横幅',
|
| 67 |
-
ratio34: '3:4 经典竖幅',
|
| 68 |
-
ratio219: '21:9 超宽银幕',
|
| 69 |
-
ratio921: '9:21 超长竖屏',
|
| 70 |
-
ratioRef: '跟随参考图',
|
| 71 |
-
ratioCustom: '自定义尺寸',
|
| 72 |
-
ratioRefMissing: '请先上传参考图',
|
| 73 |
-
resPreviewPrefix: '最终发送规格',
|
| 74 |
-
fpsLabel: '帧率 (FPS)',
|
| 75 |
-
durationLabel: '时长 (秒)',
|
| 76 |
-
cameraMotion: '镜头运动方式',
|
| 77 |
-
motionStatic: 'Static (静止机位)',
|
| 78 |
-
motionDollyIn: 'Dolly In (推近)',
|
| 79 |
-
motionDollyOut: 'Dolly Out (拉远)',
|
| 80 |
-
motionDollyLeft: 'Dolly Left (向左)',
|
| 81 |
-
motionDollyRight: 'Dolly Right (向右)',
|
| 82 |
-
motionJibUp: 'Jib Up (升臂)',
|
| 83 |
-
motionJibDown: 'Jib Down (降臂)',
|
| 84 |
-
motionFocus: 'Focus Shift (焦点)',
|
| 85 |
-
audioGen: '生成 AI 环境音 (Audio Gen)',
|
| 86 |
-
selectModel: '选择模型',
|
| 87 |
-
selectLora: '选择 LoRA',
|
| 88 |
-
defaultModel: '使用默认模型',
|
| 89 |
-
noLora: '不使用 LoRA',
|
| 90 |
-
loraStrength: 'LoRA 强度',
|
| 91 |
-
genSource: '生成媒介 / Generation Source',
|
| 92 |
-
startFrame: '起始帧 (首帧)',
|
| 93 |
-
endFrame: '结束帧 (尾帧)',
|
| 94 |
-
uploadStart: '上传首帧',
|
| 95 |
-
uploadEnd: '上传尾帧 (可选)',
|
| 96 |
-
refAudio: '参考音频 (A2V)',
|
| 97 |
-
uploadAudio: '点击上传音频',
|
| 98 |
-
sourceHint:
|
| 99 |
-
'💡 若仅上传首帧 = 图生视频/音视频;若同时上传首尾帧 = 首尾插帧。',
|
| 100 |
-
motionTransferTitle: '视频迁移 / Video Transfer',
|
| 101 |
-
motionRefVideoLabel: '参考视频',
|
| 102 |
-
motionVideoUploadText: '点击或拖拽视频',
|
| 103 |
-
motionVideoUploadHint: '用于动作或运镜迁移',
|
| 104 |
-
motionTargetImageLabel: '目标主体图',
|
| 105 |
-
motionImageUploadText: '点击或拖拽图片',
|
| 106 |
-
motionImageUploadHint: '作为主体/首帧引导',
|
| 107 |
-
motionTransferModeLabel: '迁移类型',
|
| 108 |
-
motionModeAction: '动作迁移',
|
| 109 |
-
motionModeCamera: '运镜迁移',
|
| 110 |
-
motionModeRepaint: '视频重绘',
|
| 111 |
-
motionControlType: '控制类型',
|
| 112 |
-
motionControlCanny: 'Canny 轮廓',
|
| 113 |
-
motionControlDepth: 'Depth 深度',
|
| 114 |
-
motionControlPose: 'Pose 姿态',
|
| 115 |
-
motionControlStrength: '控制强度',
|
| 116 |
-
motionTransferHint:
|
| 117 |
-
'动作迁移使用 Pose 姿态控制;运镜迁移使用原始参考视频作为 IC-LoRA guide。',
|
| 118 |
-
motionRefVideoName: '参考视频',
|
| 119 |
-
motionTargetImageName: '目标主体图',
|
| 120 |
-
motionUploadOk: '✅ {label}上传成功: {name}',
|
| 121 |
-
motionUploadFail: '❌ {label}上传失败: {message}',
|
| 122 |
-
motionClearRefVideo: '🧹 已清除参考视频',
|
| 123 |
-
motionClearTargetImage: '🧹 已清除目标主体图',
|
| 124 |
-
motionErrNeedVideo: '请先上传参考视频',
|
| 125 |
-
motionErrNeedImage: '请先上传目标主体图',
|
| 126 |
-
motionDefaultPromptNotice: '视频迁移未填写提示词,已使用默认视频迁移提示词',
|
| 127 |
-
motionStartLog: '正在发起视频迁移: {type}, 控制强度 {strength}',
|
| 128 |
-
motionStartMeta: 'FPS {fps}, 时长 {duration}s',
|
| 129 |
-
uploadFileStart: '正在上传{label}: {name}...',
|
| 130 |
-
fileReadFail: '读取本地文件失败',
|
| 131 |
-
downloadLabel: '下载',
|
| 132 |
-
queueTitle: '任务队列',
|
| 133 |
-
queueIdle: '空闲',
|
| 134 |
-
queueQueued: '排队中',
|
| 135 |
-
queueRunning: '执行中',
|
| 136 |
-
queueComplete: '已完成',
|
| 137 |
-
queueError: '失败',
|
| 138 |
-
queueCancelled: '已取消',
|
| 139 |
-
queueWaiting: '等待 {n}',
|
| 140 |
-
queueRunningSummary: '执行中 1 / 排队 {n}',
|
| 141 |
-
queueNoTasks: '暂无任务',
|
| 142 |
-
queueViewResult: '查看结果',
|
| 143 |
-
queuePosition: '队列第 {n} 位',
|
| 144 |
-
queueTaskTypeVideo: '视频',
|
| 145 |
-
queueTaskTypeMotion: '迁移',
|
| 146 |
-
queueTaskTypeBatch: '批量',
|
| 147 |
-
queueTaskTypeImage: '图像',
|
| 148 |
-
queueSubmitLog: '📥 已加入队列: {id}(前面还有 {n} 个任务)',
|
| 149 |
-
queueDoneLog: '✅ 队列任务完成: {label}',
|
| 150 |
-
queueFailLog: '❌ 队列任务失败: {label} - {error}',
|
| 151 |
-
queueCancelLog: '🛑 队列任务已取消: {label}',
|
| 152 |
-
replayRun: '重跑',
|
| 153 |
-
replayLoad: '载入参数',
|
| 154 |
-
replayLabel: 'Replay',
|
| 155 |
-
replayMissing: '⚠️ 这个历史任务没有可重放参数',
|
| 156 |
-
replayQueuedLog: '↻ Replay 已加入队列: {id}',
|
| 157 |
-
replayLoadedLog: '↗ 已载入 Replay 参数,可微调后重新渲染',
|
| 158 |
-
replayFailed: 'Replay 失败',
|
| 159 |
-
previewLoadSeed: '载入种子',
|
| 160 |
-
previewLoadParams: '载入参数',
|
| 161 |
-
previewNoReplaySeed: '⚠️ 当前预览没有可载入的种子',
|
| 162 |
-
previewNoReplayParams: '⚠️ 当前预览没有可载入的参数',
|
| 163 |
-
previewSeedLoadedLog: '已载入种子 {seed},并切换为固定种子',
|
| 164 |
-
previewNoDownload: '❌ 当前没有可下载的预览内容',
|
| 165 |
-
imgPreset: '预设分辨率 (Presets)',
|
| 166 |
-
imgOptSquare: '1:1 Square (1024x1024)',
|
| 167 |
-
imgOptLand: '16:9 Landscape (1280x720)',
|
| 168 |
-
imgOptPort: '9:16 Portrait (720x1280)',
|
| 169 |
-
imgOptCustom: 'Custom 自定义...',
|
| 170 |
-
width: '宽度',
|
| 171 |
-
height: '高度',
|
| 172 |
-
samplingSteps: '采样步数 (Steps)',
|
| 173 |
-
smartMultiFrameGroup: '智能多帧',
|
| 174 |
-
workflowModeLabel: '工作流模式(点击切换)',
|
| 175 |
-
wfSingle: '单次多关键帧',
|
| 176 |
-
wfSegments: '分段拼接',
|
| 177 |
-
uploadImages: '上传图片',
|
| 178 |
-
uploadMulti1: '点击或拖入多张图片',
|
| 179 |
-
uploadMulti2: '支持一次选多张,可多次添加',
|
| 180 |
-
batchStripTitle: '已选图片 · 顺序 = 播放先后',
|
| 181 |
-
batchStripHint: '在缩略图上按住拖动排序;松手落入虚线框位置',
|
| 182 |
-
batchFfmpegHint:
|
| 183 |
-
'💡 <strong>分段模式</strong>:2 张 = 1 段;3 张 = 2 段再拼接。<strong>单次模式</strong>:几张图就几个 latent 锚点,一条视频出片。<br>多段需 <code style="font-size:9px;">ffmpeg</code>:装好后加 PATH,或设环境变量 <code style="font-size:9px;">LTX_FFMPEG_PATH</code>,或在 <code style="font-size:9px;">%LOCALAPPDATA%\\LTXDesktop\\ffmpeg_path.txt</code> 第一行写 ffmpeg.exe 完整路径。',
|
| 184 |
-
bgmLabel: '成片配乐(可选,统一音轨)',
|
| 185 |
-
bgmUploadHint: '上传一条完整 BGM(生成完成后会替换整段成片的音轨)',
|
| 186 |
-
mainRender: '开始渲染',
|
| 187 |
-
waitingTask: '等待分配渲染任务...',
|
| 188 |
-
libHistory: '历史资产 / ASSETS',
|
| 189 |
-
libLog: '系统日志 / LOGS',
|
| 190 |
-
refresh: '刷新',
|
| 191 |
-
logReady: '> LTX-2 Studio Ready. Expecting commands...',
|
| 192 |
-
resizeHandleTitle: '拖动调整面板高度',
|
| 193 |
-
batchNeedTwo: '💡 请上传至少2张图片',
|
| 194 |
-
batchSegTitle: '视频片段设置(分段拼接)',
|
| 195 |
-
batchSegClip: '片段',
|
| 196 |
-
batchSegDuration: '时长',
|
| 197 |
-
batchSegSec: '秒',
|
| 198 |
-
batchSegPrompt: '片段提示词',
|
| 199 |
-
batchSegPromptPh: '此片段的提示词,如:跳舞、吃饭...',
|
| 200 |
-
batchKfPanelTitle: '单次多关键帧 · 时间轴',
|
| 201 |
-
batchTotalDur: '总时长',
|
| 202 |
-
batchTotalSec: '秒',
|
| 203 |
-
batchPanelHint:
|
| 204 |
-
'为每一张图设置独立持续时间:第 1 张从 0 s 开始,下一张在上一张持续结束后接上,最后一张也会延续自己的时长。因后端按<strong>整数秒</strong>建序列,实际请求里的整段时长为合计秒数<strong>向上取整</strong>(至少 2),略长于小数合计时属正常。镜头与 FPS 仍用左侧「视频生成」。',
|
| 205 |
-
batchKfTitle: '关键帧',
|
| 206 |
-
batchStrength: '引导强度',
|
| 207 |
-
batchFrameDuration: '持续时长',
|
| 208 |
-
batchFrameDurationTitle: '这一张图在时间轴上占用的时长(秒);总时长 = 每张图持续时长之和',
|
| 209 |
-
batchGapTitle: '间隔',
|
| 210 |
-
batchSec: '秒',
|
| 211 |
-
batchAnchorStart: '片头',
|
| 212 |
-
batchAnchorEnd: '片尾',
|
| 213 |
-
batchAnchorLast: '最后一帧开始',
|
| 214 |
-
batchThumbDrag: '按住拖动排序',
|
| 215 |
-
batchThumbRemove: '删除',
|
| 216 |
-
batchAddMore: '+ 继续添加',
|
| 217 |
-
batchGapInputTitle: '这一张图在时间轴上占用的时长(秒);总时长 = 每张图持续时长之和',
|
| 218 |
-
batchStrengthTitle: '与 Comfy guide strength 类似,中间帧可调低(如 0.2)减轻闪烁',
|
| 219 |
-
batchTotalPillTitle: '等于下方各「持续时长」之和,无需单独填写',
|
| 220 |
-
defaultPath: '默认路径',
|
| 221 |
-
phase_loading_model: '加载权重',
|
| 222 |
-
phase_encoding_text: 'T5 编码',
|
| 223 |
-
phase_validating_request: '校验请求',
|
| 224 |
-
phase_uploading_audio: '上传音频',
|
| 225 |
-
phase_uploading_image: '上传图像',
|
| 226 |
-
phase_inference: 'AI 推理',
|
| 227 |
-
phase_downloading_output: '下载结果',
|
| 228 |
-
phase_complete: '完成',
|
| 229 |
-
gpuBusyPrefix: 'GPU 运算中',
|
| 230 |
-
progressStepUnit: '步',
|
| 231 |
-
loaderGpuAlloc: 'GPU 正在分配资源...',
|
| 232 |
-
warnGenerating: '⚠️ 当前正在生成中,请等待完成',
|
| 233 |
-
warnBatchPrompt: '⚠️ 智能多帧请至少填写:顶部主提示词、本页全局补充词或某一「片段提示词」',
|
| 234 |
-
warnNeedPrompt: '⚠️ 请输入提示词后再开始渲染',
|
| 235 |
-
warnVideoLong: '⚠️ 时长设定为 {n}s 极长,可能导致显存溢出或耗时较久。',
|
| 236 |
-
errBatchMinImages: '请上传至少2张图片',
|
| 237 |
-
errSingleKfPrompt: '单次多关键帧请至少填写顶部主提示词或本页全局补充词',
|
| 238 |
-
loraNoneLabel: '无',
|
| 239 |
-
modelDefaultLabel: '默认',
|
| 240 |
-
tabTts: 'TTS 语音',
|
| 241 |
-
ttsStatusBarDetecting: '🔍 正在检测 TTS 模型...',
|
| 242 |
-
ttsTextTitle: '合成文本 / Text',
|
| 243 |
-
ttsTextHint: '支持在文本开头加英文括号描述声音,例如:<code style="font-size:10px;">(年轻女声,温柔甜美)</code>',
|
| 244 |
-
ttsTextPlaceholder: '输入要合成的文本内容...',
|
| 245 |
-
ttsModeTitle: '合成模式 / Mode',
|
| 246 |
-
ttsModeTextOnly: '🗣️ 文字转语音(含声音设计)',
|
| 247 |
-
ttsModeClone: '🎙️ 声音克隆',
|
| 248 |
-
ttsModeUltimate: '⭐ 终极克隆(最高还原度)',
|
| 249 |
-
ttsRefLabel: '📎 参考音频(Reference)',
|
| 250 |
-
ttsRefUploadHint: '点击上传参考音频 (.wav / .mp3)',
|
| 251 |
-
ttsUltimateLabel: '📝 参考音频对应的文本转录(可选)',
|
| 252 |
-
ttsUltimatePlaceholder: '与参考音频完全一致的文本内容...',
|
| 253 |
-
ttsParamsTitle: '高级参数 / Parameters',
|
| 254 |
-
ttsCfgLabel: 'CFG 强度',
|
| 255 |
-
ttsStepsLabel: '推理步数',
|
| 256 |
-
ttsResultTitle: '生成结果 / Output',
|
| 257 |
-
ttsDownload: '⬇️ 下载音频',
|
| 258 |
-
ttsGenBtn: '🎙️ 开始生成语音',
|
| 259 |
-
ttsGenBusy: '⏳ 生成中...',
|
| 260 |
-
ttsErrNoText: '❌ TTS: ��输入合成文本',
|
| 261 |
-
ttsErrNoRef: '❌ TTS: 声音克隆模式需要上传参考音频',
|
| 262 |
-
ttsStatusReady: '✅ VoxCPM2 就绪 — 模型目录: ',
|
| 263 |
-
ttsStatusNoPkq: '❌ voxcpm 包未安装,请在 LTX Python 环境中运行: pip install voxcpm',
|
| 264 |
-
ttsStatusNoDir: '❌ 请将 VoxCPM2 文件夹放到: ',
|
| 265 |
-
ttsStatusNotAvail: '⚠️ TTS 不可用,请检查配置',
|
| 266 |
-
ttsStatusConnErr: '❌ 无法连接后端 TTS 接口: ',
|
| 267 |
-
},
|
| 268 |
-
en: {
|
| 269 |
-
tabVideo: 'Video',
|
| 270 |
-
tabBatch: 'Frames',
|
| 271 |
-
tabMotion: 'Transfer',
|
| 272 |
-
tabImage: 'Image',
|
| 273 |
-
promptLabel: 'Prompt',
|
| 274 |
-
promptPlaceholder: 'Describe the scene...',
|
| 275 |
-
seedLabel: 'Seed',
|
| 276 |
-
seedRandom: 'Random',
|
| 277 |
-
seedFixed: 'Fixed',
|
| 278 |
-
clearVram: 'Clear VRAM',
|
| 279 |
-
clearingVram: 'Clearing...',
|
| 280 |
-
settingsTitle: 'Advanced settings',
|
| 281 |
-
langToggleAriaZh: 'Switch to English',
|
| 282 |
-
langToggleAriaEn: 'Switch to Chinese',
|
| 283 |
-
sysScanning: 'Scanning GPU...',
|
| 284 |
-
sysBusy: 'Busy...',
|
| 285 |
-
sysOnline: 'Online / Ready',
|
| 286 |
-
sysStarting: 'Starting...',
|
| 287 |
-
sysOffline: 'Backend offline (port 3000)',
|
| 288 |
-
advancedSettings: 'Advanced',
|
| 289 |
-
deviceSelect: 'GPU device',
|
| 290 |
-
gpuDetecting: 'Detecting GPU...',
|
| 291 |
-
outputPath: 'Output & upload folder',
|
| 292 |
-
outputPathPh: 'e.g. D:\\LTX_outputs',
|
| 293 |
-
savePath: 'Save path',
|
| 294 |
-
outputPathHint:
|
| 295 |
-
'Outputs default to C: drive. Enter a folder and click Save.',
|
| 296 |
-
lowVram: 'Low-VRAM mode',
|
| 297 |
-
lowVramDesc:
|
| 298 |
-
'Tries to reduce VRAM (engine-dependent). Shorter duration / lower resolution helps more.',
|
| 299 |
-
vramLimitLabel: 'Max VRAM Limit (GB, 0 for unlimited)',
|
| 300 |
-
vramLimitPh: 'e.g. 12 (0 for unlimited)',
|
| 301 |
-
saveLabel: 'Save',
|
| 302 |
-
modelCheckpointLabel: 'Video model (distilled)',
|
| 303 |
-
modelCheckpointDefault: 'Default official distilled model',
|
| 304 |
-
modelCheckpointHint: 'Recommended: distilled-fp8. Only LTX 2.3 22B distilled models are shown; dev models are hidden.',
|
| 305 |
-
modelCheckpointSaved: 'Model selected',
|
| 306 |
-
modelCheckpointNone: 'No switchable distilled models found',
|
| 307 |
-
modelCheckpointLoadFail: 'Failed to load model list',
|
| 308 |
-
modelLoraSettings: 'Model & LoRA folders',
|
| 309 |
-
modelFolder: 'Models folder',
|
| 310 |
-
modelFolderPh: 'Current LTX models directory',
|
| 311 |
-
loraFolder: 'LoRAs folder',
|
| 312 |
-
loraFolderPh: 'models\\loras',
|
| 313 |
-
loraFolderPath: 'LoRA folder path (optional)',
|
| 314 |
-
loraFolderPathPlaceholder: 'Empty = models\\loras',
|
| 315 |
-
saveScan: 'Save & scan',
|
| 316 |
-
loraHint: 'Put .safetensors / .ckpt LoRAs here, then refresh lists.',
|
| 317 |
-
basicEngine: 'Basic / Engine',
|
| 318 |
-
qualityLevel: 'Quality',
|
| 319 |
-
aspectRatio: 'Aspect ratio',
|
| 320 |
-
ratio169: '16:9 widescreen',
|
| 321 |
-
ratio916: '9:16 portrait',
|
| 322 |
-
ratio11: '1:1 square',
|
| 323 |
-
ratio43: '4:3 classic',
|
| 324 |
-
ratio34: '3:4 portrait',
|
| 325 |
-
ratio219: '21:9 ultrawide',
|
| 326 |
-
ratio921: '9:21 tall',
|
| 327 |
-
ratioRef: 'Match reference',
|
| 328 |
-
ratioCustom: 'Custom size',
|
| 329 |
-
ratioRefMissing: 'upload a reference first',
|
| 330 |
-
resPreviewPrefix: 'Output',
|
| 331 |
-
fpsLabel: 'FPS',
|
| 332 |
-
durationLabel: 'Duration (s)',
|
| 333 |
-
cameraMotion: 'Camera motion',
|
| 334 |
-
motionStatic: 'Static',
|
| 335 |
-
motionDollyIn: 'Dolly in',
|
| 336 |
-
motionDollyOut: 'Dolly out',
|
| 337 |
-
motionDollyLeft: 'Dolly left',
|
| 338 |
-
motionDollyRight: 'Dolly right',
|
| 339 |
-
motionJibUp: 'Jib up',
|
| 340 |
-
motionJibDown: 'Jib down',
|
| 341 |
-
motionFocus: 'Focus shift',
|
| 342 |
-
audioGen: 'AI ambient audio',
|
| 343 |
-
selectModel: 'Model',
|
| 344 |
-
selectLora: 'LoRA',
|
| 345 |
-
defaultModel: 'Default model',
|
| 346 |
-
noLora: 'No LoRA',
|
| 347 |
-
loraStrength: 'LoRA strength',
|
| 348 |
-
genSource: 'Source media',
|
| 349 |
-
startFrame: 'Start frame',
|
| 350 |
-
endFrame: 'End frame (optional)',
|
| 351 |
-
uploadStart: 'Upload start',
|
| 352 |
-
uploadEnd: 'Upload end (opt.)',
|
| 353 |
-
refAudio: 'Reference audio (A2V)',
|
| 354 |
-
uploadAudio: 'Upload audio',
|
| 355 |
-
sourceHint:
|
| 356 |
-
'💡 Start only = I2V / A2V; start + end = interpolation.',
|
| 357 |
-
motionTransferTitle: 'Video Transfer',
|
| 358 |
-
motionRefVideoLabel: 'Reference video',
|
| 359 |
-
motionVideoUploadText: 'Click or drop a video',
|
| 360 |
-
motionVideoUploadHint: 'Used for action or camera transfer',
|
| 361 |
-
motionTargetImageLabel: 'Target subject image',
|
| 362 |
-
motionImageUploadText: 'Click or drop an image',
|
| 363 |
-
motionImageUploadHint: 'Guides subject identity / first frame',
|
| 364 |
-
motionTransferModeLabel: 'Transfer type',
|
| 365 |
-
motionModeAction: 'Action transfer',
|
| 366 |
-
motionModeCamera: 'Camera transfer',
|
| 367 |
-
motionModeRepaint: 'Video repaint',
|
| 368 |
-
motionControlType: 'Control type',
|
| 369 |
-
motionControlCanny: 'Canny contour',
|
| 370 |
-
motionControlDepth: 'Depth',
|
| 371 |
-
motionControlPose: 'Pose',
|
| 372 |
-
motionControlStrength: 'Control strength',
|
| 373 |
-
motionTransferHint:
|
| 374 |
-
'Action transfer uses Pose control; camera transfer uses the raw reference video as an IC-LoRA guide.',
|
| 375 |
-
motionRefVideoName: 'reference video',
|
| 376 |
-
motionTargetImageName: 'target subject image',
|
| 377 |
-
motionUploadOk: '✅ {label} uploaded: {name}',
|
| 378 |
-
motionUploadFail: '❌ {label} upload failed: {message}',
|
| 379 |
-
motionClearRefVideo: '🧹 Reference video cleared',
|
| 380 |
-
motionClearTargetImage: '🧹 Target subject image cleared',
|
| 381 |
-
motionErrNeedVideo: 'Upload a reference video first',
|
| 382 |
-
motionErrNeedImage: 'Upload a target subject image first',
|
| 383 |
-
motionDefaultPromptNotice: 'No prompt entered for video transfer; using the default video transfer prompt.',
|
| 384 |
-
motionStartLog: 'Starting video transfer: {type}, strength {strength}',
|
| 385 |
-
motionStartMeta: 'FPS {fps}, duration {duration}s',
|
| 386 |
-
uploadFileStart: 'Uploading {label}: {name}...',
|
| 387 |
-
fileReadFail: 'Failed to read local file',
|
| 388 |
-
downloadLabel: 'Download',
|
| 389 |
-
queueTitle: 'Task Queue',
|
| 390 |
-
queueIdle: 'Idle',
|
| 391 |
-
queueQueued: 'Queued',
|
| 392 |
-
queueRunning: 'Running',
|
| 393 |
-
queueComplete: 'Done',
|
| 394 |
-
queueError: 'Error',
|
| 395 |
-
queueCancelled: 'Cancelled',
|
| 396 |
-
queueWaiting: '{n} waiting',
|
| 397 |
-
queueRunningSummary: 'Running 1 / Queued {n}',
|
| 398 |
-
queueNoTasks: 'No tasks',
|
| 399 |
-
queueViewResult: 'View result',
|
| 400 |
-
queuePosition: 'Queue #{n}',
|
| 401 |
-
queueTaskTypeVideo: 'Video',
|
| 402 |
-
queueTaskTypeMotion: 'Transfer',
|
| 403 |
-
queueTaskTypeBatch: 'Batch',
|
| 404 |
-
queueTaskTypeImage: 'Image',
|
| 405 |
-
queueSubmitLog: '📥 Added to queue: {id} ({n} ahead)',
|
| 406 |
-
queueDoneLog: '✅ Queue task complete: {label}',
|
| 407 |
-
queueFailLog: '❌ Queue task failed: {label} - {error}',
|
| 408 |
-
queueCancelLog: '🛑 Queue task cancelled: {label}',
|
| 409 |
-
replayRun: 'Replay',
|
| 410 |
-
replayLoad: 'Load',
|
| 411 |
-
replayLabel: 'Replay',
|
| 412 |
-
replayMissing: '⚠️ This item has no replay payload',
|
| 413 |
-
replayQueuedLog: '↻ Replay added to queue: {id}',
|
| 414 |
-
replayLoadedLog: '↗ Replay settings loaded; adjust and render again',
|
| 415 |
-
replayFailed: 'Replay failed',
|
| 416 |
-
previewLoadSeed: 'Load Seed',
|
| 417 |
-
previewLoadParams: 'Load Params',
|
| 418 |
-
previewNoReplaySeed: '⚠️ Current preview has no seed to load',
|
| 419 |
-
previewNoReplayParams: '⚠️ Current preview has no params to load',
|
| 420 |
-
previewSeedLoadedLog: 'Loaded seed {seed} and switched to fixed seed',
|
| 421 |
-
previewNoDownload: '❌ No downloadable preview right now',
|
| 422 |
-
imgPreset: 'Resolution presets',
|
| 423 |
-
imgOptSquare: '1:1 (1024×1024)',
|
| 424 |
-
imgOptLand: '16:9 (1280×720)',
|
| 425 |
-
imgOptPort: '9:16 (720×1280)',
|
| 426 |
-
imgOptCustom: 'Custom...',
|
| 427 |
-
width: 'Width',
|
| 428 |
-
height: 'Height',
|
| 429 |
-
samplingSteps: 'Steps',
|
| 430 |
-
smartMultiFrameGroup: 'Smart multi-frame',
|
| 431 |
-
workflowModeLabel: 'Workflow',
|
| 432 |
-
wfSingle: 'Single pass',
|
| 433 |
-
wfSegments: 'Segments',
|
| 434 |
-
uploadImages: 'Upload images',
|
| 435 |
-
uploadMulti1: 'Click or drop multiple images',
|
| 436 |
-
uploadMulti2: 'Multi-select OK; add more anytime.',
|
| 437 |
-
batchStripTitle: 'Order = playback',
|
| 438 |
-
batchStripHint: 'Drag thumbnails to reorder.',
|
| 439 |
-
batchFfmpegHint:
|
| 440 |
-
'💡 <strong>Segments</strong>: 2 images → 1 clip; 3 → 2 clips stitched. <strong>Single</strong>: N images → N latent anchors, one video.<br>Stitching needs <code style="font-size:9px;">ffmpeg</code> on PATH, or <code style="font-size:9px;">LTX_FFMPEG_PATH</code>, or <code style="font-size:9px;">%LOCALAPPDATA%\\LTXDesktop\\ffmpeg_path.txt</code> with full path to ffmpeg.exe.',
|
| 441 |
-
bgmLabel: 'Full-length BGM (optional)',
|
| 442 |
-
bgmUploadHint: 'Replaces final mix audio after generation.',
|
| 443 |
-
mainRender: 'Render',
|
| 444 |
-
waitingTask: 'Waiting for task...',
|
| 445 |
-
libHistory: 'Assets',
|
| 446 |
-
libLog: 'Logs',
|
| 447 |
-
refresh: 'Refresh',
|
| 448 |
-
logReady: '> LTX-2 Studio ready.',
|
| 449 |
-
resizeHandleTitle: 'Drag to resize panel',
|
| 450 |
-
batchNeedTwo: '💡 Upload at least 2 images',
|
| 451 |
-
batchSegTitle: 'Segment settings',
|
| 452 |
-
batchSegClip: 'Clip',
|
| 453 |
-
batchSegDuration: 'Duration',
|
| 454 |
-
batchSegSec: 's',
|
| 455 |
-
batchSegPrompt: 'Prompt',
|
| 456 |
-
batchSegPromptPh: 'e.g. dancing, walking...',
|
| 457 |
-
batchKfPanelTitle: 'Single pass · timeline',
|
| 458 |
-
batchTotalDur: 'Total',
|
| 459 |
-
batchTotalSec: 's',
|
| 460 |
-
batchPanelHint:
|
| 461 |
-
'Set an independent duration for each image: the first starts at 0s, each next image starts after the previous duration, and the last image also keeps its own duration. Backend uses whole seconds (ceil, min 2). Motion & FPS use the Video panel.',
|
| 462 |
-
batchKfTitle: 'Keyframe',
|
| 463 |
-
batchStrength: 'Strength',
|
| 464 |
-
batchFrameDuration: 'Duration',
|
| 465 |
-
batchFrameDurationTitle: 'How long this image occupies on the timeline; total = sum of all image durations',
|
| 466 |
-
batchGapTitle: 'Gap',
|
| 467 |
-
batchSec: 's',
|
| 468 |
-
batchAnchorStart: 'start',
|
| 469 |
-
batchAnchorEnd: 'end',
|
| 470 |
-
batchAnchorLast: 'last starts',
|
| 471 |
-
batchThumbDrag: 'Drag to reorder',
|
| 472 |
-
batchThumbRemove: 'Remove',
|
| 473 |
-
batchAddMore: '+ Add more',
|
| 474 |
-
batchGapInputTitle: 'How long this image occupies on the timeline; total = sum of all image durations',
|
| 475 |
-
batchStrengthTitle: 'Guide strength (lower on middle keys may reduce flicker)',
|
| 476 |
-
batchTotalPillTitle: 'Equals the sum of durations below',
|
| 477 |
-
defaultPath: 'default',
|
| 478 |
-
phase_loading_model: 'Loading weights',
|
| 479 |
-
phase_encoding_text: 'T5 encode',
|
| 480 |
-
phase_validating_request: 'Validating',
|
| 481 |
-
phase_uploading_audio: 'Uploading audio',
|
| 482 |
-
phase_uploading_image: 'Uploading image',
|
| 483 |
-
phase_inference: 'Inference',
|
| 484 |
-
phase_downloading_output: 'Downloading',
|
| 485 |
-
phase_complete: 'Done',
|
| 486 |
-
gpuBusyPrefix: 'GPU',
|
| 487 |
-
progressStepUnit: 'steps',
|
| 488 |
-
loaderGpuAlloc: 'Allocating GPU...',
|
| 489 |
-
warnGenerating: '⚠️ Already generating, please wait.',
|
| 490 |
-
warnBatchPrompt: '⚠️ Enter main prompt, page extra prompt, or a segment prompt.',
|
| 491 |
-
warnNeedPrompt: '⚠️ Enter a prompt first.',
|
| 492 |
-
warnVideoLong: '⚠️ Duration {n}s is very long; may OOM or take a long time.',
|
| 493 |
-
errBatchMinImages: 'Upload at least 2 images.',
|
| 494 |
-
errSingleKfNeedPrompt: 'Enter main or page extra prompt for single-pass keyframes.',
|
| 495 |
-
loraNoneLabel: 'none',
|
| 496 |
-
modelDefaultLabel: 'default',
|
| 497 |
-
loraPlacementHintWithDir:
|
| 498 |
-
'Place LoRAs into the current models directory: <code>{dir}</code>\\loras',
|
| 499 |
-
loraPlacementHint: 'Place LoRAs in the <code>loras</code> folder under the current models directory.',
|
| 500 |
-
tabTts: 'TTS',
|
| 501 |
-
ttsStatusBarDetecting: '🔍 Detecting TTS model...',
|
| 502 |
-
ttsTextTitle: 'Synthesis Text',
|
| 503 |
-
ttsTextHint: 'Supports descriptors in brackets at start, e.g. <code style="font-size:10px;">(Young female, soft and sweet)</code>',
|
| 504 |
-
ttsTextPlaceholder: 'Enter text to synthesize...',
|
| 505 |
-
ttsModeTitle: 'Synthesis Mode',
|
| 506 |
-
ttsModeTextOnly: '🗣️ Text to Speech (with sound design)',
|
| 507 |
-
ttsModeClone: '🎙️ Voice Cloning',
|
| 508 |
-
ttsModeUltimate: '⭐ Ultimate Clone (Max similarity)',
|
| 509 |
-
ttsRefLabel: '📎 Reference Audio',
|
| 510 |
-
ttsRefUploadHint: 'Click to upload reference (.wav / .mp3)',
|
| 511 |
-
ttsUltimateLabel: '📝 Audio Transcript (Optional)',
|
| 512 |
-
ttsUltimatePlaceholder: 'Text content matches reference audio exactly...',
|
| 513 |
-
ttsParamsTitle: 'Parameters',
|
| 514 |
-
ttsCfgLabel: 'CFG Strength',
|
| 515 |
-
ttsStepsLabel: 'Inference Steps',
|
| 516 |
-
ttsResultTitle: 'Output',
|
| 517 |
-
ttsDownload: '⬇️ Download Audio',
|
| 518 |
-
ttsGenBtn: '🎙️ Start Synthesis',
|
| 519 |
-
ttsGenBusy: '⏳ Generating...',
|
| 520 |
-
ttsErrNoText: '❌ TTS: Please enter synthesis text',
|
| 521 |
-
ttsErrNoRef: '❌ TTS: Voice cloning requires reference audio',
|
| 522 |
-
ttsStatusReady: '✅ VoxCPM2 Ready — Model dir: ',
|
| 523 |
-
ttsStatusNoPkq: '❌ voxcpm package not installed. Run: pip install voxcpm',
|
| 524 |
-
ttsStatusNoDir: '❌ Put the VoxCPM2 folder at: ',
|
| 525 |
-
ttsStatusNotAvail: '⚠️ TTS unavailable, check config',
|
| 526 |
-
ttsStatusConnErr: '❌ Cannot connect to TTS API: ',
|
| 527 |
-
},
|
| 528 |
-
};
|
| 529 |
-
|
| 530 |
-
function getLang() {
|
| 531 |
-
return localStorage.getItem(STORAGE_KEY) === 'en' ? 'en' : 'zh';
|
| 532 |
-
}
|
| 533 |
-
|
| 534 |
-
function setLang(lang) {
|
| 535 |
-
const L = lang === 'en' ? 'en' : 'zh';
|
| 536 |
-
localStorage.setItem(STORAGE_KEY, L);
|
| 537 |
-
document.documentElement.lang = L === 'en' ? 'en' : 'zh-CN';
|
| 538 |
-
try {
|
| 539 |
-
applyI18n();
|
| 540 |
-
} catch (err) {
|
| 541 |
-
console.error('[i18n] applyI18n failed:', err);
|
| 542 |
-
}
|
| 543 |
-
updateLangButton();
|
| 544 |
-
if (typeof global.onUiLanguageChanged === 'function') {
|
| 545 |
-
try {
|
| 546 |
-
global.onUiLanguageChanged();
|
| 547 |
-
} catch (e) {
|
| 548 |
-
console.warn('onUiLanguageChanged', e);
|
| 549 |
-
}
|
| 550 |
-
}
|
| 551 |
-
}
|
| 552 |
-
|
| 553 |
-
function t(key) {
|
| 554 |
-
const L = getLang();
|
| 555 |
-
const table = STR[L] || STR.zh;
|
| 556 |
-
if (Object.prototype.hasOwnProperty.call(table, key)) return table[key];
|
| 557 |
-
if (Object.prototype.hasOwnProperty.call(STR.zh, key)) return STR.zh[key];
|
| 558 |
-
return key;
|
| 559 |
-
}
|
| 560 |
-
|
| 561 |
-
function applyI18n(root) {
|
| 562 |
-
root = root || document;
|
| 563 |
-
root.querySelectorAll('[data-i18n]').forEach(function (el) {
|
| 564 |
-
var key = el.getAttribute('data-i18n');
|
| 565 |
-
if (!key) return;
|
| 566 |
-
if (el.tagName === 'OPTION') {
|
| 567 |
-
el.textContent = t(key);
|
| 568 |
-
} else {
|
| 569 |
-
el.textContent = t(key);
|
| 570 |
-
}
|
| 571 |
-
});
|
| 572 |
-
root.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) {
|
| 573 |
-
var key = el.getAttribute('data-i18n-placeholder');
|
| 574 |
-
if (key) el.placeholder = t(key);
|
| 575 |
-
});
|
| 576 |
-
root.querySelectorAll('[data-i18n-title]').forEach(function (el) {
|
| 577 |
-
var key = el.getAttribute('data-i18n-title');
|
| 578 |
-
if (key) el.title = t(key);
|
| 579 |
-
});
|
| 580 |
-
root.querySelectorAll('[data-i18n-html]').forEach(function (el) {
|
| 581 |
-
var key = el.getAttribute('data-i18n-html');
|
| 582 |
-
if (key) el.innerHTML = t(key);
|
| 583 |
-
});
|
| 584 |
-
root.querySelectorAll('[data-i18n-value]').forEach(function (el) {
|
| 585 |
-
var key = el.getAttribute('data-i18n-value');
|
| 586 |
-
if (key && (el.tagName === 'INPUT' || el.tagName === 'BUTTON')) {
|
| 587 |
-
el.value = t(key);
|
| 588 |
-
}
|
| 589 |
-
});
|
| 590 |
-
}
|
| 591 |
-
|
| 592 |
-
function updateLangButton() {
|
| 593 |
-
var btn = document.getElementById('lang-toggle-btn');
|
| 594 |
-
if (!btn) return;
|
| 595 |
-
btn.textContent = getLang() === 'zh' ? 'EN' : '中';
|
| 596 |
-
btn.setAttribute(
|
| 597 |
-
'aria-label',
|
| 598 |
-
getLang() === 'zh' ? t('langToggleAriaZh') : t('langToggleAriaEn')
|
| 599 |
-
);
|
| 600 |
-
btn.classList.toggle('active', getLang() === 'en');
|
| 601 |
-
}
|
| 602 |
-
|
| 603 |
-
function toggleUiLanguage() {
|
| 604 |
-
try {
|
| 605 |
-
setLang(getLang() === 'zh' ? 'en' : 'zh');
|
| 606 |
-
} catch (err) {
|
| 607 |
-
console.error('[i18n] toggleUiLanguage failed:', err);
|
| 608 |
-
}
|
| 609 |
-
}
|
| 610 |
-
|
| 611 |
-
/** 避免 CSP 拦截内联 onclick;确保按钮一定能触发 */
|
| 612 |
-
function bindLangToggleButton() {
|
| 613 |
-
var btn = document.getElementById('lang-toggle-btn');
|
| 614 |
-
if (!btn || btn.dataset.i18nBound === '1') return;
|
| 615 |
-
btn.dataset.i18nBound = '1';
|
| 616 |
-
btn.removeAttribute('onclick');
|
| 617 |
-
btn.addEventListener('click', function (ev) {
|
| 618 |
-
ev.preventDefault();
|
| 619 |
-
toggleUiLanguage();
|
| 620 |
-
});
|
| 621 |
-
}
|
| 622 |
-
|
| 623 |
-
function boot() {
|
| 624 |
-
document.documentElement.lang = getLang() === 'en' ? 'en' : 'zh-CN';
|
| 625 |
-
try {
|
| 626 |
-
applyI18n();
|
| 627 |
-
} catch (err) {
|
| 628 |
-
console.error('[i18n] applyI18n failed:', err);
|
| 629 |
-
}
|
| 630 |
-
updateLangButton();
|
| 631 |
-
bindLangToggleButton();
|
| 632 |
-
}
|
| 633 |
-
|
| 634 |
-
global.getUiLang = getLang;
|
| 635 |
-
global.setUiLang = setLang;
|
| 636 |
-
global.t = t;
|
| 637 |
-
global.applyI18n = applyI18n;
|
| 638 |
-
global.toggleUiLanguage = toggleUiLanguage;
|
| 639 |
-
global.updateLangToggleButton = updateLangButton;
|
| 640 |
-
|
| 641 |
-
if (document.readyState === 'loading') {
|
| 642 |
-
document.addEventListener('DOMContentLoaded', boot);
|
| 643 |
-
} else {
|
| 644 |
-
boot();
|
| 645 |
-
}
|
| 646 |
-
})(typeof window !== 'undefined' ? window : global);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/UI/index.css
DELETED
|
@@ -1,982 +0,0 @@
|
|
| 1 |
-
:root {
|
| 2 |
-
--accent: #2563EB; /* Refined blue – not too bright, not purple */
|
| 3 |
-
--accent-hover:#3B82F6;
|
| 4 |
-
--accent-dim: rgba(37,99,235,0.14);
|
| 5 |
-
--accent-ring: rgba(37,99,235,0.35);
|
| 6 |
-
--bg: #111113;
|
| 7 |
-
--panel: #18181B;
|
| 8 |
-
--panel-2: #1F1F23;
|
| 9 |
-
--item: rgba(255,255,255,0.035);
|
| 10 |
-
--border: rgba(255,255,255,0.08);
|
| 11 |
-
--border-2: rgba(255,255,255,0.05);
|
| 12 |
-
--text-dim: #71717A;
|
| 13 |
-
--text-sub: #A1A1AA;
|
| 14 |
-
--text: #FAFAFA;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
* { box-sizing: border-box; -webkit-font-smoothing: antialiased; min-width: 0; }
|
| 18 |
-
body {
|
| 19 |
-
background: var(--bg); margin: 0; color: var(--text);
|
| 20 |
-
font-family: -apple-system, "SF Pro Display", "Segoe UI", sans-serif;
|
| 21 |
-
display: flex; height: 100vh; overflow: hidden;
|
| 22 |
-
font-size: 13px; line-height: 1.5;
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
.sidebar {
|
| 26 |
-
width: 460px; min-width: 460px;
|
| 27 |
-
background: var(--panel);
|
| 28 |
-
border-right: 1px solid var(--border);
|
| 29 |
-
display: flex; flex-direction: column; z-index: 20;
|
| 30 |
-
overflow-y: auto; overflow-x: hidden;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
/* Scrollbar */
|
| 34 |
-
::-webkit-scrollbar { width: 5px; height: 5px; }
|
| 35 |
-
::-webkit-scrollbar-track { background: transparent; }
|
| 36 |
-
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 10px; }
|
| 37 |
-
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.18); }
|
| 38 |
-
|
| 39 |
-
.sidebar-header { padding: 24px 24px 4px; }
|
| 40 |
-
|
| 41 |
-
.lang-toggle {
|
| 42 |
-
background: #333;
|
| 43 |
-
border: 1px solid #555;
|
| 44 |
-
color: var(--text-dim);
|
| 45 |
-
padding: 4px 10px;
|
| 46 |
-
border-radius: 6px;
|
| 47 |
-
font-size: 11px;
|
| 48 |
-
cursor: pointer;
|
| 49 |
-
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
| 50 |
-
font-weight: 700;
|
| 51 |
-
min-width: 44px;
|
| 52 |
-
flex-shrink: 0;
|
| 53 |
-
}
|
| 54 |
-
.lang-toggle:hover {
|
| 55 |
-
background: var(--item);
|
| 56 |
-
color: var(--text);
|
| 57 |
-
border-color: var(--accent);
|
| 58 |
-
}
|
| 59 |
-
.lang-toggle.active {
|
| 60 |
-
background: #333;
|
| 61 |
-
color: var(--text);
|
| 62 |
-
border-color: #555;
|
| 63 |
-
}
|
| 64 |
-
.sidebar-section { padding: 8px 24px 18px; border-bottom: 1px solid var(--border); }
|
| 65 |
-
|
| 66 |
-
.setting-group {
|
| 67 |
-
background: rgba(255,255,255,0.025);
|
| 68 |
-
border: 1px solid var(--border-2);
|
| 69 |
-
border-radius: 10px;
|
| 70 |
-
padding: 14px;
|
| 71 |
-
margin-bottom: 12px;
|
| 72 |
-
}
|
| 73 |
-
.group-title {
|
| 74 |
-
font-size: 10px; color: var(--text-dim); font-weight: 700;
|
| 75 |
-
text-transform: uppercase; letter-spacing: 0.7px;
|
| 76 |
-
margin-bottom: 12px; padding-bottom: 5px;
|
| 77 |
-
border-bottom: 1px solid var(--border-2);
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
/* Mode Tabs */
|
| 81 |
-
.tabs {
|
| 82 |
-
display: flex; gap: 4px; margin-bottom: 14px;
|
| 83 |
-
background: rgba(255,255,255,0.04);
|
| 84 |
-
padding: 4px; border-radius: 10px;
|
| 85 |
-
border: 1px solid var(--border-2);
|
| 86 |
-
}
|
| 87 |
-
.tab {
|
| 88 |
-
flex: 1; padding: 9px 0; text-align: center; border-radius: 7px;
|
| 89 |
-
cursor: pointer; font-size: 12px; color: var(--text-dim);
|
| 90 |
-
transition: all 0.2s; font-weight: 600;
|
| 91 |
-
display: flex; align-items: center; justify-content: center;
|
| 92 |
-
}
|
| 93 |
-
.tab.active { background: var(--accent); color: #fff; box-shadow: 0 1px 6px rgba(10,132,255,0.45); }
|
| 94 |
-
.tab:hover:not(.active) { background: rgba(255,255,255,0.06); color: var(--text); }
|
| 95 |
-
html[lang="en"] .tabs { gap: 3px; padding: 3px; }
|
| 96 |
-
html[lang="en"] .tab { font-size: 11px; padding: 8px 0; font-weight: 700; }
|
| 97 |
-
html[lang="en"] .tab svg { width: 13px; height: 13px; margin-right: 4px !important; flex-shrink: 0; }
|
| 98 |
-
|
| 99 |
-
.label-group { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
| 100 |
-
label { display: block; font-size: 11px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
| 101 |
-
.val-badge { font-size: 11px; color: var(--accent); font-family: "SF Mono", ui-monospace, monospace; font-weight: 600; }
|
| 102 |
-
|
| 103 |
-
input[type="text"], input[type="number"], select, textarea {
|
| 104 |
-
width: 100%; background: var(--panel-2);
|
| 105 |
-
border: 1px solid var(--border);
|
| 106 |
-
border-radius: 7px; color: var(--text);
|
| 107 |
-
padding: 8px 11px; font-size: 12.5px; outline: none; margin-bottom: 9px;
|
| 108 |
-
/* Only transition border/shadow – NOT background-image to prevent arrow flicker */
|
| 109 |
-
transition: border-color 0.15s, box-shadow 0.15s;
|
| 110 |
-
}
|
| 111 |
-
input:focus, select:focus, textarea:focus {
|
| 112 |
-
border-color: var(--accent);
|
| 113 |
-
box-shadow: 0 0 0 2px var(--accent-ring);
|
| 114 |
-
}
|
| 115 |
-
select {
|
| 116 |
-
-webkit-appearance: none; -moz-appearance: none; appearance: none;
|
| 117 |
-
/* Stable grey arrow – no background shorthand so it won't animate */
|
| 118 |
-
background-color: var(--panel-2);
|
| 119 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2371717A' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 120 |
-
background-repeat: no-repeat;
|
| 121 |
-
background-position: right 10px center;
|
| 122 |
-
background-size: 12px;
|
| 123 |
-
padding-right: 28px;
|
| 124 |
-
cursor: pointer;
|
| 125 |
-
/* Explicitly do NOT transition background properties */
|
| 126 |
-
transition: border-color 0.15s, box-shadow 0.15s;
|
| 127 |
-
}
|
| 128 |
-
select:focus { background-color: var(--panel-2); }
|
| 129 |
-
select option { background: #27272A; color: var(--text); }
|
| 130 |
-
textarea { resize: vertical; min-height: 78px; font-family: inherit; }
|
| 131 |
-
|
| 132 |
-
.slider-container { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
|
| 133 |
-
input[type="range"] { flex: 1; accent-color: var(--accent); height: 4px; cursor: pointer; border-radius: 2px; }
|
| 134 |
-
|
| 135 |
-
.upload-zone {
|
| 136 |
-
border: 1px dashed var(--border); border-radius: 10px;
|
| 137 |
-
padding: 18px 10px; text-align: center; cursor: pointer;
|
| 138 |
-
background: rgba(255,255,255,0.03); margin-bottom: 10px; position: relative;
|
| 139 |
-
transition: all 0.2s;
|
| 140 |
-
}
|
| 141 |
-
.upload-zone:hover, .upload-zone.dragover { background: var(--accent-dim); border-color: var(--accent); }
|
| 142 |
-
.upload-zone.has-images {
|
| 143 |
-
padding: 12px; background: rgba(255,255,255,0.025);
|
| 144 |
-
}
|
| 145 |
-
.upload-zone.has-images .upload-placeholder-mini {
|
| 146 |
-
display: flex; align-items: center; gap: 8px; justify-content: center;
|
| 147 |
-
color: var(--text-dim); font-size: 11px;
|
| 148 |
-
}
|
| 149 |
-
.upload-zone.has-images .upload-placeholder-mini span {
|
| 150 |
-
background: var(--item); padding: 6px 12px; border-radius: 6px;
|
| 151 |
-
}
|
| 152 |
-
#batch-images-placeholder { display: block; }
|
| 153 |
-
.upload-zone.has-images #batch-images-placeholder { display: none; }
|
| 154 |
-
|
| 155 |
-
/* 批量模式:上传区下方的横向缩略图条 */
|
| 156 |
-
.batch-thumb-strip-wrap {
|
| 157 |
-
margin-top: 10px;
|
| 158 |
-
margin-bottom: 4px;
|
| 159 |
-
}
|
| 160 |
-
.batch-thumb-strip-head {
|
| 161 |
-
display: flex;
|
| 162 |
-
flex-direction: column;
|
| 163 |
-
gap: 2px;
|
| 164 |
-
margin-bottom: 8px;
|
| 165 |
-
}
|
| 166 |
-
.batch-thumb-strip-title {
|
| 167 |
-
font-size: 11px;
|
| 168 |
-
font-weight: 700;
|
| 169 |
-
color: var(--text-sub);
|
| 170 |
-
}
|
| 171 |
-
.batch-thumb-strip-hint {
|
| 172 |
-
font-size: 10px;
|
| 173 |
-
color: var(--text-dim);
|
| 174 |
-
}
|
| 175 |
-
.batch-images-container {
|
| 176 |
-
display: flex;
|
| 177 |
-
flex-direction: row;
|
| 178 |
-
flex-wrap: nowrap;
|
| 179 |
-
gap: 10px;
|
| 180 |
-
overflow-x: auto;
|
| 181 |
-
overflow-y: visible;
|
| 182 |
-
padding: 6px 4px 14px;
|
| 183 |
-
margin: 0 -4px;
|
| 184 |
-
scrollbar-width: thin;
|
| 185 |
-
scrollbar-color: var(--border) transparent;
|
| 186 |
-
align-items: center;
|
| 187 |
-
}
|
| 188 |
-
.batch-images-container::-webkit-scrollbar { height: 6px; }
|
| 189 |
-
.batch-images-container::-webkit-scrollbar-thumb {
|
| 190 |
-
background: var(--border);
|
| 191 |
-
border-radius: 3px;
|
| 192 |
-
}
|
| 193 |
-
.batch-image-wrapper {
|
| 194 |
-
flex: 0 0 72px;
|
| 195 |
-
width: 72px;
|
| 196 |
-
height: 72px;
|
| 197 |
-
position: relative;
|
| 198 |
-
border-radius: 10px;
|
| 199 |
-
overflow: hidden;
|
| 200 |
-
background: var(--item);
|
| 201 |
-
border: 1px solid var(--border);
|
| 202 |
-
cursor: grab;
|
| 203 |
-
touch-action: none;
|
| 204 |
-
user-select: none;
|
| 205 |
-
-webkit-user-select: none;
|
| 206 |
-
transition:
|
| 207 |
-
flex-basis 0.38s cubic-bezier(0.22, 1, 0.36, 1),
|
| 208 |
-
width 0.38s cubic-bezier(0.22, 1, 0.36, 1),
|
| 209 |
-
min-width 0.38s cubic-bezier(0.22, 1, 0.36, 1),
|
| 210 |
-
margin 0.38s cubic-bezier(0.22, 1, 0.36, 1),
|
| 211 |
-
opacity 0.25s ease,
|
| 212 |
-
border-color 0.2s ease,
|
| 213 |
-
box-shadow 0.2s ease,
|
| 214 |
-
transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
| 215 |
-
}
|
| 216 |
-
.batch-image-wrapper:active { cursor: grabbing; }
|
| 217 |
-
.batch-image-wrapper.batch-thumb--source {
|
| 218 |
-
flex: 0 0 0;
|
| 219 |
-
width: 0;
|
| 220 |
-
min-width: 0;
|
| 221 |
-
height: 72px;
|
| 222 |
-
margin: 0;
|
| 223 |
-
padding: 0;
|
| 224 |
-
border: none;
|
| 225 |
-
overflow: hidden;
|
| 226 |
-
opacity: 0;
|
| 227 |
-
background: transparent;
|
| 228 |
-
box-shadow: none;
|
| 229 |
-
pointer-events: none;
|
| 230 |
-
/* 收起必须瞬时:若与占位框同时用 0.38s 过渡,右侧缩略图会与「突然出现」的槽位不同步而闪一下 */
|
| 231 |
-
transition: none !important;
|
| 232 |
-
}
|
| 233 |
-
/* 按下瞬间:冻结其它卡片与槽位动画,避免「槽位插入 + 邻居过渡」两帧打架 */
|
| 234 |
-
.batch-images-container.is-batch-settling .batch-image-wrapper:not(.batch-thumb--source) {
|
| 235 |
-
transition: none !important;
|
| 236 |
-
}
|
| 237 |
-
.batch-images-container.is-batch-settling .batch-thumb-drop-slot {
|
| 238 |
-
animation: none;
|
| 239 |
-
opacity: 1;
|
| 240 |
-
}
|
| 241 |
-
/* 拖动时跟手的浮动缩略图(避免原槽位透明后光标下像「黑块」) */
|
| 242 |
-
.batch-thumb-floating-ghost {
|
| 243 |
-
position: fixed;
|
| 244 |
-
left: 0;
|
| 245 |
-
top: 0;
|
| 246 |
-
z-index: 99999;
|
| 247 |
-
width: 76px;
|
| 248 |
-
height: 76px;
|
| 249 |
-
border-radius: 12px;
|
| 250 |
-
overflow: hidden;
|
| 251 |
-
pointer-events: none;
|
| 252 |
-
will-change: transform;
|
| 253 |
-
box-shadow:
|
| 254 |
-
0 20px 50px rgba(0, 0, 0, 0.45),
|
| 255 |
-
0 10px 28px rgba(0, 0, 0, 0.28),
|
| 256 |
-
0 0 0 1px rgba(255, 255, 255, 0.18);
|
| 257 |
-
transform: translate3d(0, 0, 0) scale(1.06) rotate(-1deg);
|
| 258 |
-
}
|
| 259 |
-
.batch-thumb-floating-ghost img {
|
| 260 |
-
width: 100%;
|
| 261 |
-
height: 100%;
|
| 262 |
-
object-fit: cover;
|
| 263 |
-
display: block;
|
| 264 |
-
pointer-events: none;
|
| 265 |
-
}
|
| 266 |
-
.batch-thumb-drop-slot {
|
| 267 |
-
flex: 0 0 72px;
|
| 268 |
-
width: 72px;
|
| 269 |
-
height: 72px;
|
| 270 |
-
box-sizing: border-box;
|
| 271 |
-
border-radius: 12px;
|
| 272 |
-
border: 2px dashed rgba(255, 255, 255, 0.22);
|
| 273 |
-
background: linear-gradient(145deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.03));
|
| 274 |
-
pointer-events: none;
|
| 275 |
-
transition: border-color 0.35s ease, box-shadow 0.35s ease, opacity 0.35s ease;
|
| 276 |
-
animation: batch-slot-breathe 2.4s ease-in-out infinite;
|
| 277 |
-
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
|
| 278 |
-
}
|
| 279 |
-
@keyframes batch-slot-breathe {
|
| 280 |
-
0%, 100% { opacity: 0.88; }
|
| 281 |
-
50% { opacity: 1; }
|
| 282 |
-
}
|
| 283 |
-
.batch-image-wrapper .batch-thumb-img-wrap {
|
| 284 |
-
width: 100%;
|
| 285 |
-
height: 100%;
|
| 286 |
-
border-radius: 9px;
|
| 287 |
-
overflow: hidden;
|
| 288 |
-
/* 必须让事件落到外层 .batch-image-wrapper,否则 HTML5 drag 无法从 draggable 父级启动 */
|
| 289 |
-
pointer-events: none;
|
| 290 |
-
}
|
| 291 |
-
.batch-image-wrapper .batch-thumb-img {
|
| 292 |
-
width: 100%;
|
| 293 |
-
height: 100%;
|
| 294 |
-
object-fit: cover;
|
| 295 |
-
display: block;
|
| 296 |
-
pointer-events: none;
|
| 297 |
-
user-select: none;
|
| 298 |
-
-webkit-user-drag: none;
|
| 299 |
-
}
|
| 300 |
-
.batch-thumb-remove {
|
| 301 |
-
position: absolute;
|
| 302 |
-
top: 3px;
|
| 303 |
-
right: 3px;
|
| 304 |
-
z-index: 5;
|
| 305 |
-
box-sizing: border-box;
|
| 306 |
-
min-width: 22px;
|
| 307 |
-
height: 22px;
|
| 308 |
-
padding: 0 5px;
|
| 309 |
-
margin: 0;
|
| 310 |
-
border: 1px solid rgba(255, 255, 255, 0.12);
|
| 311 |
-
border-radius: 6px;
|
| 312 |
-
background: rgba(0, 0, 0, 0.5);
|
| 313 |
-
font-family: inherit;
|
| 314 |
-
font-size: 14px;
|
| 315 |
-
font-weight: 400;
|
| 316 |
-
line-height: 1;
|
| 317 |
-
color: rgba(255, 255, 255, 0.9);
|
| 318 |
-
opacity: 0.72;
|
| 319 |
-
cursor: pointer;
|
| 320 |
-
display: flex;
|
| 321 |
-
align-items: center;
|
| 322 |
-
justify-content: center;
|
| 323 |
-
transition: background 0.12s, opacity 0.12s, border-color 0.12s;
|
| 324 |
-
pointer-events: auto;
|
| 325 |
-
}
|
| 326 |
-
.batch-image-wrapper:hover .batch-thumb-remove {
|
| 327 |
-
opacity: 1;
|
| 328 |
-
background: rgba(0, 0, 0, 0.68);
|
| 329 |
-
border-color: rgba(255, 255, 255, 0.2);
|
| 330 |
-
}
|
| 331 |
-
.batch-thumb-remove:hover {
|
| 332 |
-
background: rgba(80, 20, 20, 0.75) !important;
|
| 333 |
-
border-color: rgba(255, 180, 180, 0.35);
|
| 334 |
-
color: #fff;
|
| 335 |
-
}
|
| 336 |
-
.batch-thumb-remove:focus-visible {
|
| 337 |
-
opacity: 1;
|
| 338 |
-
outline: 2px solid var(--accent-dim, rgba(120, 160, 255, 0.6));
|
| 339 |
-
outline-offset: 1px;
|
| 340 |
-
}
|
| 341 |
-
.upload-icon { font-size: 18px; margin-bottom: 6px; opacity: 0.45; }
|
| 342 |
-
.upload-text { font-size: 11px; color: var(--text); }
|
| 343 |
-
.upload-hint { font-size: 10px; color: var(--text-dim); margin-top: 3px; }
|
| 344 |
-
.preview-thumb { width: 100%; height: auto; max-height: 100px; object-fit: contain; border-radius: 8px; display: none; margin-top: 10px; }
|
| 345 |
-
.clear-img-overlay {
|
| 346 |
-
position: absolute; top: 8px; right: 8px; background: rgba(255,59,48,0.85); color: white;
|
| 347 |
-
width: 20px; height: 20px; border-radius: 10px; display: none; align-items: center; justify-content: center;
|
| 348 |
-
font-size: 11px; cursor: pointer; z-index: 5;
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
.btn-outline {
|
| 352 |
-
background: var(--panel-2);
|
| 353 |
-
border: 1px solid var(--border);
|
| 354 |
-
color: var(--text-sub); padding: 5px 12px; border-radius: 7px;
|
| 355 |
-
font-size: 11.5px; font-weight: 600; cursor: pointer;
|
| 356 |
-
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
| 357 |
-
display: inline-flex; align-items: center; justify-content: center; gap: 5px;
|
| 358 |
-
white-space: nowrap;
|
| 359 |
-
}
|
| 360 |
-
.btn-outline:hover:not(:disabled) { background: rgba(255,255,255,0.08); color: var(--text); border-color: rgba(255,255,255,0.18); }
|
| 361 |
-
.btn-outline:active { opacity: 0.7; }
|
| 362 |
-
.btn-outline:disabled { opacity: 0.3; cursor: not-allowed; }
|
| 363 |
-
|
| 364 |
-
.btn-icon {
|
| 365 |
-
padding: 5px; background: transparent; border: none; color: var(--text-dim);
|
| 366 |
-
border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
| 367 |
-
transition: color 0.15s, background 0.15s;
|
| 368 |
-
}
|
| 369 |
-
.btn-icon:hover { color: var(--text-sub); background: rgba(255,255,255,0.07); }
|
| 370 |
-
|
| 371 |
-
.btn-primary {
|
| 372 |
-
width: 100%; padding: 13px;
|
| 373 |
-
background: var(--accent); border: none;
|
| 374 |
-
border-radius: 9px; color: #fff; font-weight: 700; font-size: 13.5px;
|
| 375 |
-
letter-spacing: 0.2px; cursor: pointer; margin-top: 14px;
|
| 376 |
-
transition: background 0.15s;
|
| 377 |
-
}
|
| 378 |
-
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); }
|
| 379 |
-
.btn-primary:active { opacity: 0.82; }
|
| 380 |
-
.btn-primary:disabled { background: rgba(255,255,255,0.08); color: var(--text-dim); cursor: not-allowed; }
|
| 381 |
-
|
| 382 |
-
.btn-danger {
|
| 383 |
-
width: 100%; padding: 12px; background: #DC2626; border: none;
|
| 384 |
-
border-radius: 9px; color: #fff; font-weight: 700; font-size: 13.5px;
|
| 385 |
-
cursor: pointer; margin-top: 8px; display: none; transition: background 0.15s;
|
| 386 |
-
}
|
| 387 |
-
.btn-danger:hover { background: #EF4444; }
|
| 388 |
-
|
| 389 |
-
/* Workspace */
|
| 390 |
-
.workspace { flex: 1; display: flex; flex-direction: column; background: #0A0A0A; position: relative; overflow: hidden; }
|
| 391 |
-
.viewer { flex: 2; display: flex; align-items: center; justify-content: center; padding: 16px; background: #0A0A0A; position: relative; min-height: 40vh; }
|
| 392 |
-
.monitor {
|
| 393 |
-
width: 100%; height: 100%; max-width: 1650px; border-radius: 10px; border: 1px solid var(--border);
|
| 394 |
-
overflow: hidden; position: relative; background: #070707;
|
| 395 |
-
display: flex; align-items: center; justify-content: center;
|
| 396 |
-
background-image: radial-gradient(rgba(255,255,255,0.02) 1px, transparent 1px);
|
| 397 |
-
background-size: 18px 18px;
|
| 398 |
-
}
|
| 399 |
-
.monitor img, .monitor video {
|
| 400 |
-
width: auto; height: auto; max-width: 100%; max-height: 100%;
|
| 401 |
-
object-fit: contain; display: none; z-index: 2; border-radius: 3px;
|
| 402 |
-
}
|
| 403 |
-
|
| 404 |
-
.progress-container { position: absolute; bottom: 0; left: 0; width: 100%; height: 2px; background: var(--border-2); z-index: 10; }
|
| 405 |
-
#progress-fill { width: 0%; height: 100%; background: var(--accent); transition: width 0.5s; }
|
| 406 |
-
#loading-txt { font-size: 12px; color: var(--text-sub); font-weight: 600; z-index: 5; position: absolute; display: none; }
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
.spinner {
|
| 411 |
-
width: 12px; height: 12px;
|
| 412 |
-
border: 2px solid rgba(255,255,255,0.2);
|
| 413 |
-
border-top-color: currentColor;
|
| 414 |
-
border-radius: 50%;
|
| 415 |
-
animation: spin 1s linear infinite;
|
| 416 |
-
}
|
| 417 |
-
@keyframes spin { to { transform: rotate(360deg); } }
|
| 418 |
-
|
| 419 |
-
.loading-card {
|
| 420 |
-
display: flex; align-items: center; justify-content: center;
|
| 421 |
-
flex-direction: column; gap: 6px; color: var(--text-dim); font-size: 10px;
|
| 422 |
-
background: rgba(37,99,235,0.07) !important;
|
| 423 |
-
border-color: rgba(37,99,235,0.3) !important;
|
| 424 |
-
}
|
| 425 |
-
.loading-card .spinner { width: 28px; height: 28px; border-width: 3px; color: var(--accent); }
|
| 426 |
-
.loading-card:hover { background: rgba(37,99,235,0.14) !important; border-color: var(--accent) !important; }
|
| 427 |
-
|
| 428 |
-
.library { flex: 1.5; border-top: 1px solid var(--border); padding: 14px 20px; display: flex; flex-direction: column; background: #0F0F11; overflow-y: hidden; }
|
| 429 |
-
#log-container { flex: 1; overflow-y: auto; padding-right: 4px; }
|
| 430 |
-
#log { font-family: ui-monospace, "SF Mono", monospace; font-size: 10.5px; color: var(--text-dim); line-height: 1.7; }
|
| 431 |
-
|
| 432 |
-
/* History wrapper: scrollable area for thumbnails only */
|
| 433 |
-
#history-wrapper {
|
| 434 |
-
flex: 1;
|
| 435 |
-
overflow-y: auto;
|
| 436 |
-
min-height: 110px; /* always show at least one row */
|
| 437 |
-
padding-right: 4px;
|
| 438 |
-
}
|
| 439 |
-
#history-container {
|
| 440 |
-
display: grid;
|
| 441 |
-
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
| 442 |
-
justify-content: start;
|
| 443 |
-
gap: 10px; align-content: flex-start;
|
| 444 |
-
padding-bottom: 4px;
|
| 445 |
-
}
|
| 446 |
-
/* Pagination row: hidden, using infinite scroll instead */
|
| 447 |
-
#pagination-bar {
|
| 448 |
-
display: none;
|
| 449 |
-
}
|
| 450 |
-
|
| 451 |
-
.history-card {
|
| 452 |
-
width: 100%; max-width: 200px; aspect-ratio: 16 / 9;
|
| 453 |
-
background: #1A1A1E; border-radius: 7px;
|
| 454 |
-
overflow: hidden; border: 1px solid var(--border);
|
| 455 |
-
cursor: pointer; position: relative; transition: border-color 0.15s, transform 0.15s;
|
| 456 |
-
}
|
| 457 |
-
.history-card:hover { border-color: var(--accent); transform: translateY(-1px); }
|
| 458 |
-
.history-card img, .history-card video {
|
| 459 |
-
width: 100%; height: 100%; object-fit: cover;
|
| 460 |
-
background: #1A1A1E;
|
| 461 |
-
}
|
| 462 |
-
.history-audio-thumb {
|
| 463 |
-
width: 100%; height: 100%; display: flex; flex-direction: column;
|
| 464 |
-
align-items: center; justify-content: center; gap: 6px;
|
| 465 |
-
color: var(--text); background: linear-gradient(135deg, rgba(35,35,40,0.95), rgba(18,18,22,0.98));
|
| 466 |
-
font-size: 11px; font-weight: 700; text-align: center; padding: 10px; box-sizing: border-box;
|
| 467 |
-
}
|
| 468 |
-
.history-audio-icon { font-size: 24px; line-height: 1; }
|
| 469 |
-
#audio-wrapper {
|
| 470 |
-
position: absolute; inset: 0; width: 100%; height: 100%; min-height: 0;
|
| 471 |
-
display: none; flex-direction: column; align-items: stretch; justify-content: stretch; z-index: 2;
|
| 472 |
-
background: #070707;
|
| 473 |
-
background-image: radial-gradient(rgba(255,255,255,0.035) 1px, transparent 1px);
|
| 474 |
-
background-size: 18px 18px;
|
| 475 |
-
}
|
| 476 |
-
.audio-preview-art {
|
| 477 |
-
position: absolute; inset: 0 0 54px 0; display: flex; flex-direction: column;
|
| 478 |
-
align-items: center; justify-content: center; gap: 12px; padding: 24px;
|
| 479 |
-
color: var(--text); cursor: pointer; user-select: none; z-index: 2;
|
| 480 |
-
}
|
| 481 |
-
.audio-preview-icon {
|
| 482 |
-
width: 86px; height: 86px; display: flex; align-items: center; justify-content: center;
|
| 483 |
-
border: 1px solid var(--border); border-radius: 50%; background: rgba(255,255,255,0.04);
|
| 484 |
-
font-size: 46px; font-weight: 800; color: var(--accent);
|
| 485 |
-
}
|
| 486 |
-
#audio-preview-title {
|
| 487 |
-
max-width: min(620px, 86%); font-size: 13px; font-weight: 800; color: var(--text);
|
| 488 |
-
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: center;
|
| 489 |
-
}
|
| 490 |
-
#audio-wrapper .plyr {
|
| 491 |
-
position: absolute; left: 0; right: 0; bottom: 0; top: auto;
|
| 492 |
-
width: 100%; height: auto !important; min-height: 0; z-index: 3;
|
| 493 |
-
border-radius: 0;
|
| 494 |
-
}
|
| 495 |
-
#audio-wrapper .plyr--audio .plyr__controls {
|
| 496 |
-
border-radius: 0; border-top: 1px solid var(--border); background: rgba(12,12,14,0.96);
|
| 497 |
-
}
|
| 498 |
-
#res-audio { width: 100%; }
|
| 499 |
-
.preview-download-btn {
|
| 500 |
-
position: absolute;
|
| 501 |
-
top: 14px;
|
| 502 |
-
right: 14px;
|
| 503 |
-
z-index: 6;
|
| 504 |
-
display: inline-flex;
|
| 505 |
-
align-items: center;
|
| 506 |
-
gap: 8px;
|
| 507 |
-
padding: 8px 12px;
|
| 508 |
-
border-radius: 999px;
|
| 509 |
-
border: 1px solid rgba(255,255,255,0.16);
|
| 510 |
-
background: rgba(10,10,12,0.72);
|
| 511 |
-
color: #fff;
|
| 512 |
-
font-size: 11px;
|
| 513 |
-
font-weight: 800;
|
| 514 |
-
letter-spacing: 0.02em;
|
| 515 |
-
cursor: pointer;
|
| 516 |
-
backdrop-filter: blur(10px);
|
| 517 |
-
box-shadow: 0 8px 24px rgba(0,0,0,0.24);
|
| 518 |
-
transition: transform 0.18s ease, background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
|
| 519 |
-
}
|
| 520 |
-
.preview-download-btn:hover {
|
| 521 |
-
transform: translateY(-1px);
|
| 522 |
-
background: rgba(18,18,22,0.9);
|
| 523 |
-
border-color: rgba(92,214,143,0.4);
|
| 524 |
-
box-shadow: 0 10px 28px rgba(0,0,0,0.3);
|
| 525 |
-
}
|
| 526 |
-
.preview-download-btn:active {
|
| 527 |
-
transform: translateY(0);
|
| 528 |
-
background: rgba(26,26,30,0.96);
|
| 529 |
-
}
|
| 530 |
-
.preview-download-btn-icon {
|
| 531 |
-
width: 24px;
|
| 532 |
-
height: 24px;
|
| 533 |
-
display: inline-flex;
|
| 534 |
-
align-items: center;
|
| 535 |
-
justify-content: center;
|
| 536 |
-
border-radius: 999px;
|
| 537 |
-
background: rgba(255,255,255,0.08);
|
| 538 |
-
color: var(--accent);
|
| 539 |
-
flex-shrink: 0;
|
| 540 |
-
}
|
| 541 |
-
.preview-download-btn-text {
|
| 542 |
-
line-height: 1;
|
| 543 |
-
}
|
| 544 |
-
.preview-replay-actions {
|
| 545 |
-
position: absolute;
|
| 546 |
-
top: 14px;
|
| 547 |
-
left: 14px;
|
| 548 |
-
z-index: 80;
|
| 549 |
-
display: inline-flex;
|
| 550 |
-
gap: 8px;
|
| 551 |
-
align-items: center;
|
| 552 |
-
pointer-events: auto;
|
| 553 |
-
}
|
| 554 |
-
.preview-replay-actions button {
|
| 555 |
-
height: 34px;
|
| 556 |
-
padding: 0 12px;
|
| 557 |
-
border-radius: 999px;
|
| 558 |
-
border: 1px solid rgba(255,255,255,0.16);
|
| 559 |
-
background: rgba(10,10,12,0.72);
|
| 560 |
-
color: #fff;
|
| 561 |
-
font-size: 11px;
|
| 562 |
-
font-weight: 800;
|
| 563 |
-
cursor: pointer;
|
| 564 |
-
backdrop-filter: blur(10px);
|
| 565 |
-
pointer-events: auto;
|
| 566 |
-
}
|
| 567 |
-
.preview-replay-actions button:hover {
|
| 568 |
-
border-color: rgba(92,214,143,0.42);
|
| 569 |
-
background: rgba(18,32,24,0.9);
|
| 570 |
-
color: var(--accent);
|
| 571 |
-
}
|
| 572 |
-
.preview-replay-actions.is-unavailable button {
|
| 573 |
-
opacity: 0.42;
|
| 574 |
-
color: var(--text-dim);
|
| 575 |
-
border-color: rgba(255,255,255,0.08);
|
| 576 |
-
background: rgba(10,10,12,0.54);
|
| 577 |
-
}
|
| 578 |
-
.seed-panel {
|
| 579 |
-
margin-top: 10px;
|
| 580 |
-
padding: 10px;
|
| 581 |
-
border-radius: 10px;
|
| 582 |
-
border: 1px solid var(--border-2);
|
| 583 |
-
background: rgba(255,255,255,0.025);
|
| 584 |
-
}
|
| 585 |
-
.seed-panel-head {
|
| 586 |
-
display: flex;
|
| 587 |
-
align-items: center;
|
| 588 |
-
margin-bottom: 8px;
|
| 589 |
-
color: var(--text-dim);
|
| 590 |
-
font-size: 10px;
|
| 591 |
-
font-weight: 800;
|
| 592 |
-
letter-spacing: 0.6px;
|
| 593 |
-
text-transform: uppercase;
|
| 594 |
-
}
|
| 595 |
-
.seed-control {
|
| 596 |
-
display: grid;
|
| 597 |
-
grid-template-columns: minmax(0, 1fr) 112px;
|
| 598 |
-
gap: 8px;
|
| 599 |
-
align-items: stretch;
|
| 600 |
-
}
|
| 601 |
-
.seed-input-shell {
|
| 602 |
-
height: 36px;
|
| 603 |
-
display: flex;
|
| 604 |
-
align-items: center;
|
| 605 |
-
border-radius: 8px;
|
| 606 |
-
background: var(--panel-2);
|
| 607 |
-
border: 1px solid var(--border);
|
| 608 |
-
overflow: hidden;
|
| 609 |
-
transition: border-color 0.15s, box-shadow 0.15s, opacity 0.15s;
|
| 610 |
-
}
|
| 611 |
-
.seed-input-shell:focus-within {
|
| 612 |
-
border-color: var(--accent);
|
| 613 |
-
box-shadow: 0 0 0 2px var(--accent-ring);
|
| 614 |
-
}
|
| 615 |
-
#seed-value {
|
| 616 |
-
height: 100%;
|
| 617 |
-
width: 100%;
|
| 618 |
-
margin: 0;
|
| 619 |
-
border: 0;
|
| 620 |
-
border-radius: 0;
|
| 621 |
-
background: transparent;
|
| 622 |
-
box-shadow: none;
|
| 623 |
-
color: var(--text);
|
| 624 |
-
font-family: ui-monospace, "SF Mono", monospace;
|
| 625 |
-
font-size: 12px;
|
| 626 |
-
font-weight: 700;
|
| 627 |
-
font-variant-numeric: tabular-nums;
|
| 628 |
-
padding: 0 11px;
|
| 629 |
-
}
|
| 630 |
-
#seed-value:focus { box-shadow: none; }
|
| 631 |
-
.seed-input-shell.is-random #seed-value {
|
| 632 |
-
color: var(--text-dim);
|
| 633 |
-
}
|
| 634 |
-
.seed-mode-tabs {
|
| 635 |
-
height: 36px;
|
| 636 |
-
display: grid;
|
| 637 |
-
grid-template-columns: 1fr 1fr;
|
| 638 |
-
gap: 3px;
|
| 639 |
-
padding: 3px;
|
| 640 |
-
border-radius: 8px;
|
| 641 |
-
border: 1px solid var(--border);
|
| 642 |
-
background: rgba(255,255,255,0.04);
|
| 643 |
-
}
|
| 644 |
-
.seed-mode-option {
|
| 645 |
-
margin: 0;
|
| 646 |
-
height: 28px;
|
| 647 |
-
display: inline-flex;
|
| 648 |
-
align-items: center;
|
| 649 |
-
justify-content: center;
|
| 650 |
-
border-radius: 6px;
|
| 651 |
-
color: var(--text-dim);
|
| 652 |
-
cursor: pointer;
|
| 653 |
-
font-size: 10.5px;
|
| 654 |
-
font-weight: 800;
|
| 655 |
-
letter-spacing: 0;
|
| 656 |
-
text-transform: none;
|
| 657 |
-
transition: background 0.15s, color 0.15s;
|
| 658 |
-
}
|
| 659 |
-
.seed-mode-option input { display: none; }
|
| 660 |
-
.seed-mode-option.is-active {
|
| 661 |
-
background: var(--accent);
|
| 662 |
-
color: #fff;
|
| 663 |
-
}
|
| 664 |
-
.seed-mode-option:not(.is-active):hover {
|
| 665 |
-
background: rgba(255,255,255,0.06);
|
| 666 |
-
color: var(--text-sub);
|
| 667 |
-
}
|
| 668 |
-
/* 解码/加载完成前避免视频黑块猛闪,与卡片底色一致;就绪后淡入 */
|
| 669 |
-
.history-card .history-thumb-media {
|
| 670 |
-
opacity: 0;
|
| 671 |
-
transition: opacity 0.28s ease;
|
| 672 |
-
}
|
| 673 |
-
.history-card .history-thumb-media.history-thumb-ready {
|
| 674 |
-
opacity: 1;
|
| 675 |
-
}
|
| 676 |
-
.history-type-badge {
|
| 677 |
-
position: absolute; top: 5px; left: 5px; font-size: 8px; padding: 1px 5px; border-radius: 3px;
|
| 678 |
-
background: rgba(0,0,0,0.8); color: var(--text-sub); border: 1px solid rgba(255,255,255,0.06);
|
| 679 |
-
z-index: 2; font-weight: 700; letter-spacing: 0.4px;
|
| 680 |
-
}
|
| 681 |
-
.history-delete-btn {
|
| 682 |
-
position: absolute; top: 5px; right: 5px; width: 20px; height: 20px;
|
| 683 |
-
border-radius: 50%; border: none; background: rgba(255,50,50,0.8); color: #fff;
|
| 684 |
-
font-size: 10px; cursor: pointer; z-index: 3; display: flex; align-items: center; justify-content: center;
|
| 685 |
-
opacity: 0; transition: opacity 0.2s;
|
| 686 |
-
}
|
| 687 |
-
.history-card:hover .history-delete-btn { opacity: 1; }
|
| 688 |
-
.history-delete-btn:hover { background: rgba(255,0,0,0.9); }
|
| 689 |
-
.vram-bar { width: 160px; height: 5px; background: rgba(255,255,255,0.08); border-radius: 999px; overflow: hidden; display: inline-block; vertical-align: middle; }
|
| 690 |
-
.vram-used { height: 100%; background: var(--accent); width: 0%; transition: width 0.5s; }
|
| 691 |
-
|
| 692 |
-
/* 智能多帧:工作流模式卡片式单选 */
|
| 693 |
-
.smart-param-mode-label {
|
| 694 |
-
font-size: 10px;
|
| 695 |
-
color: var(--text-dim);
|
| 696 |
-
font-weight: 700;
|
| 697 |
-
margin-bottom: 8px;
|
| 698 |
-
letter-spacing: 0.04em;
|
| 699 |
-
text-transform: uppercase;
|
| 700 |
-
}
|
| 701 |
-
.smart-param-modes {
|
| 702 |
-
display: flex;
|
| 703 |
-
flex-direction: row;
|
| 704 |
-
align-items: stretch;
|
| 705 |
-
gap: 0;
|
| 706 |
-
padding: 3px;
|
| 707 |
-
margin-bottom: 12px;
|
| 708 |
-
background: var(--panel-2);
|
| 709 |
-
border-radius: 8px;
|
| 710 |
-
border: 1px solid var(--border);
|
| 711 |
-
}
|
| 712 |
-
.smart-param-mode-opt {
|
| 713 |
-
display: flex;
|
| 714 |
-
align-items: center;
|
| 715 |
-
justify-content: center;
|
| 716 |
-
flex: 1;
|
| 717 |
-
min-width: 0;
|
| 718 |
-
gap: 0;
|
| 719 |
-
margin: 0;
|
| 720 |
-
padding: 6px 8px;
|
| 721 |
-
border-radius: 6px;
|
| 722 |
-
border: none;
|
| 723 |
-
background: transparent;
|
| 724 |
-
cursor: pointer;
|
| 725 |
-
transition: background 0.15s, color 0.15s;
|
| 726 |
-
position: relative;
|
| 727 |
-
}
|
| 728 |
-
.smart-param-mode-opt:hover:not(:has(input:checked)) {
|
| 729 |
-
background: rgba(255, 255, 255, 0.05);
|
| 730 |
-
}
|
| 731 |
-
.smart-param-mode-opt input[type="radio"] {
|
| 732 |
-
position: absolute;
|
| 733 |
-
opacity: 0;
|
| 734 |
-
width: 0;
|
| 735 |
-
height: 0;
|
| 736 |
-
margin: 0;
|
| 737 |
-
}
|
| 738 |
-
.smart-param-mode-opt:has(input:checked) {
|
| 739 |
-
background: var(--accent);
|
| 740 |
-
box-shadow: none;
|
| 741 |
-
}
|
| 742 |
-
.smart-param-mode-opt:has(input:checked) .smart-param-mode-title {
|
| 743 |
-
color: #fff;
|
| 744 |
-
}
|
| 745 |
-
.smart-param-mode-title {
|
| 746 |
-
font-size: 11px;
|
| 747 |
-
font-weight: 600;
|
| 748 |
-
color: var(--text-sub);
|
| 749 |
-
text-align: center;
|
| 750 |
-
line-height: 1.25;
|
| 751 |
-
flex: none;
|
| 752 |
-
min-width: 0;
|
| 753 |
-
}
|
| 754 |
-
/* 单次多关键帧:时间轴面板 */
|
| 755 |
-
.batch-kf-panel {
|
| 756 |
-
background: var(--item);
|
| 757 |
-
border-radius: 10px;
|
| 758 |
-
padding: 12px 14px;
|
| 759 |
-
margin-bottom: 10px;
|
| 760 |
-
border: 1px solid var(--border);
|
| 761 |
-
}
|
| 762 |
-
.batch-kf-panel-hd {
|
| 763 |
-
display: flex;
|
| 764 |
-
flex-wrap: wrap;
|
| 765 |
-
align-items: center;
|
| 766 |
-
justify-content: space-between;
|
| 767 |
-
gap: 10px;
|
| 768 |
-
margin-bottom: 8px;
|
| 769 |
-
}
|
| 770 |
-
.batch-kf-panel-title {
|
| 771 |
-
font-size: 12px;
|
| 772 |
-
font-weight: 700;
|
| 773 |
-
color: var(--text);
|
| 774 |
-
}
|
| 775 |
-
.batch-kf-total-pill {
|
| 776 |
-
font-size: 11px;
|
| 777 |
-
color: var(--text-sub);
|
| 778 |
-
background: var(--panel-2);
|
| 779 |
-
border: 1px solid var(--border);
|
| 780 |
-
border-radius: 999px;
|
| 781 |
-
padding: 6px 12px;
|
| 782 |
-
white-space: nowrap;
|
| 783 |
-
}
|
| 784 |
-
.batch-kf-total-pill strong {
|
| 785 |
-
color: var(--accent);
|
| 786 |
-
font-weight: 800;
|
| 787 |
-
font-variant-numeric: tabular-nums;
|
| 788 |
-
margin: 0 2px;
|
| 789 |
-
}
|
| 790 |
-
.batch-kf-total-unit {
|
| 791 |
-
font-size: 10px;
|
| 792 |
-
color: var(--text-dim);
|
| 793 |
-
}
|
| 794 |
-
.batch-kf-panel-hint {
|
| 795 |
-
font-size: 10px;
|
| 796 |
-
color: var(--text-dim);
|
| 797 |
-
line-height: 1.5;
|
| 798 |
-
margin: 0 0 12px;
|
| 799 |
-
}
|
| 800 |
-
.batch-kf-timeline-col {
|
| 801 |
-
display: flex;
|
| 802 |
-
flex-direction: column;
|
| 803 |
-
gap: 0;
|
| 804 |
-
}
|
| 805 |
-
.batch-kf-kcard {
|
| 806 |
-
border-radius: 10px;
|
| 807 |
-
border: 1px solid var(--border);
|
| 808 |
-
background: rgba(255, 255, 255, 0.03);
|
| 809 |
-
padding: 10px 12px;
|
| 810 |
-
}
|
| 811 |
-
.batch-kf-kcard-head {
|
| 812 |
-
display: flex;
|
| 813 |
-
align-items: center;
|
| 814 |
-
gap: 12px;
|
| 815 |
-
margin-bottom: 10px;
|
| 816 |
-
}
|
| 817 |
-
.batch-kf-kthumb {
|
| 818 |
-
width: 48px;
|
| 819 |
-
height: 48px;
|
| 820 |
-
border-radius: 8px;
|
| 821 |
-
object-fit: cover;
|
| 822 |
-
flex-shrink: 0;
|
| 823 |
-
border: 1px solid var(--border);
|
| 824 |
-
}
|
| 825 |
-
.batch-kf-kcard-titles {
|
| 826 |
-
display: flex;
|
| 827 |
-
flex-direction: column;
|
| 828 |
-
gap: 4px;
|
| 829 |
-
min-width: 0;
|
| 830 |
-
}
|
| 831 |
-
.batch-kf-ktitle {
|
| 832 |
-
font-size: 12px;
|
| 833 |
-
font-weight: 700;
|
| 834 |
-
color: var(--text);
|
| 835 |
-
}
|
| 836 |
-
.batch-kf-anchor {
|
| 837 |
-
font-size: 11px;
|
| 838 |
-
color: var(--accent);
|
| 839 |
-
font-variant-numeric: tabular-nums;
|
| 840 |
-
font-weight: 600;
|
| 841 |
-
}
|
| 842 |
-
.batch-kf-kcard-ctrl {
|
| 843 |
-
display: flex;
|
| 844 |
-
flex-wrap: wrap;
|
| 845 |
-
align-items: center;
|
| 846 |
-
gap: 12px;
|
| 847 |
-
}
|
| 848 |
-
.batch-kf-klabel {
|
| 849 |
-
font-size: 10px;
|
| 850 |
-
color: var(--text-dim);
|
| 851 |
-
display: flex;
|
| 852 |
-
align-items: center;
|
| 853 |
-
gap: 8px;
|
| 854 |
-
}
|
| 855 |
-
.batch-kf-klabel input[type="number"] {
|
| 856 |
-
width: 72px;
|
| 857 |
-
padding: 6px 8px;
|
| 858 |
-
font-size: 12px;
|
| 859 |
-
border-radius: 6px;
|
| 860 |
-
border: 1px solid var(--border);
|
| 861 |
-
background: var(--panel);
|
| 862 |
-
color: var(--text);
|
| 863 |
-
}
|
| 864 |
-
/* 关键帧之间:细时间轴 + 单行紧凑间隔输入 */
|
| 865 |
-
.batch-kf-gap {
|
| 866 |
-
display: flex;
|
| 867 |
-
align-items: stretch;
|
| 868 |
-
gap: 8px;
|
| 869 |
-
padding: 0 0 6px;
|
| 870 |
-
margin: 0 0 0 10px;
|
| 871 |
-
}
|
| 872 |
-
.batch-kf-gap-rail {
|
| 873 |
-
width: 2px;
|
| 874 |
-
flex-shrink: 0;
|
| 875 |
-
border-radius: 2px;
|
| 876 |
-
background: linear-gradient(
|
| 877 |
-
180deg,
|
| 878 |
-
rgba(255, 255, 255, 0.06),
|
| 879 |
-
var(--accent-dim),
|
| 880 |
-
rgba(255, 255, 255, 0.04)
|
| 881 |
-
);
|
| 882 |
-
min-height: 22px;
|
| 883 |
-
align-self: stretch;
|
| 884 |
-
}
|
| 885 |
-
.batch-kf-gap-inner {
|
| 886 |
-
display: flex;
|
| 887 |
-
align-items: center;
|
| 888 |
-
gap: 8px;
|
| 889 |
-
flex: 1;
|
| 890 |
-
min-width: 0;
|
| 891 |
-
padding: 2px 0 4px;
|
| 892 |
-
}
|
| 893 |
-
.batch-kf-gap-ix {
|
| 894 |
-
font-size: 10px;
|
| 895 |
-
font-weight: 600;
|
| 896 |
-
color: var(--text-dim);
|
| 897 |
-
font-variant-numeric: tabular-nums;
|
| 898 |
-
letter-spacing: -0.02em;
|
| 899 |
-
flex-shrink: 0;
|
| 900 |
-
}
|
| 901 |
-
.batch-kf-seg-field {
|
| 902 |
-
display: inline-flex;
|
| 903 |
-
align-items: center;
|
| 904 |
-
gap: 3px;
|
| 905 |
-
margin: 0;
|
| 906 |
-
cursor: text;
|
| 907 |
-
}
|
| 908 |
-
.batch-kf-seg-input {
|
| 909 |
-
width: 46px;
|
| 910 |
-
min-width: 0;
|
| 911 |
-
padding: 2px 5px;
|
| 912 |
-
font-size: 11px;
|
| 913 |
-
font-weight: 600;
|
| 914 |
-
line-height: 1.3;
|
| 915 |
-
border-radius: 4px;
|
| 916 |
-
border: 1px solid var(--border);
|
| 917 |
-
background: rgba(0, 0, 0, 0.2);
|
| 918 |
-
color: var(--text);
|
| 919 |
-
font-variant-numeric: tabular-nums;
|
| 920 |
-
}
|
| 921 |
-
.batch-kf-seg-input:hover {
|
| 922 |
-
border-color: rgba(255, 255, 255, 0.12);
|
| 923 |
-
}
|
| 924 |
-
.batch-kf-seg-input:focus {
|
| 925 |
-
outline: none;
|
| 926 |
-
border-color: var(--accent);
|
| 927 |
-
box-shadow: 0 0 0 1px var(--accent-ring);
|
| 928 |
-
}
|
| 929 |
-
.batch-kf-gap-unit {
|
| 930 |
-
font-size: 10px;
|
| 931 |
-
color: var(--text-dim);
|
| 932 |
-
font-weight: 500;
|
| 933 |
-
flex-shrink: 0;
|
| 934 |
-
}
|
| 935 |
-
|
| 936 |
-
.sub-mode-toggle { display: flex; background: var(--panel-2); border-radius: 7px; padding: 3px; border: 1px solid var(--border); }
|
| 937 |
-
.sub-mode-btn { flex: 1; padding: 6px 0; border-radius: 5px; border: none; background: transparent; font-size: 11.5px; color: var(--text-dim); font-weight: 600; cursor: pointer; transition: background 0.15s, color 0.15s; }
|
| 938 |
-
.sub-mode-btn.active { background: var(--accent); color: #fff; }
|
| 939 |
-
.sub-mode-btn:hover:not(.active) { background: rgba(255,255,255,0.05); color: var(--text-sub); }
|
| 940 |
-
|
| 941 |
-
.vid-section { display: none; margin-top: 12px; }
|
| 942 |
-
.vid-section.active-section { display: block; animation: fadeIn 0.25s ease; }
|
| 943 |
-
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
| 944 |
-
|
| 945 |
-
/* Status indicator */
|
| 946 |
-
@keyframes breathe-orange {
|
| 947 |
-
0%,100% { box-shadow: 0 0 4px #FF9F0A; opacity: 0.7; }
|
| 948 |
-
50% { box-shadow: 0 0 10px #FF9F0A; opacity: 1; }
|
| 949 |
-
}
|
| 950 |
-
.indicator-busy { background: #FF9F0A !important; animation: breathe-orange 1.6s infinite ease-in-out !important; box-shadow: none !important; transition: all 0.3s; }
|
| 951 |
-
.indicator-ready { background: #30D158 !important; box-shadow: 0 0 8px rgba(48,209,88,0.6) !important; animation: none !important; transition: all 0.3s; }
|
| 952 |
-
.indicator-offline { background: #636366 !important; box-shadow: none !important; animation: none !important; transition: all 0.3s; }
|
| 953 |
-
|
| 954 |
-
.res-preview-tag { font-size: 11px; color: var(--accent); margin-bottom: 10px; font-family: ui-monospace, monospace; }
|
| 955 |
-
.top-status { display: flex; justify-content: space-between; font-size: 12px; color: var(--text-dim); margin-bottom: 8px; align-items: center; }
|
| 956 |
-
.checkbox-container { display: flex; align-items: center; gap: 8px; cursor: pointer; background: rgba(255,255,255,0.02); padding: 10px; border-radius: 8px; border: 1px solid var(--border-2); }
|
| 957 |
-
.checkbox-container input { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; margin: 0; }
|
| 958 |
-
.checkbox-container label { margin-bottom: 0; cursor: pointer; text-transform: none; color: var(--text); }
|
| 959 |
-
.flex-row { display: flex; gap: 10px; }
|
| 960 |
-
.flex-1 { flex: 1; min-width: 0; }
|
| 961 |
-
|
| 962 |
-
@media (max-width: 1024px) {
|
| 963 |
-
body { flex-direction: column; overflow-y: auto; }
|
| 964 |
-
.sidebar { width: 100%; min-width: 100%; border-right: none; border-bottom: 1px solid var(--border); height: auto; overflow: visible; }
|
| 965 |
-
.workspace { height: auto; min-height: 100vh; overflow: visible; }
|
| 966 |
-
}
|
| 967 |
-
:root {
|
| 968 |
-
--plyr-color-main: #3F51B5;
|
| 969 |
-
--plyr-video-control-background-hover: rgba(255,255,255,0.1);
|
| 970 |
-
--plyr-control-radius: 6px;
|
| 971 |
-
--plyr-player-width: 100%;
|
| 972 |
-
}
|
| 973 |
-
.plyr {
|
| 974 |
-
border-radius: 8px;
|
| 975 |
-
overflow: hidden;
|
| 976 |
-
width: 100%;
|
| 977 |
-
height: 100%;
|
| 978 |
-
}
|
| 979 |
-
.plyr--video .plyr__controls {
|
| 980 |
-
background: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.8));
|
| 981 |
-
padding: 20px 15px 15px 15px;
|
| 982 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/UI/index.html
DELETED
|
@@ -1,604 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="zh-CN">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>LTX-2 | Multi-GPU Cinematic Studio</title>
|
| 7 |
-
<link rel="stylesheet" href="index.css?v=en-tabs-1">
|
| 8 |
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/plyr/3.7.8/plyr.css" />
|
| 9 |
-
</head>
|
| 10 |
-
<body>
|
| 11 |
-
|
| 12 |
-
<aside class="sidebar">
|
| 13 |
-
<div class="sidebar-header">
|
| 14 |
-
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
|
| 15 |
-
<div style="display: flex; align-items: center; gap: 10px;">
|
| 16 |
-
<div id="sys-indicator" class="indicator-ready" style="width: 12px; height: 12px; border-radius: 50%;"></div>
|
| 17 |
-
<span style="font-weight: 800; font-size: 18px;">LTX-2 STUDIO</span>
|
| 18 |
-
</div>
|
| 19 |
-
<div style="display: flex; gap: 8px; align-items: center;">
|
| 20 |
-
<button id="clearGpuBtn" onclick="clearGpu()" class="btn-outline" data-i18n="clearVram">释放显存</button>
|
| 21 |
-
<button type="button" id="lang-toggle-btn" class="lang-toggle">EN</button>
|
| 22 |
-
</div>
|
| 23 |
-
</div>
|
| 24 |
-
|
| 25 |
-
<div class="top-status" style="margin-bottom: 5px;">
|
| 26 |
-
<div style="display: flex; align-items: center; gap: 8px;">
|
| 27 |
-
<span id="sys-status" style="font-weight:bold; color: var(--text-dim); font-size: 12px;" data-i18n="sysScanning">正在扫描 GPU...</span>
|
| 28 |
-
</div>
|
| 29 |
-
|
| 30 |
-
<button type="button" onclick="const el = document.getElementById('sys-settings'); el.style.display = el.style.display === 'none' ? 'block' : 'none';" class="btn-icon" data-i18n-title="settingsTitle" title="系统高级设置">
|
| 31 |
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
| 32 |
-
</button>
|
| 33 |
-
|
| 34 |
-
</div>
|
| 35 |
-
|
| 36 |
-
<div style="font-size: 11px; color: var(--text-dim); margin-bottom: 20px; display: flex; align-items: center; width: 100%;">
|
| 37 |
-
<div class="vram-bar" style="width: 120px; min-width: 120px; margin-top: 0; margin-right: 12px;"><div class="vram-used" id="vram-fill"></div></div>
|
| 38 |
-
<span id="vram-text" style="font-variant-numeric: tabular-nums; flex-shrink: 0; text-align: right;">0/32 GB</span>
|
| 39 |
-
<span id="gpu-name" style="display: none;"></span> <!-- Hidden globally to avoid duplicate -->
|
| 40 |
-
</div>
|
| 41 |
-
|
| 42 |
-
<div id="sys-settings" style="display: none; padding: 14px; background: rgba(0,0,0,0.4) !important; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1); margin-bottom: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); backdrop-filter: blur(10px);">
|
| 43 |
-
<div style="font-size: 13px; font-weight: bold; margin-bottom: 12px; color: #fff;" data-i18n="advancedSettings">高级设置</div>
|
| 44 |
-
|
| 45 |
-
<label style="font-size: 11px; margin-bottom: 6px;" data-i18n="deviceSelect">工作设备选择</label>
|
| 46 |
-
<select id="gpu-selector" onchange="switchGpu(this.value)" style="margin-bottom: 12px; font-size: 11px; padding: 6px;">
|
| 47 |
-
<option value="" data-i18n="gpuDetecting">正在检测 GPU...</option>
|
| 48 |
-
</select>
|
| 49 |
-
|
| 50 |
-
<label style="font-size: 11px; margin-bottom: 6px; margin-top: 12px;" data-i18n="vramLimitLabel">可用最高显存上限 (GB, 0为全开优先显存)</label>
|
| 51 |
-
<div style="display: flex; gap: 6px; margin-bottom: 9px; align-items: stretch;">
|
| 52 |
-
<input type="number" id="vram-limit-input" value="0" data-i18n-placeholder="vramLimitPh" placeholder="例如: 12 (0表示无限制)" style="flex: 1; height: 28px; box-sizing: border-box; font-size: 12px; padding: 0 10px;">
|
| 53 |
-
<button onclick="saveVramLimit()" style="font-size: 12px; padding: 0 10px; height: 28px; box-sizing: border-box; white-space: nowrap; background: #333; border: 1px solid #555; color: #fff; border-radius: 7px; cursor: pointer;" data-i18n="saveLabel">保存</button>
|
| 54 |
-
</div>
|
| 55 |
-
<div id="vram-limit-status" style="font-size: 10px; color: var(--text-dim);"></div>
|
| 56 |
-
|
| 57 |
-
<label style="font-size: 11px; margin-bottom: 6px; margin-top: 12px;" data-i18n="modelCheckpointLabel">视频模型(蒸馏版)</label>
|
| 58 |
-
<div style="display: flex; gap: 6px; margin-bottom: 9px; align-items: stretch;">
|
| 59 |
-
<select id="model-checkpoint-select" onchange="saveSelectedModelCheckpoint()" style="flex: 1; height: 28px; box-sizing: border-box; font-size: 12px; padding: 0 8px;">
|
| 60 |
-
<option value="" data-i18n="modelCheckpointDefault">默认官方蒸馏模型</option>
|
| 61 |
-
</select>
|
| 62 |
-
<button onclick="loadModelCheckpoints()" style="font-size: 12px; padding: 0 10px; height: 28px; box-sizing: border-box; white-space: nowrap; background: #333; border: 1px solid #555; color: #fff; border-radius: 7px; cursor: pointer;" data-i18n="refresh">刷新</button>
|
| 63 |
-
</div>
|
| 64 |
-
<div id="model-checkpoint-status" style="font-size: 10px; color: var(--text-dim); margin-bottom: 6px;" data-i18n="modelCheckpointHint">推荐使用 distilled-fp8;仅显示 LTX 2.3 22B 蒸馏模型,避开 dev 模型。</div>
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
<label style="font-size: 11px; margin-bottom: 6px; margin-top: 12px;" data-i18n="loraFolderPath">LoRA 文件夹路径(可选)</label>
|
| 68 |
-
<div style="display: flex; gap: 6px; margin-bottom: 9px; align-items: stretch;">
|
| 69 |
-
<input type="text" id="lora-dir-input" placeholder="留空使用 模型目录\\loras" data-i18n-placeholder="loraFolderPathPlaceholder" style="flex: 1; height: 28px; box-sizing: border-box; font-size: 12px; padding: 0 10px;">
|
| 70 |
-
<button onclick="saveLoraDir()" style="font-size: 12px; padding: 0 10px; height: 28px; box-sizing: border-box; white-space: nowrap; background: #333; border: 1px solid #555; color: #fff; border-radius: 7px; cursor: pointer;" data-i18n="saveLabel">保存</button>
|
| 71 |
-
</div>
|
| 72 |
-
<div id="lora-placement-hint" style="font-size: 10px; color: var(--text-dim); line-height: 1.45; margin-bottom: 6px;" data-i18n="loraPlacementHint">将 LoRA 文件放到当前模型目录下的 loras 文件夹。</div>
|
| 73 |
-
<div id="lora-dir-status" style="font-size: 10px; color: var(--text-dim);"></div>
|
| 74 |
-
</div>
|
| 75 |
-
|
| 76 |
-
<div id="queue-panel" style="padding: 12px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; margin-bottom: 14px;">
|
| 77 |
-
<div style="display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:8px;">
|
| 78 |
-
<div style="font-size:12px; font-weight:800; color:var(--text-main);" data-i18n="queueTitle">任务队列</div>
|
| 79 |
-
<div id="queue-summary" style="font-size:10px; color:var(--text-dim);" data-i18n="queueIdle">空闲</div>
|
| 80 |
-
</div>
|
| 81 |
-
<div id="queue-list" style="display:flex; flex-direction:column; gap:6px;"></div>
|
| 82 |
-
</div>
|
| 83 |
-
</div>
|
| 84 |
-
|
| 85 |
-
<div class="sidebar-section" id="main-tabs-section">
|
| 86 |
-
<div class="tabs">
|
| 87 |
-
<div id="tab-video" class="tab" onclick="switchMode('video')">
|
| 88 |
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>
|
| 89 |
-
<span data-i18n="tabVideo">视频生成</span>
|
| 90 |
-
</div>
|
| 91 |
-
<div id="tab-batch" class="tab" onclick="switchMode('batch')">
|
| 92 |
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
|
| 93 |
-
<span data-i18n="tabBatch">智能多帧</span>
|
| 94 |
-
</div>
|
| 95 |
-
<div id="tab-motion" class="tab" onclick="switchMode('motion')">
|
| 96 |
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><path d="M3 12h5l3-8 4 16 3-8h3"></path></svg>
|
| 97 |
-
<span data-i18n="tabMotion">视频迁移</span>
|
| 98 |
-
</div>
|
| 99 |
-
<div id="tab-image" class="tab" onclick="switchMode('image')">
|
| 100 |
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
|
| 101 |
-
<span data-i18n="tabImage">图像生成</span>
|
| 102 |
-
</div>
|
| 103 |
-
<div id="tab-tts" class="tab" onclick="switchMode('tts')">
|
| 104 |
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>
|
| 105 |
-
<span data-i18n="tabTts">TTS 语音</span>
|
| 106 |
-
</div>
|
| 107 |
-
</div>
|
| 108 |
-
|
| 109 |
-
<div id="prompt-container">
|
| 110 |
-
<label data-i18n="promptLabel">视觉描述词 (Prompt)</label>
|
| 111 |
-
<textarea id="prompt" data-i18n-placeholder="promptPlaceholder" placeholder="在此输入视觉描述词 (Prompt)..." style="height: 90px; margin-bottom: 0;"></textarea>
|
| 112 |
-
</div>
|
| 113 |
-
|
| 114 |
-
<div id="seed-settings" class="seed-panel">
|
| 115 |
-
<div class="seed-panel-head">
|
| 116 |
-
<span data-i18n="seedLabel">随机种子 (Seed)</span>
|
| 117 |
-
</div>
|
| 118 |
-
<div class="seed-control">
|
| 119 |
-
<div class="seed-input-shell">
|
| 120 |
-
<input type="number" id="seed-value" min="1" max="2147483647" value="42" disabled>
|
| 121 |
-
</div>
|
| 122 |
-
<div class="seed-mode-tabs" role="radiogroup" aria-label="Seed mode">
|
| 123 |
-
<label class="seed-mode-option is-active">
|
| 124 |
-
<input type="radio" name="seed-mode" value="random" checked onchange="updateSeedModeUI()">
|
| 125 |
-
<span data-i18n="seedRandom">随机</span>
|
| 126 |
-
</label>
|
| 127 |
-
<label class="seed-mode-option">
|
| 128 |
-
<input type="radio" name="seed-mode" value="fixed" onchange="updateSeedModeUI()">
|
| 129 |
-
<span data-i18n="seedFixed">固定</span>
|
| 130 |
-
</label>
|
| 131 |
-
</div>
|
| 132 |
-
</div>
|
| 133 |
-
</div>
|
| 134 |
-
</div>
|
| 135 |
-
|
| 136 |
-
<!-- 视频模式选项 -->
|
| 137 |
-
<div class="sidebar-section" id="video-opts" style="display:none">
|
| 138 |
-
<div class="setting-group">
|
| 139 |
-
<div class="group-title" data-i18n="basicEngine">基础画面 / Basic EngineSpecs</div>
|
| 140 |
-
<div class="flex-row">
|
| 141 |
-
<div class="flex-1">
|
| 142 |
-
<label data-i18n="qualityLevel">清晰度级别</label>
|
| 143 |
-
<select id="vid-quality" onchange="updateResPreview()">
|
| 144 |
-
<option value="1080">1080P Full HD</option>
|
| 145 |
-
<option value="720" selected>720P Standard</option>
|
| 146 |
-
<option value="540">540P Preview</option>
|
| 147 |
-
</select>
|
| 148 |
-
</div>
|
| 149 |
-
<div class="flex-1">
|
| 150 |
-
<label data-i18n="aspectRatio">画幅比例</label>
|
| 151 |
-
<select id="vid-ratio" onchange="updateResPreview()">
|
| 152 |
-
<option value="16:9" data-i18n="ratio169">16:9 电影宽幅</option>
|
| 153 |
-
<option value="9:16" data-i18n="ratio916">9:16 移动竖屏</option>
|
| 154 |
-
<option value="1:1" data-i18n="ratio11">1:1 方形</option>
|
| 155 |
-
<option value="4:3" data-i18n="ratio43">4:3 经典横幅</option>
|
| 156 |
-
<option value="3:4" data-i18n="ratio34">3:4 经典竖幅</option>
|
| 157 |
-
<option value="21:9" data-i18n="ratio219">21:9 超宽银幕</option>
|
| 158 |
-
<option value="9:21" data-i18n="ratio921">9:21 超长竖屏</option>
|
| 159 |
-
<option value="ref" data-i18n="ratioRef">跟随参考图</option>
|
| 160 |
-
<option value="custom" data-i18n="ratioCustom">自定义尺寸</option>
|
| 161 |
-
</select>
|
| 162 |
-
</div>
|
| 163 |
-
</div>
|
| 164 |
-
<div id="vid-custom-size" class="flex-row" style="display:none; margin-top: -2px; margin-bottom: 10px;">
|
| 165 |
-
<div class="flex-1"><label data-i18n="width">宽度</label><input type="number" id="vid-custom-w" value="1280" min="64" step="64" onchange="updateResPreview()"></div>
|
| 166 |
-
<div class="flex-1"><label data-i18n="height">高度</label><input type="number" id="vid-custom-h" value="704" min="64" step="64" onchange="updateResPreview()"></div>
|
| 167 |
-
</div>
|
| 168 |
-
<div id="res-preview" class="res-preview-tag" style="margin-top: -5px; margin-bottom: 12px;">最终发送: 1280x704</div>
|
| 169 |
-
|
| 170 |
-
<div class="flex-row">
|
| 171 |
-
<div class="flex-1">
|
| 172 |
-
<label data-i18n="fpsLabel">帧率 (FPS)</label>
|
| 173 |
-
<select id="vid-fps">
|
| 174 |
-
<option value="24" selected>24 FPS</option>
|
| 175 |
-
<option value="25">25 FPS</option>
|
| 176 |
-
<option value="30">30 FPS</option>
|
| 177 |
-
<option value="48">48 FPS</option>
|
| 178 |
-
<option value="60">60 FPS</option>
|
| 179 |
-
</select>
|
| 180 |
-
</div>
|
| 181 |
-
<div class="flex-1">
|
| 182 |
-
<label data-i18n="durationLabel">时长 (秒)</label>
|
| 183 |
-
<input type="number" id="vid-duration" value="5" min="1" max="30" step="1">
|
| 184 |
-
</div>
|
| 185 |
-
</div>
|
| 186 |
-
|
| 187 |
-
<label style="margin-top: 12px;" data-i18n="cameraMotion">镜头运动方式</label>
|
| 188 |
-
<select id="vid-motion">
|
| 189 |
-
<option value="static" selected data-i18n="motionStatic">Static (静止机位)</option>
|
| 190 |
-
<option value="dolly_in" data-i18n="motionDollyIn">Dolly In (推近)</option>
|
| 191 |
-
<option value="dolly_out" data-i18n="motionDollyOut">Dolly Out (拉远)</option>
|
| 192 |
-
<option value="dolly_left" data-i18n="motionDollyLeft">Dolly Left (向左)</option>
|
| 193 |
-
<option value="dolly_right" data-i18n="motionDollyRight">Dolly Right (向右)</option>
|
| 194 |
-
<option value="jib_up" data-i18n="motionJibUp">Jib Up (升臂)</option>
|
| 195 |
-
<option value="jib_down" data-i18n="motionJibDown">Jib Down (降臂)</option>
|
| 196 |
-
<option value="focus_shift" data-i18n="motionFocus">Focus Shift (焦点)</option>
|
| 197 |
-
</select>
|
| 198 |
-
<div class="checkbox-container" style="margin-top: 8px;">
|
| 199 |
-
<input type="checkbox" id="vid-audio" checked>
|
| 200 |
-
<label for="vid-audio" data-i18n="audioGen">生成 AI 环境音 (Audio Gen)</label>
|
| 201 |
-
</div>
|
| 202 |
-
|
| 203 |
-
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
| 204 |
-
<label data-i18n="selectLora" style="margin: 0;">选择 LoRA</label>
|
| 205 |
-
<button type="button" onclick="addLoraSelection('loras-container')" style="background:none; border:none; color:var(--accent); cursor:pointer; font-size:16px; padding:0 4px;" title="添加 LoRA">+</button>
|
| 206 |
-
</div>
|
| 207 |
-
<div id="loras-container" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px;"></div>
|
| 208 |
-
</div>
|
| 209 |
-
|
| 210 |
-
<!-- 生成媒介组 -->
|
| 211 |
-
<div class="setting-group" id="video-source-group">
|
| 212 |
-
<div class="group-title" data-i18n="genSource">生成媒介 / Generation Source</div>
|
| 213 |
-
|
| 214 |
-
<div class="flex-row" style="margin-bottom: 10px;">
|
| 215 |
-
<div class="flex-1">
|
| 216 |
-
<label data-i18n="startFrame">起始帧 (首帧)</label>
|
| 217 |
-
<div class="upload-zone" id="start-frame-drop-zone" onclick="document.getElementById('start-frame-input').click()">
|
| 218 |
-
<div class="clear-img-overlay" id="clear-start-frame-overlay" onclick="event.stopPropagation(); clearFrame('start')">×</div>
|
| 219 |
-
<div id="start-frame-placeholder">
|
| 220 |
-
<div class="upload-icon">🖼️</div>
|
| 221 |
-
<div class="upload-text" data-i18n="uploadStart">上传首帧</div>
|
| 222 |
-
</div>
|
| 223 |
-
<img id="start-frame-preview" class="preview-thumb">
|
| 224 |
-
<input type="file" id="start-frame-input" accept="image/*" style="display:none" onchange="handleFrameUpload(this.files[0], 'start')">
|
| 225 |
-
</div>
|
| 226 |
-
<input type="hidden" id="start-frame-path">
|
| 227 |
-
</div>
|
| 228 |
-
<div class="flex-1">
|
| 229 |
-
<label data-i18n="endFrame">结束帧 (尾帧)</label>
|
| 230 |
-
<div class="upload-zone" id="end-frame-drop-zone" onclick="document.getElementById('end-frame-input').click()">
|
| 231 |
-
<div class="clear-img-overlay" id="clear-end-frame-overlay" onclick="event.stopPropagation(); clearFrame('end')">×</div>
|
| 232 |
-
<div id="end-frame-placeholder">
|
| 233 |
-
<div class="upload-icon">🏁</div>
|
| 234 |
-
<div class="upload-text" data-i18n="uploadEnd">上传尾帧 (可选)</div>
|
| 235 |
-
</div>
|
| 236 |
-
<img id="end-frame-preview" class="preview-thumb">
|
| 237 |
-
<input type="file" id="end-frame-input" accept="image/*" style="display:none" onchange="handleFrameUpload(this.files[0], 'end')">
|
| 238 |
-
</div>
|
| 239 |
-
<input type="hidden" id="end-frame-path">
|
| 240 |
-
</div>
|
| 241 |
-
</div>
|
| 242 |
-
|
| 243 |
-
<div class="flex-row">
|
| 244 |
-
<div class="flex-1">
|
| 245 |
-
<label data-i18n="refAudio">参考音频 (A2V)</label>
|
| 246 |
-
<div class="upload-zone" id="audio-drop-zone" onclick="document.getElementById('vid-audio-input').click()">
|
| 247 |
-
<div class="clear-img-overlay" id="clear-audio-overlay" onclick="event.stopPropagation(); clearUploadedAudio()">×</div>
|
| 248 |
-
<div id="audio-upload-placeholder">
|
| 249 |
-
<div class="upload-icon">🎵</div>
|
| 250 |
-
<div class="upload-text" data-i18n="uploadAudio">点击上传音频</div>
|
| 251 |
-
</div>
|
| 252 |
-
<div id="audio-upload-status" style="display:none;">
|
| 253 |
-
<div class="upload-icon" style="color:var(--accent); opacity:1;">✔️</div>
|
| 254 |
-
<div id="audio-filename-status" class="upload-text"></div>
|
| 255 |
-
</div>
|
| 256 |
-
<input type="file" id="vid-audio-input" accept="audio/*" style="display:none" onchange="handleAudioUpload(this.files[0])">
|
| 257 |
-
</div>
|
| 258 |
-
<input type="hidden" id="uploaded-audio-path">
|
| 259 |
-
</div>
|
| 260 |
-
</div>
|
| 261 |
-
<div style="font-size: 10px; color: var(--text-dim); text-align: center; margin-top: 5px;" data-i18n="sourceHint">
|
| 262 |
-
💡 若仅上传首帧 = 图生视频/音视频;若同时上传首尾帧 = 首尾插帧。
|
| 263 |
-
</div>
|
| 264 |
-
</div>
|
| 265 |
-
</div>
|
| 266 |
-
|
| 267 |
-
<!-- 图像模式选项 -->
|
| 268 |
-
<div class="sidebar-section" id="motion-opts" style="display:none">
|
| 269 |
-
<div class="setting-group">
|
| 270 |
-
<div>
|
| 271 |
-
<label data-i18n="motionTransferModeLabel">迁移类型</label>
|
| 272 |
-
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-top:6px;">
|
| 273 |
-
<button type="button" id="motion-mode-action" class="btn-outline" onclick="setVideoTransferMode('action')" style="height:32px;background:var(--accent);color:#fff;border-color:var(--accent);" data-i18n="motionModeAction">动作迁移</button>
|
| 274 |
-
<button type="button" id="motion-mode-camera" class="btn-outline" onclick="setVideoTransferMode('camera')" style="height:32px;" data-i18n="motionModeCamera">运镜迁移</button>
|
| 275 |
-
<button type="button" id="motion-mode-repaint" class="btn-outline" onclick="setVideoTransferMode('repaint')" style="height:32px;" data-i18n="motionModeRepaint">视频重绘</button>
|
| 276 |
-
</div>
|
| 277 |
-
</div>
|
| 278 |
-
|
| 279 |
-
<div id="motion-action-control-section" style="margin-top: 12px;">
|
| 280 |
-
<label data-i18n="motionControlType">控制类型</label>
|
| 281 |
-
<select id="motion-conditioning-type">
|
| 282 |
-
<option value="pose" selected data-i18n="motionControlPose">Pose 姿态</option>
|
| 283 |
-
<option value="canny" data-i18n="motionControlCanny">Canny 轮廓</option>
|
| 284 |
-
<option value="depth" data-i18n="motionControlDepth">Depth 深度</option>
|
| 285 |
-
<option value="video" hidden>Video</option>
|
| 286 |
-
</select>
|
| 287 |
-
</div>
|
| 288 |
-
|
| 289 |
-
<label style="margin-top: 12px;" data-i18n="motionRefVideoLabel">参考动作视频</label>
|
| 290 |
-
<div class="upload-zone" id="motion-video-drop-zone" onclick="document.getElementById('motion-video-input').click()" style="height: 96px;">
|
| 291 |
-
<div class="clear-img-overlay" id="clear-motion-video-overlay" onclick="event.stopPropagation(); clearMotionVideo()">×</div>
|
| 292 |
-
<div id="motion-video-placeholder">
|
| 293 |
-
<div class="upload-icon">🎬</div>
|
| 294 |
-
<div data-i18n="motionVideoUploadText">点击或拖拽视频</div>
|
| 295 |
-
<small data-i18n="motionVideoUploadHint">提取动作/轮廓作为控制</small>
|
| 296 |
-
</div>
|
| 297 |
-
<div id="motion-video-status" style="display:none; padding: 10px; text-align:center;">
|
| 298 |
-
<div style="font-size: 22px;">🎞️</div>
|
| 299 |
-
<div id="motion-video-name" style="font-size: 11px; color: var(--text-sub); word-break: break-all;"></div>
|
| 300 |
-
</div>
|
| 301 |
-
</div>
|
| 302 |
-
<input type="file" id="motion-video-input" accept="video/*" hidden onchange="handleMotionVideoUpload(this.files[0])">
|
| 303 |
-
<input type="hidden" id="motion-video-path">
|
| 304 |
-
|
| 305 |
-
<div id="motion-image-section">
|
| 306 |
-
<label style="margin-top: 12px;" data-i18n="motionTargetImageLabel">目标主体图</label>
|
| 307 |
-
<div class="upload-zone" id="motion-image-drop-zone" onclick="document.getElementById('motion-image-input').click()" style="height: 130px;">
|
| 308 |
-
<div class="clear-img-overlay" id="clear-motion-image-overlay" onclick="event.stopPropagation(); clearMotionImage()">×</div>
|
| 309 |
-
<div id="motion-image-placeholder">
|
| 310 |
-
<div class="upload-icon">🖼️</div>
|
| 311 |
-
<div data-i18n="motionImageUploadText">点击或拖拽图片</div>
|
| 312 |
-
<small data-i18n="motionImageUploadHint">作为主体/首帧引导</small>
|
| 313 |
-
</div>
|
| 314 |
-
<img id="motion-image-preview" style="display:none; width:100%; height:100%; object-fit:cover; border-radius:10px;">
|
| 315 |
-
</div>
|
| 316 |
-
<input type="file" id="motion-image-input" accept="image/*" hidden onchange="handleMotionImageUpload(this.files[0])">
|
| 317 |
-
<input type="hidden" id="motion-image-path">
|
| 318 |
-
</div>
|
| 319 |
-
|
| 320 |
-
<div style="margin-top: 12px;">
|
| 321 |
-
<label style="display:flex;justify-content:space-between;align-items:center;">
|
| 322 |
-
<span id="motion-strength-label" data-i18n="motionControlStrength">控制强度</span>
|
| 323 |
-
<span id="motion-strength-val" style="color:var(--accent);font-size:11px;font-weight:800;">1</span>
|
| 324 |
-
</label>
|
| 325 |
-
<input type="range" id="motion-strength" value="1" min="0" max="2" step="0.05" style="width:100%;" oninput="document.getElementById('motion-strength-val').textContent = Number(this.value).toFixed(2).replace(/\.?0+$/, '')">
|
| 326 |
-
</div>
|
| 327 |
-
</div>
|
| 328 |
-
</div>
|
| 329 |
-
|
| 330 |
-
<div id="image-opts" class="sidebar-section" style="display:none">
|
| 331 |
-
<label data-i18n="imgPreset">预设分辨率 (Presets)</label>
|
| 332 |
-
<select id="img-res-preset" onchange="applyImgPreset(this.value)">
|
| 333 |
-
<option value="1024x1024" data-i18n="imgOptSquare">1:1 Square (1024x1024)</option>
|
| 334 |
-
<option value="1280x720" data-i18n="imgOptLand">16:9 Landscape (1280x720)</option>
|
| 335 |
-
<option value="720x1280" data-i18n="imgOptPort">9:16 Portrait (720x1280)</option>
|
| 336 |
-
<option value="custom" data-i18n="imgOptCustom">Custom 自定义...</option>
|
| 337 |
-
</select>
|
| 338 |
-
|
| 339 |
-
<div id="img-custom-res" class="flex-row" style="margin-top: 10px;">
|
| 340 |
-
<div class="flex-1"><label data-i18n="width">宽度</label><input type="number" id="img-w" value="1024" onchange="updateImgResPreview()"></div>
|
| 341 |
-
<div class="flex-1"><label data-i18n="height">高度</label><input type="number" id="img-h" value="1024" onchange="updateImgResPreview()"></div>
|
| 342 |
-
</div>
|
| 343 |
-
<div id="img-res-preview" class="res-preview-tag">最终发送: 1024x1024</div>
|
| 344 |
-
|
| 345 |
-
<div class="label-group" style="margin-top: 15px;">
|
| 346 |
-
<label data-i18n="samplingSteps">采样步数 (Steps)</label>
|
| 347 |
-
<span class="val-badge" id="stepsVal">28</span>
|
| 348 |
-
</div>
|
| 349 |
-
<div class="slider-container">
|
| 350 |
-
<input type="range" id="img-steps" min="1" max="50" value="28" oninput="document.getElementById('stepsVal').innerText=this.value">
|
| 351 |
-
</div>
|
| 352 |
-
</div>
|
| 353 |
-
|
| 354 |
-
<!-- 智能多帧模式 -->
|
| 355 |
-
<div class="sidebar-section" id="batch-opts" style="display:none">
|
| 356 |
-
<div class="setting-group">
|
| 357 |
-
<div class="group-title" data-i18n="smartMultiFrameGroup">智能多帧</div>
|
| 358 |
-
<div class="smart-param-mode-label" data-i18n="workflowModeLabel">工作流模式(点击切换)</div>
|
| 359 |
-
<div class="smart-param-modes" role="radiogroup" aria-label="工作流模式">
|
| 360 |
-
<label class="smart-param-mode-opt">
|
| 361 |
-
<input type="radio" name="batch-workflow" value="single" checked onchange="onBatchWorkflowChange()">
|
| 362 |
-
<span class="smart-param-mode-title" data-i18n="wfSingle">单次多关键帧</span>
|
| 363 |
-
</label>
|
| 364 |
-
<label class="smart-param-mode-opt">
|
| 365 |
-
<input type="radio" name="batch-workflow" value="segments" onchange="onBatchWorkflowChange()">
|
| 366 |
-
<span class="smart-param-mode-title" data-i18n="wfSegments">分段拼接</span>
|
| 367 |
-
</label>
|
| 368 |
-
</div>
|
| 369 |
-
|
| 370 |
-
<label data-i18n="uploadImages">上传图片</label>
|
| 371 |
-
<div class="upload-zone" id="batch-images-drop-zone" onclick="document.getElementById('batch-images-input').click()" style="min-height: 72px; margin-bottom: 0;">
|
| 372 |
-
<div id="batch-images-placeholder">
|
| 373 |
-
<div class="upload-icon">📁</div>
|
| 374 |
-
<div class="upload-text" data-i18n="uploadMulti1">点击或拖入多张图片</div>
|
| 375 |
-
<div class="upload-hint" data-i18n="uploadMulti2">支持一次选多张,可多次添加</div>
|
| 376 |
-
</div>
|
| 377 |
-
<input type="file" id="batch-images-input" accept="image/*" multiple style="display:none" onchange="handleBatchImagesUpload(this.files, true)">
|
| 378 |
-
</div>
|
| 379 |
-
<input type="hidden" id="batch-images-path">
|
| 380 |
-
|
| 381 |
-
<div class="batch-thumb-strip-wrap" id="batch-thumb-strip-wrap" style="display: none;">
|
| 382 |
-
<div class="batch-thumb-strip-head">
|
| 383 |
-
<span class="batch-thumb-strip-title" data-i18n="batchStripTitle">已选图片 · 顺序 = 播放先后</span>
|
| 384 |
-
<span class="batch-thumb-strip-hint" data-i18n="batchStripHint">在缩略图上按住拖动排序;松手落入虚线框位置</span>
|
| 385 |
-
</div>
|
| 386 |
-
<div class="batch-images-container" id="batch-images-container"></div>
|
| 387 |
-
</div>
|
| 388 |
-
|
| 389 |
-
<div style="font-size: 10px; color: var(--text-dim); margin-bottom: 12px; margin-top: 10px; line-height: 1.45;" data-i18n-html="batchFfmpegHint">
|
| 390 |
-
💡 <strong>分段模式</strong>:2 张 = 1 段;3 张 = 2 段再拼接。<strong>单次模式</strong>:几张图就几个 latent 锚点,一条视频出片。<br>
|
| 391 |
-
多段需 <code style="font-size:9px;">ffmpeg</code>:装好后加 PATH,或设环境变量 <code style="font-size:9px;">LTX_FFMPEG_PATH</code>,或在 <code style="font-size:9px;">%LOCALAPPDATA%\LTXDesktop\ffmpeg_path.txt</code> 第一行写 ffmpeg.exe 完整路径。
|
| 392 |
-
</div>
|
| 393 |
-
|
| 394 |
-
<label style="margin-top: 8px;" data-i18n="bgmLabel">成片配乐(可选,统一音轨)</label>
|
| 395 |
-
<div class="upload-zone" id="batch-audio-drop-zone" onclick="document.getElementById('batch-audio-input').click()" style="min-height: 44px; margin-bottom: 8px; position: relative;">
|
| 396 |
-
<div class="clear-img-overlay" id="clear-batch-audio-overlay" onclick="event.stopPropagation(); clearBatchBackgroundAudio()" style="display: none;">×</div>
|
| 397 |
-
<div id="batch-audio-placeholder">
|
| 398 |
-
<div class="upload-text" style="font-size: 11px;" data-i18n="bgmUploadHint">上传一条完整 BGM(生成完成后会替换整段成片的音轨)</div>
|
| 399 |
-
</div>
|
| 400 |
-
<div id="batch-audio-status" style="display: none; font-size: 11px; color: var(--accent);"></div>
|
| 401 |
-
<input type="file" id="batch-audio-input" accept="audio/*" style="display:none" onchange="handleBatchBackgroundAudioUpload(this.files[0])">
|
| 402 |
-
</div>
|
| 403 |
-
<input type="hidden" id="batch-background-audio-path">
|
| 404 |
-
|
| 405 |
-
<div id="batch-segments-container" style="margin-top: 15px;"></div>
|
| 406 |
-
</div>
|
| 407 |
-
|
| 408 |
-
<div class="setting-group">
|
| 409 |
-
<div class="group-title" data-i18n="basicEngine">基础画面 / Basic EngineSpecs</div>
|
| 410 |
-
<div class="flex-row">
|
| 411 |
-
<div class="flex-1">
|
| 412 |
-
<label data-i18n="qualityLevel">清晰度级别</label>
|
| 413 |
-
<select id="batch-quality" onchange="updateBatchResPreview()">
|
| 414 |
-
<option value="1080">1080P Full HD</option>
|
| 415 |
-
<option value="720" selected>720P Standard</option>
|
| 416 |
-
<option value="540">540P Preview</option>
|
| 417 |
-
</select>
|
| 418 |
-
</div>
|
| 419 |
-
<div class="flex-1">
|
| 420 |
-
<label data-i18n="aspectRatio">画幅比例</label>
|
| 421 |
-
<select id="batch-ratio" onchange="updateBatchResPreview()">
|
| 422 |
-
<option value="16:9" data-i18n="ratio169">16:9 电影宽幅</option>
|
| 423 |
-
<option value="9:16" data-i18n="ratio916">9:16 移动竖屏</option>
|
| 424 |
-
<option value="1:1" data-i18n="ratio11">1:1 方形</option>
|
| 425 |
-
<option value="4:3" data-i18n="ratio43">4:3 经典横幅</option>
|
| 426 |
-
<option value="3:4" data-i18n="ratio34">3:4 经典竖幅</option>
|
| 427 |
-
<option value="21:9" data-i18n="ratio219">21:9 超宽银幕</option>
|
| 428 |
-
<option value="9:21" data-i18n="ratio921">9:21 超长竖屏</option>
|
| 429 |
-
<option value="ref" data-i18n="ratioRef">跟随参考图</option>
|
| 430 |
-
<option value="custom" data-i18n="ratioCustom">自定义尺寸</option>
|
| 431 |
-
</select>
|
| 432 |
-
</div>
|
| 433 |
-
</div>
|
| 434 |
-
<div id="batch-custom-size" class="flex-row" style="display:none; margin-top: -2px; margin-bottom: 10px;">
|
| 435 |
-
<div class="flex-1"><label data-i18n="width">宽度</label><input type="number" id="batch-custom-w" value="1280" min="64" step="64" onchange="updateBatchResPreview()"></div>
|
| 436 |
-
<div class="flex-1"><label data-i18n="height">高度</label><input type="number" id="batch-custom-h" value="704" min="64" step="64" onchange="updateBatchResPreview()"></div>
|
| 437 |
-
</div>
|
| 438 |
-
<div id="batch-res-preview" class="res-preview-tag" style="margin-top: -5px; margin-bottom: 12px;">最终发送: 1280x704</div>
|
| 439 |
-
|
| 440 |
-
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
| 441 |
-
<label data-i18n="selectLora" style="margin: 0;">选择 LoRA</label>
|
| 442 |
-
<button type="button" onclick="addLoraSelection('batch-loras-container')" style="background:none; border:none; color:var(--accent); cursor:pointer; font-size:16px; padding:0 4px;" title="添加 LoRA">+</button>
|
| 443 |
-
</div>
|
| 444 |
-
<div id="batch-loras-container" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px;"></div>
|
| 445 |
-
</div>
|
| 446 |
-
</div>
|
| 447 |
-
|
| 448 |
-
<!-- TTS 语音合成面板 -->
|
| 449 |
-
<div id="tts-opts" class="sidebar-section" style="display:none; padding-top: 0;">
|
| 450 |
-
<!-- 状态栏 -->
|
| 451 |
-
<div id="tts-status-bar" style="padding: 8px 12px; border-radius: 8px; background: rgba(255,255,255,0.04); border: 1px solid var(--border); margin-bottom: 14px; font-size: 11px; color: var(--text-dim);" data-i18n="ttsStatusBarDetecting">
|
| 452 |
-
🔍 正在检测 TTS 模型...
|
| 453 |
-
</div>
|
| 454 |
-
|
| 455 |
-
<!-- 合成文本 -->
|
| 456 |
-
<div class="setting-group">
|
| 457 |
-
<div class="group-title" data-i18n="ttsTextTitle">合成文本 / Text</div>
|
| 458 |
-
<label style="font-size:11px; color:var(--text-dim); margin-bottom:5px; display:block;" data-i18n-html="ttsTextHint">
|
| 459 |
-
支持在文本开头加英文括号描述声音,例如:<code style="font-size:10px;">(年轻女声,温柔甜美)</code>
|
| 460 |
-
</label>
|
| 461 |
-
<textarea id="tts-text" data-i18n-placeholder="ttsTextPlaceholder" placeholder="输入要合成的文本内容..." style="width:100%; height:90px; padding:8px; font-size:12px; box-sizing:border-box; resize:vertical; border-radius:8px; border:1px solid var(--border); background:var(--item); color:var(--text);"></textarea>
|
| 462 |
-
</div>
|
| 463 |
-
|
| 464 |
-
<!-- 合成模式 -->
|
| 465 |
-
<div class="setting-group">
|
| 466 |
-
<div class="group-title" data-i18n="ttsModeTitle">合成模式 / Mode</div>
|
| 467 |
-
<select id="tts-mode" onchange="onTtsModeChange()" style="margin-bottom:8px;">
|
| 468 |
-
<option value="text_only" data-i18n="ttsModeTextOnly">🗣️ 文字转语音(含声音设计)</option>
|
| 469 |
-
<option value="clone" data-i18n="ttsModeClone">🎙️ 声音克隆</option>
|
| 470 |
-
<option value="ultimate_clone" data-i18n="ttsModeUltimate">⭐ 终极克隆(最高还原度)</option>
|
| 471 |
-
</select>
|
| 472 |
-
|
| 473 |
-
<!-- 声音克隆:参考音频 -->
|
| 474 |
-
<div id="tts-ref-section" style="display:none;">
|
| 475 |
-
<label style="font-size:11px; margin-bottom:4px; display:block;" data-i18n="ttsRefLabel">📎 参考音频(Reference)</label>
|
| 476 |
-
<div class="upload-zone" id="tts-ref-drop" onclick="document.getElementById('tts-ref-input').click()" style="min-height:48px; margin-bottom:6px; position:relative;">
|
| 477 |
-
<div class="clear-img-overlay" id="tts-ref-clear" onclick="event.stopPropagation(); clearTtsRef()" style="display:none;">×</div>
|
| 478 |
-
<div id="tts-ref-placeholder">
|
| 479 |
-
<div class="upload-icon">🎵</div>
|
| 480 |
-
<div class="upload-text" style="font-size:11px;" data-i18n="ttsRefUploadHint">点击上传参考音频 (.wav / .mp3)</div>
|
| 481 |
-
</div>
|
| 482 |
-
<div id="tts-ref-status" style="display:none; font-size:11px; color:var(--accent); padding:8px; text-align:center;"></div>
|
| 483 |
-
<input type="file" id="tts-ref-input" accept="audio/*" style="display:none" onchange="handleTtsRefUpload(this.files[0])">
|
| 484 |
-
</div>
|
| 485 |
-
</div>
|
| 486 |
-
|
| 487 |
-
<!-- 终极克隆额外选项 -->
|
| 488 |
-
<div id="tts-ultimate-section" style="display:none;">
|
| 489 |
-
<label style="font-size:11px; margin-bottom:4px; display:block;" data-i18n="ttsUltimateLabel">📝 参考音频对应的文本转录(可选,能显著提升相似度)</label>
|
| 490 |
-
<textarea id="tts-prompt-text" data-i18n-placeholder="ttsUltimatePlaceholder" placeholder="与参考音频完全一致的文本内容..." style="width:100%; height:60px; padding:6px 8px; font-size:11px; box-sizing:border-box; resize:vertical; border-radius:6px; border:1px solid var(--border); background:var(--item); color:var(--text); margin-bottom:6px;"></textarea>
|
| 491 |
-
</div>
|
| 492 |
-
</div>
|
| 493 |
-
|
| 494 |
-
<!-- 参数调节 -->
|
| 495 |
-
<div class="setting-group">
|
| 496 |
-
<div class="group-title" data-i18n="ttsParamsTitle">高级参数 / Parameters</div>
|
| 497 |
-
<div class="flex-row" style="gap:12px;">
|
| 498 |
-
<div class="flex-1">
|
| 499 |
-
<div class="label-group">
|
| 500 |
-
<label style="font-size:11px;" data-i18n="ttsCfgLabel">CFG 强度</label>
|
| 501 |
-
<span class="val-badge" id="ttsCfgVal">2.0</span>
|
| 502 |
-
</div>
|
| 503 |
-
<div class="slider-container">
|
| 504 |
-
<input type="range" id="tts-cfg" min="0.5" max="5.0" step="0.5" value="2.0"
|
| 505 |
-
oninput="document.getElementById('ttsCfgVal').textContent=this.value">
|
| 506 |
-
</div>
|
| 507 |
-
</div>
|
| 508 |
-
<div class="flex-1">
|
| 509 |
-
<div class="label-group">
|
| 510 |
-
<label style="font-size:11px;" data-i18n="ttsStepsLabel">推理步数</label>
|
| 511 |
-
<span class="val-badge" id="ttsStepsVal">10</span>
|
| 512 |
-
</div>
|
| 513 |
-
<div class="slider-container">
|
| 514 |
-
<input type="range" id="tts-steps" min="5" max="50" step="5" value="10"
|
| 515 |
-
oninput="document.getElementById('ttsStepsVal').textContent=this.value">
|
| 516 |
-
</div>
|
| 517 |
-
</div>
|
| 518 |
-
</div>
|
| 519 |
-
</div>
|
| 520 |
-
|
| 521 |
-
<!-- 输出结果播放区 -->
|
| 522 |
-
<div id="tts-result-section" style="display:none;" class="setting-group">
|
| 523 |
-
<div class="group-title" data-i18n="ttsResultTitle">生成结果 / Output</div>
|
| 524 |
-
<audio id="tts-audio-player" controls style="width:100%; border-radius:8px; margin-bottom:6px;"></audio>
|
| 525 |
-
<a id="tts-download-link" href="#" download style="font-size:11px; color:var(--accent); text-decoration:none;" data-i18n="ttsDownload">⬇️ 下载音频</a>
|
| 526 |
-
</div>
|
| 527 |
-
|
| 528 |
-
<div style="padding: 0 0 10px 0;">
|
| 529 |
-
<button class="btn-primary" id="tts-gen-btn" onclick="runTts()" data-i18n="ttsGenBtn">🎙️ 开始生成语音</button>
|
| 530 |
-
</div>
|
| 531 |
-
</div>
|
| 532 |
-
|
| 533 |
-
<div style="padding: 0 30px 30px 30px;">
|
| 534 |
-
<button class="btn-primary" id="mainBtn" onclick="run()" data-i18n="mainRender">开始渲染</button>
|
| 535 |
-
</div>
|
| 536 |
-
</aside>
|
| 537 |
-
|
| 538 |
-
<main class="workspace">
|
| 539 |
-
<section class="viewer" id="viewer-section">
|
| 540 |
-
<div class="monitor" id="viewer">
|
| 541 |
-
<button id="preview-download-btn" type="button" onclick="downloadCurrentPreviewAsset()" class="preview-download-btn" style="display:none;">
|
| 542 |
-
<span class="preview-download-btn-icon" aria-hidden="true">
|
| 543 |
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
| 544 |
-
<path d="M12 3v12"></path>
|
| 545 |
-
<path d="M7 10l5 5 5-5"></path>
|
| 546 |
-
<path d="M5 21h14"></path>
|
| 547 |
-
</svg>
|
| 548 |
-
</span>
|
| 549 |
-
<span class="preview-download-btn-text" data-i18n="downloadLabel">下载</span>
|
| 550 |
-
</button>
|
| 551 |
-
<div id="preview-replay-actions" class="preview-replay-actions" style="display:none;">
|
| 552 |
-
<button type="button" onclick="loadCurrentPreviewSeed()" data-i18n="previewLoadSeed">载入种子</button>
|
| 553 |
-
<button type="button" onclick="loadCurrentPreviewParams()" data-i18n="previewLoadParams">载入参数</button>
|
| 554 |
-
</div>
|
| 555 |
-
<div id="loading-txt" data-i18n="waitingTask">等待分配渲染任务...</div>
|
| 556 |
-
<img id="res-img" src="">
|
| 557 |
-
<div id="video-wrapper" style="width:100%; height:100%; display:none; max-height:100%; align-items:center; justify-content:center;">
|
| 558 |
-
<video id="res-video" autoplay loop playsinline></video>
|
| 559 |
-
</div>
|
| 560 |
-
<div id="audio-wrapper">
|
| 561 |
-
<div class="audio-preview-art" id="audio-preview-art" role="button" tabindex="0">
|
| 562 |
-
<div class="audio-preview-icon">♪</div>
|
| 563 |
-
<div id="audio-preview-title">TTS Audio</div>
|
| 564 |
-
</div>
|
| 565 |
-
<audio id="res-audio" controls></audio>
|
| 566 |
-
</div>
|
| 567 |
-
<div class="progress-container"><div id="progress-fill"></div></div>
|
| 568 |
-
</div>
|
| 569 |
-
</section>
|
| 570 |
-
|
| 571 |
-
<!-- Drag Handle -->
|
| 572 |
-
<div id="resize-handle" style="
|
| 573 |
-
height: 5px; background: transparent; cursor: row-resize;
|
| 574 |
-
flex-shrink: 0; position: relative; z-index: 50;
|
| 575 |
-
display: flex; align-items: center; justify-content: center;
|
| 576 |
-
" data-i18n-title="resizeHandleTitle" title="拖动调整面板高度">
|
| 577 |
-
<div style="width: 40px; height: 3px; background: var(--border); border-radius: 999px; pointer-events: none;"></div>
|
| 578 |
-
</div>
|
| 579 |
-
|
| 580 |
-
<section class="library" id="library-section">
|
| 581 |
-
<div style="display: flex; justify-content: space-between; margin-bottom: 15px; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 10px;">
|
| 582 |
-
<div style="display: flex; gap: 20px;">
|
| 583 |
-
<span id="tab-history" style="font-size: 11px; font-weight: 800; color: var(--accent); cursor: pointer; border-bottom: 2px solid var(--accent); padding-bottom: 11px; margin-bottom: -11px;" onclick="switchLibTab('history')" data-i18n="libHistory">历史资产 / ASSETS</span>
|
| 584 |
-
<span id="tab-log" style="font-size: 11px; font-weight: 800; color: var(--text-dim); cursor: pointer; border-bottom: 2px solid transparent; padding-bottom: 11px; margin-bottom: -11px;" onclick="switchLibTab('log')" data-i18n="libLog">系统日志 / LOGS</span>
|
| 585 |
-
</div>
|
| 586 |
-
<button type="button" onclick="fetchHistory(currentHistoryPage)" style="background: var(--item); border: 1px solid var(--border); border-radius: 6px; color: var(--text-dim); font-size: 11px; padding: 4px 10px; cursor: pointer;" data-i18n="refresh">刷新</button>
|
| 587 |
-
</div>
|
| 588 |
-
|
| 589 |
-
<div id="log-container" style="display: none; flex: 1; flex-direction: column;">
|
| 590 |
-
<div id="log" data-i18n="logReady">> LTX-2 Studio Ready. Expecting commands...</div>
|
| 591 |
-
</div>
|
| 592 |
-
|
| 593 |
-
<div id="history-wrapper">
|
| 594 |
-
<div id="history-container"></div>
|
| 595 |
-
</div>
|
| 596 |
-
<div id="pagination-bar" style="display:none;"></div>
|
| 597 |
-
</section>
|
| 598 |
-
</main>
|
| 599 |
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/plyr/3.7.8/plyr.min.js"></script>
|
| 600 |
-
<script src="i18n.js?v=en-tabs-1"></script>
|
| 601 |
-
<script src="index.js?v=model-switch-1"></script>
|
| 602 |
-
|
| 603 |
-
</body>
|
| 604 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/UI/index.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
LTX2.3-1.0.4-new/main.py
DELETED
|
@@ -1,266 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import sys
|
| 3 |
-
import subprocess
|
| 4 |
-
import threading
|
| 5 |
-
import time
|
| 6 |
-
import socket
|
| 7 |
-
import logging
|
| 8 |
-
from fastapi import FastAPI
|
| 9 |
-
from fastapi.responses import FileResponse
|
| 10 |
-
from fastapi.staticfiles import StaticFiles
|
| 11 |
-
import uvicorn
|
| 12 |
-
|
| 13 |
-
# ============================================================
|
| 14 |
-
# 配置区 (动态路径适配与补丁挂载)
|
| 15 |
-
# ============================================================
|
| 16 |
-
def resolve_ltx_path():
|
| 17 |
-
import glob, tempfile, subprocess
|
| 18 |
-
sc_dir = os.path.join(os.getcwd(), "LTX_Shortcut")
|
| 19 |
-
os.makedirs(sc_dir, exist_ok=True)
|
| 20 |
-
lnk_files = glob.glob(os.path.join(sc_dir, "*.lnk"))
|
| 21 |
-
if not lnk_files:
|
| 22 |
-
print("\033[91m[ERROR] 未在 LTX_Shortcut 文件夹中找到快捷方式!\n请打开程序目录下的 LTX_Shortcut 文件夹,并将官方 LTX Desktop 的快捷方式复制进去后重试。\033[0m")
|
| 23 |
-
sys.exit(1)
|
| 24 |
-
|
| 25 |
-
lnk_path = lnk_files[0]
|
| 26 |
-
# 使用 VBScript 解析快捷方式,兼容所有 Windows 系统
|
| 27 |
-
vbs_code = f'''Set sh = CreateObject("WScript.Shell")\nSet obj = sh.CreateShortcut("{os.path.abspath(lnk_path)}")\nWScript.Echo obj.TargetPath'''
|
| 28 |
-
fd, vbs_path = tempfile.mkstemp(suffix='.vbs')
|
| 29 |
-
with os.fdopen(fd, 'w') as f:
|
| 30 |
-
f.write(vbs_code)
|
| 31 |
-
try:
|
| 32 |
-
out = subprocess.check_output(['cscript', '//nologo', vbs_path], stderr=subprocess.STDOUT)
|
| 33 |
-
target_exe = out.decode('ansi').strip()
|
| 34 |
-
finally:
|
| 35 |
-
os.remove(vbs_path)
|
| 36 |
-
|
| 37 |
-
if not target_exe or not os.path.exists(target_exe):
|
| 38 |
-
# 如果快捷方式解析失败,或者解析出来的是朋友电脑的路径(当前电脑不存在),自动全盘搜索默认路径
|
| 39 |
-
default_paths = [
|
| 40 |
-
os.path.join(os.environ.get("LOCALAPPDATA", ""), r"Programs\LTX Desktop\LTX Desktop.exe"),
|
| 41 |
-
r"C:\Program Files\LTX Desktop\LTX Desktop.exe",
|
| 42 |
-
r"D:\Program Files\LTX Desktop\LTX Desktop.exe",
|
| 43 |
-
r"E:\Program Files\LTX Desktop\LTX Desktop.exe"
|
| 44 |
-
]
|
| 45 |
-
found = False
|
| 46 |
-
for p in default_paths:
|
| 47 |
-
if os.path.exists(p):
|
| 48 |
-
target_exe = p
|
| 49 |
-
print(f"\033[96m[INFO] 自动检测到 LTX 原版安装路径: {p}\033[0m")
|
| 50 |
-
found = True
|
| 51 |
-
break
|
| 52 |
-
|
| 53 |
-
if not found:
|
| 54 |
-
print(f"\033[91m[ERROR] 未能找到原版 LTX Desktop 的安装路径!\033[0m")
|
| 55 |
-
print("请清理 LTX_Shortcut 文件夹,并将您当前电脑上真正的原版快捷方式重贴复制进去。")
|
| 56 |
-
sys.exit(1)
|
| 57 |
-
|
| 58 |
-
return os.path.dirname(target_exe)
|
| 59 |
-
|
| 60 |
-
USER_PROFILE = os.path.expanduser("~")
|
| 61 |
-
PYTHON_EXE = os.path.join(USER_PROFILE, r"AppData\Local\LTXDesktop\python\python.exe")
|
| 62 |
-
DATA_DIR = os.path.join(USER_PROFILE, r"AppData\Local\LTXDesktop")
|
| 63 |
-
|
| 64 |
-
# 1. 动态获取主安装路径
|
| 65 |
-
LTX_INSTALL_DIR = resolve_ltx_path()
|
| 66 |
-
BACKEND_DIR = os.path.join(LTX_INSTALL_DIR, r"resources\backend")
|
| 67 |
-
UI_FILE_NAME = "UI/index.html"
|
| 68 |
-
|
| 69 |
-
# 环境致命检测:如果官方 Python 还没解压释放,立刻强制中断整个程序
|
| 70 |
-
if not os.path.exists(PYTHON_EXE):
|
| 71 |
-
print(f"\n\033[1;41m [致命错误] 您的电脑上尚未配置好 LTX 的官方渲染核心框架! \033[0m")
|
| 72 |
-
print(f"\033[93m此应用仅是 UI 图形控制台,必需依赖原版软件环境才能生成。在 ({PYTHON_EXE}) 未找到运行引擎。\n")
|
| 73 |
-
print(">> 解决方案:\n1. 请先在您的电脑上正常安装【LTX Desktop 官方原版软件】。")
|
| 74 |
-
print("2. 必需:双击打开运行一次原版软件!(运行后原版软件会在后台自动释放环境)")
|
| 75 |
-
print("3. 把原版软件的快捷方式复制到本文档的 LTX_Shortcut 文件夹里面。")
|
| 76 |
-
print("4. 全部完成后,再重新启动本 run.bat 脚本即可!\033[0m\n")
|
| 77 |
-
os._exit(1)
|
| 78 |
-
|
| 79 |
-
# 2. 从目录读取改动过的 Python 文件 (热修复拦截器)
|
| 80 |
-
PATCHES_DIR = os.path.join(os.getcwd(), "patches")
|
| 81 |
-
os.makedirs(PATCHES_DIR, exist_ok=True)
|
| 82 |
-
|
| 83 |
-
# 3. 默认输出定向至程序根目录
|
| 84 |
-
LOCAL_OUTPUTS = os.path.join(os.getcwd(), "outputs")
|
| 85 |
-
os.makedirs(LOCAL_OUTPUTS, exist_ok=True)
|
| 86 |
-
|
| 87 |
-
# 强制注入自定义输出录至 LTX 缓存数据中
|
| 88 |
-
os.makedirs(DATA_DIR, exist_ok=True)
|
| 89 |
-
with open(os.path.join(DATA_DIR, "custom_dir.txt"), 'w', encoding='utf-8') as f:
|
| 90 |
-
f.write(LOCAL_OUTPUTS)
|
| 91 |
-
|
| 92 |
-
os.environ["LTX_APP_DATA_DIR"] = DATA_DIR
|
| 93 |
-
|
| 94 |
-
# 将 patches 目录优先级提升,做到 Python 无损替换
|
| 95 |
-
os.environ["PYTHONPATH"] = f"{PATCHES_DIR};{BACKEND_DIR}"
|
| 96 |
-
|
| 97 |
-
def get_lan_ip():
|
| 98 |
-
try:
|
| 99 |
-
host_name = socket.gethostname()
|
| 100 |
-
_, _, ip_list = socket.gethostbyname_ex(host_name)
|
| 101 |
-
|
| 102 |
-
candidates = []
|
| 103 |
-
for ip in ip_list:
|
| 104 |
-
if ip.startswith("192.168."):
|
| 105 |
-
return ip
|
| 106 |
-
elif ip.startswith("10.") or (ip.startswith("172.") and 16 <= int(ip.split('.')[1]) <= 31):
|
| 107 |
-
candidates.append(ip)
|
| 108 |
-
|
| 109 |
-
if candidates:
|
| 110 |
-
return candidates[0]
|
| 111 |
-
|
| 112 |
-
# Fallback to the default socket routing approach if no obvious LAN IP found
|
| 113 |
-
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
| 114 |
-
s.connect(("8.8.8.8", 80))
|
| 115 |
-
ip = s.getsockname()[0]
|
| 116 |
-
s.close()
|
| 117 |
-
return ip
|
| 118 |
-
except:
|
| 119 |
-
return "127.0.0.1"
|
| 120 |
-
|
| 121 |
-
LAN_IP = get_lan_ip()
|
| 122 |
-
|
| 123 |
-
# ============================================================
|
| 124 |
-
# 服务启动逻辑
|
| 125 |
-
# ============================================================
|
| 126 |
-
def check_port_in_use(port):
|
| 127 |
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
| 128 |
-
return s.connect_ex(('127.0.0.1', port)) == 0
|
| 129 |
-
|
| 130 |
-
def launch_backend():
|
| 131 |
-
"""启动核心引擎 - 监听 0.0.0.0 确保局域网可调"""
|
| 132 |
-
if check_port_in_use(3000):
|
| 133 |
-
print(f"\n\033[1;41m [致命错误] 3000 端口已被占用,无法启动核心引擎! \033[0m")
|
| 134 |
-
print("\033[93m>> 绝大多数情况下,这是因为【官方原版 LTX Desktop】正在您的电脑后台运行。\033[0m")
|
| 135 |
-
print(">> 冲突会导致显存爆炸。请检查右下角系统托盘图标,右键完全退出官方软件。")
|
| 136 |
-
print(">> 退出后重新双击 run.bat 启动本程序!\n")
|
| 137 |
-
os._exit(1)
|
| 138 |
-
|
| 139 |
-
print(f"\033[96m[CORE] 核心引擎正在启动...\033[0m")
|
| 140 |
-
# 只开启重要级别的 Python 应用层日志,去除无用的 HTTP 刷屏
|
| 141 |
-
import logging as _logging
|
| 142 |
-
_logging.basicConfig(
|
| 143 |
-
level=_logging.INFO,
|
| 144 |
-
format="[%(asctime)s] %(levelname)s %(name)s: %(message)s",
|
| 145 |
-
datefmt="%H:%M:%S",
|
| 146 |
-
force=True
|
| 147 |
-
)
|
| 148 |
-
|
| 149 |
-
# 构建绝对无损的环境拦截器:防止其他电脑被 cwd 劫持加载原版文件
|
| 150 |
-
launcher_code = f"""
|
| 151 |
-
import sys
|
| 152 |
-
import os
|
| 153 |
-
|
| 154 |
-
patch_dir = r"{PATCHES_DIR}"
|
| 155 |
-
backend_dir = r"{BACKEND_DIR}"
|
| 156 |
-
|
| 157 |
-
# 防御性清除:强行剥离所有的默认 backend_dir 引用
|
| 158 |
-
sys.path = [p for p in sys.path if p and os.path.normpath(p) != os.path.normpath(backend_dir)]
|
| 159 |
-
sys.path = [p for p in sys.path if p and p != "." and p != ""]
|
| 160 |
-
|
| 161 |
-
# 绝对插队注入:优先搜索 PATCHES_DIR
|
| 162 |
-
sys.path.insert(0, patch_dir)
|
| 163 |
-
sys.path.insert(1, backend_dir)
|
| 164 |
-
|
| 165 |
-
import uvicorn
|
| 166 |
-
from ltx2_server import app
|
| 167 |
-
|
| 168 |
-
if __name__ == '__main__':
|
| 169 |
-
uvicorn.run(app, host="0.0.0.0", port=3000, log_level="info", access_log=False)
|
| 170 |
-
"""
|
| 171 |
-
launcher_path = os.path.join(PATCHES_DIR, "launcher.py")
|
| 172 |
-
with open(launcher_path, "w", encoding="utf-8") as f:
|
| 173 |
-
f.write(launcher_code)
|
| 174 |
-
|
| 175 |
-
cmd = [PYTHON_EXE, launcher_path]
|
| 176 |
-
env = os.environ.copy()
|
| 177 |
-
result = subprocess.run(cmd, cwd=BACKEND_DIR, env=env)
|
| 178 |
-
if result.returncode != 0:
|
| 179 |
-
print(f"\n\033[1;41m [致命错误] 核心引擎异常崩溃退出! (Exit Code: {result.returncode})\033[0m")
|
| 180 |
-
print(">> 请检查上述终端报错信息。确认显卡驱动是否正常。")
|
| 181 |
-
os._exit(1)
|
| 182 |
-
|
| 183 |
-
ui_app = FastAPI()
|
| 184 |
-
# 已移除存在安全隐患的静态资源挂载目录
|
| 185 |
-
|
| 186 |
-
UI_NO_CACHE_HEADERS = {"Cache-Control": "no-store, max-age=0"}
|
| 187 |
-
|
| 188 |
-
@ui_app.get("/")
|
| 189 |
-
async def serve_index():
|
| 190 |
-
return FileResponse(os.path.join(os.getcwd(), UI_FILE_NAME), headers=UI_NO_CACHE_HEADERS)
|
| 191 |
-
|
| 192 |
-
@ui_app.get("/index.css")
|
| 193 |
-
async def serve_css():
|
| 194 |
-
return FileResponse(os.path.join(os.getcwd(), "UI/index.css"), headers=UI_NO_CACHE_HEADERS)
|
| 195 |
-
|
| 196 |
-
@ui_app.get("/index.js")
|
| 197 |
-
async def serve_js():
|
| 198 |
-
return FileResponse(os.path.join(os.getcwd(), "UI/index.js"), headers=UI_NO_CACHE_HEADERS)
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
@ui_app.get("/i18n.js")
|
| 202 |
-
async def serve_i18n():
|
| 203 |
-
return FileResponse(os.path.join(os.getcwd(), "UI/i18n.js"), headers=UI_NO_CACHE_HEADERS)
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
def launch_ui_server():
|
| 207 |
-
print(f"\033[92m[UI] 工作站已就绪!\033[0m")
|
| 208 |
-
print(f"\033[92m[LOCAL] 本机访问: http://127.0.0.1:4000\033[0m")
|
| 209 |
-
print(f"\033[93m[WIFI] 局域网访问: http://{LAN_IP}:4000\033[0m")
|
| 210 |
-
|
| 211 |
-
# 彻底压制 WinError 10054 (客户端强制断开) 的底层警告报错
|
| 212 |
-
if sys.platform == 'win32':
|
| 213 |
-
# Uvicorn 内部会拉起循环,所以只能通过底层 Logging Filter 拦截控制台噪音
|
| 214 |
-
class UvicornAsyncioNoiseFilter(logging.Filter):
|
| 215 |
-
"""压掉客户端断开、Win Proactor 管道收尾等无害 asyncio 控制台刷屏。"""
|
| 216 |
-
|
| 217 |
-
def filter(self, record):
|
| 218 |
-
if record.name != "asyncio":
|
| 219 |
-
return True
|
| 220 |
-
msg = record.getMessage()
|
| 221 |
-
if "_call_connection_lost" in msg or "_ProactorBasePipeTransport" in msg:
|
| 222 |
-
return False
|
| 223 |
-
if hasattr(record, "exc_info") and record.exc_info:
|
| 224 |
-
exc_type, exc_value, _ = record.exc_info
|
| 225 |
-
if isinstance(exc_value, ConnectionResetError) and getattr(
|
| 226 |
-
exc_value, "winerror", None
|
| 227 |
-
) == 10054:
|
| 228 |
-
return False
|
| 229 |
-
if "10054" in msg and "ConnectionResetError" in msg:
|
| 230 |
-
return False
|
| 231 |
-
return True
|
| 232 |
-
|
| 233 |
-
logging.getLogger("asyncio").addFilter(UvicornAsyncioNoiseFilter())
|
| 234 |
-
|
| 235 |
-
uvicorn.run(ui_app, host="0.0.0.0", port=4000, log_level="warning", access_log=False)
|
| 236 |
-
|
| 237 |
-
if __name__ == "__main__":
|
| 238 |
-
os.system('cls' if os.name == 'nt' else 'clear')
|
| 239 |
-
print("\033[1;97;44m LTX-2 CINEMATIC WORKSTATION | NETWORK ENABLED \033[0m\n")
|
| 240 |
-
|
| 241 |
-
threading.Thread(target=launch_backend, daemon=True).start()
|
| 242 |
-
|
| 243 |
-
# 强制校验 3000 端口是否存活
|
| 244 |
-
print("\033[93m[SYS] 正在等待内部核心 3000 端口启动...\033[0m")
|
| 245 |
-
backend_ready = False
|
| 246 |
-
for _ in range(30):
|
| 247 |
-
try:
|
| 248 |
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
| 249 |
-
if s.connect_ex(('127.0.0.1', 3000)) == 0:
|
| 250 |
-
backend_ready = True
|
| 251 |
-
break
|
| 252 |
-
except Exception:
|
| 253 |
-
pass
|
| 254 |
-
time.sleep(1)
|
| 255 |
-
|
| 256 |
-
if backend_ready:
|
| 257 |
-
print("\033[92m[SYS] 3000 端口已通过连通性握手验证!后端装载成功。\033[0m")
|
| 258 |
-
else:
|
| 259 |
-
print("\033[1;41m [崩坏警告] 等待 30 秒后,3000 端口依然无法连通! \033[0m")
|
| 260 |
-
print(">> Uvicorn 可能在后台陷入了死锁,或者被防火墙拦截,前端大概率将无法连接到后端!")
|
| 261 |
-
print(">> 请检查上方是否有 Python 报错。\n")
|
| 262 |
-
|
| 263 |
-
try:
|
| 264 |
-
launch_ui_server()
|
| 265 |
-
except KeyboardInterrupt:
|
| 266 |
-
sys.exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/API模式问题修复说明.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
# LTX 本地显卡模式修复
|
| 2 |
-
|
| 3 |
-
## 问题描述
|
| 4 |
-
系统强制使用 FAL API 生成图片,即使本地有 GPU 可用。
|
| 5 |
-
|
| 6 |
-
## 原因
|
| 7 |
-
LTX 强制要求 GPU 有 31GB VRAM 才会使用本地显卡,低于此值会强制走 API 模式。
|
| 8 |
-
|
| 9 |
-
## 修复方法
|
| 10 |
-
|
| 11 |
-
### 方法一:自动替换(推荐)
|
| 12 |
-
运行程序后,patches 目录中的文件会自动替换原版文件。
|
| 13 |
-
|
| 14 |
-
### 方法二:手动替换
|
| 15 |
-
|
| 16 |
-
#### 1. 修改 VRAM 阈值
|
| 17 |
-
- **原文件**: `C:\Program Files\LTX Desktop\resources\backend\runtime_config\runtime_policy.py`
|
| 18 |
-
- **找到** (第16行):
|
| 19 |
-
```python
|
| 20 |
-
return vram_gb < 31
|
| 21 |
-
```
|
| 22 |
-
- **改为**:
|
| 23 |
-
```python
|
| 24 |
-
return vram_gb < 6
|
| 25 |
-
```
|
| 26 |
-
|
| 27 |
-
#### 2. 清空无效 API Key
|
| 28 |
-
- **原文件**: `C:\Users\Administrator\AppData\Local\LTXDesktop\settings.json`
|
| 29 |
-
- **找到**:
|
| 30 |
-
```json
|
| 31 |
-
"fal_api_key": "12123",
|
| 32 |
-
```
|
| 33 |
-
- **改为**:
|
| 34 |
-
```json
|
| 35 |
-
"fal_api_key": "",
|
| 36 |
-
```
|
| 37 |
-
|
| 38 |
-
## 说明
|
| 39 |
-
- VRAM 阈值改为 6GB,意味着 6GB 及以上显存都会使用本地显卡
|
| 40 |
-
- 清空 fal_api_key 避免系统误判为已配置 API
|
| 41 |
-
- 修改后重启程序即可生效
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/__pycache__/api_types.cpython-313.pyc
DELETED
|
Binary file (16.2 kB)
|
|
|
LTX2.3-1.0.4-new/patches/__pycache__/app_factory.cpython-313.pyc
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:6a39240a5806a7be8c147f3fbd45dc7ea3f8c0a6a3550741d90423d4345335ee
|
| 3 |
-
size 143860
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/__pycache__/keep_models_runtime.cpython-313.pyc
DELETED
|
Binary file (894 Bytes)
|
|
|
LTX2.3-1.0.4-new/patches/__pycache__/lora_build_hook.cpython-313.pyc
DELETED
|
Binary file (8.77 kB)
|
|
|
LTX2.3-1.0.4-new/patches/__pycache__/lora_injection.cpython-313.pyc
DELETED
|
Binary file (5.19 kB)
|
|
|
LTX2.3-1.0.4-new/patches/__pycache__/low_vram_runtime.cpython-313.pyc
DELETED
|
Binary file (12.1 kB)
|
|
|
LTX2.3-1.0.4-new/patches/__pycache__/ltx_dev_video_pipeline.cpython-313.pyc
DELETED
|
Binary file (6.05 kB)
|
|
|
LTX2.3-1.0.4-new/patches/__pycache__/ltx_fp8_video_pipeline.cpython-313.pyc
DELETED
|
Binary file (11.1 kB)
|
|
|
LTX2.3-1.0.4-new/patches/__pycache__/tts_worker.cpython-313.pyc
DELETED
|
Binary file (11.3 kB)
|
|
|
LTX2.3-1.0.4-new/patches/api_types.py
DELETED
|
@@ -1,403 +0,0 @@
|
|
| 1 |
-
"""Pydantic request/response models and TypedDicts for ltx2_server."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from typing import Literal, NamedTuple, TypeAlias, TypedDict
|
| 6 |
-
from typing import Annotated
|
| 7 |
-
|
| 8 |
-
from pydantic import BaseModel, Field, StringConstraints
|
| 9 |
-
|
| 10 |
-
NonEmptyPrompt = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
|
| 11 |
-
ModelFileType = Literal[
|
| 12 |
-
"checkpoint",
|
| 13 |
-
"upsampler",
|
| 14 |
-
"distilled_lora",
|
| 15 |
-
"ic_lora",
|
| 16 |
-
"depth_processor",
|
| 17 |
-
"person_detector",
|
| 18 |
-
"pose_processor",
|
| 19 |
-
"text_encoder",
|
| 20 |
-
"zit",
|
| 21 |
-
]
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
class ImageConditioningInput(NamedTuple):
|
| 25 |
-
"""Image conditioning triplet used by all video pipelines."""
|
| 26 |
-
|
| 27 |
-
path: str
|
| 28 |
-
frame_idx: int
|
| 29 |
-
strength: float
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
# ============================================================
|
| 33 |
-
# TypedDicts for module-level state globals
|
| 34 |
-
# ============================================================
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
class GenerationState(TypedDict):
|
| 38 |
-
id: str | None
|
| 39 |
-
cancelled: bool
|
| 40 |
-
result: str | list[str] | None
|
| 41 |
-
error: str | None
|
| 42 |
-
status: str # "idle" | "running" | "complete" | "cancelled" | "error"
|
| 43 |
-
phase: str
|
| 44 |
-
progress: int
|
| 45 |
-
current_step: int
|
| 46 |
-
total_steps: int
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
JsonObject: TypeAlias = dict[str, object]
|
| 50 |
-
VideoCameraMotion = Literal[
|
| 51 |
-
"none",
|
| 52 |
-
"dolly_in",
|
| 53 |
-
"dolly_out",
|
| 54 |
-
"dolly_left",
|
| 55 |
-
"dolly_right",
|
| 56 |
-
"jib_up",
|
| 57 |
-
"jib_down",
|
| 58 |
-
"static",
|
| 59 |
-
"focus_shift",
|
| 60 |
-
]
|
| 61 |
-
|
| 62 |
-
RetakeMode: TypeAlias = Literal[
|
| 63 |
-
"replace_audio_and_video", "replace_video", "replace_audio"
|
| 64 |
-
]
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
# ============================================================
|
| 68 |
-
# Response Models
|
| 69 |
-
# ============================================================
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
class ModelStatusItem(BaseModel):
|
| 73 |
-
id: str
|
| 74 |
-
name: str
|
| 75 |
-
loaded: bool
|
| 76 |
-
downloaded: bool
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
class GpuTelemetry(BaseModel):
|
| 80 |
-
name: str
|
| 81 |
-
vram: int
|
| 82 |
-
vramUsed: int
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
class HealthResponse(BaseModel):
|
| 86 |
-
status: str
|
| 87 |
-
models_loaded: bool
|
| 88 |
-
active_model: str | None
|
| 89 |
-
gpu_info: GpuTelemetry
|
| 90 |
-
sage_attention: bool
|
| 91 |
-
models_status: list[ModelStatusItem]
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
class GpuInfoResponse(BaseModel):
|
| 95 |
-
cuda_available: bool
|
| 96 |
-
mps_available: bool = False
|
| 97 |
-
gpu_available: bool = False
|
| 98 |
-
gpu_name: str | None
|
| 99 |
-
vram_gb: int | None
|
| 100 |
-
gpu_info: GpuTelemetry
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
class RuntimePolicyResponse(BaseModel):
|
| 104 |
-
force_api_generations: bool
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
class GenerationProgressResponse(BaseModel):
|
| 108 |
-
status: str
|
| 109 |
-
phase: str
|
| 110 |
-
progress: int
|
| 111 |
-
currentStep: int | None
|
| 112 |
-
totalSteps: int | None
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
class ModelInfo(BaseModel):
|
| 116 |
-
id: str
|
| 117 |
-
name: str
|
| 118 |
-
description: str
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
class ModelFileStatus(BaseModel):
|
| 122 |
-
id: ModelFileType
|
| 123 |
-
name: str
|
| 124 |
-
description: str
|
| 125 |
-
downloaded: bool
|
| 126 |
-
size: int
|
| 127 |
-
expected_size: int
|
| 128 |
-
required: bool = True
|
| 129 |
-
is_folder: bool = False
|
| 130 |
-
optional_reason: str | None = None
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
class TextEncoderStatus(BaseModel):
|
| 134 |
-
downloaded: bool
|
| 135 |
-
size_bytes: int
|
| 136 |
-
size_gb: float
|
| 137 |
-
expected_size_gb: float
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
class ModelsStatusResponse(BaseModel):
|
| 141 |
-
models: list[ModelFileStatus]
|
| 142 |
-
all_downloaded: bool
|
| 143 |
-
total_size: int
|
| 144 |
-
downloaded_size: int
|
| 145 |
-
total_size_gb: float
|
| 146 |
-
downloaded_size_gb: float
|
| 147 |
-
models_path: str
|
| 148 |
-
has_api_key: bool
|
| 149 |
-
text_encoder_status: TextEncoderStatus
|
| 150 |
-
use_local_text_encoder: bool
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
class DownloadProgressRunningResponse(BaseModel):
|
| 154 |
-
status: Literal["downloading"]
|
| 155 |
-
current_downloading_file: ModelFileType | None
|
| 156 |
-
current_file_progress: float
|
| 157 |
-
total_progress: float
|
| 158 |
-
total_downloaded_bytes: int
|
| 159 |
-
expected_total_bytes: int
|
| 160 |
-
completed_files: set[ModelFileType]
|
| 161 |
-
all_files: set[ModelFileType]
|
| 162 |
-
error: None = None
|
| 163 |
-
speed_bytes_per_sec: float
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
class DownloadProgressCompleteResponse(BaseModel):
|
| 167 |
-
status: Literal["complete"]
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
class DownloadProgressErrorResponse(BaseModel):
|
| 171 |
-
status: Literal["error"]
|
| 172 |
-
error: str
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
DownloadProgressResponse: TypeAlias = (
|
| 176 |
-
DownloadProgressRunningResponse
|
| 177 |
-
| DownloadProgressCompleteResponse
|
| 178 |
-
| DownloadProgressErrorResponse
|
| 179 |
-
)
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
class SuggestGapPromptResponse(BaseModel):
|
| 183 |
-
status: str = "success"
|
| 184 |
-
suggested_prompt: str
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
class GenerateVideoCompleteResponse(BaseModel):
|
| 188 |
-
status: Literal["complete"]
|
| 189 |
-
video_path: str
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
class GenerateVideoCancelledResponse(BaseModel):
|
| 193 |
-
status: Literal["cancelled"]
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
GenerateVideoResponse: TypeAlias = (
|
| 197 |
-
GenerateVideoCompleteResponse | GenerateVideoCancelledResponse
|
| 198 |
-
)
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
class GenerateImageCompleteResponse(BaseModel):
|
| 202 |
-
status: Literal["complete"]
|
| 203 |
-
image_paths: list[str]
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
class GenerateImageCancelledResponse(BaseModel):
|
| 207 |
-
status: Literal["cancelled"]
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
GenerateImageResponse: TypeAlias = (
|
| 211 |
-
GenerateImageCompleteResponse | GenerateImageCancelledResponse
|
| 212 |
-
)
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
class CancelCancellingResponse(BaseModel):
|
| 216 |
-
status: Literal["cancelling"]
|
| 217 |
-
id: str
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
class CancelNoActiveGenerationResponse(BaseModel):
|
| 221 |
-
status: Literal["no_active_generation"]
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
CancelResponse: TypeAlias = CancelCancellingResponse | CancelNoActiveGenerationResponse
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
class RetakeVideoResponse(BaseModel):
|
| 228 |
-
status: Literal["complete"]
|
| 229 |
-
video_path: str
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
class RetakePayloadResponse(BaseModel):
|
| 233 |
-
status: Literal["complete"]
|
| 234 |
-
result: JsonObject
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
class RetakeCancelledResponse(BaseModel):
|
| 238 |
-
status: Literal["cancelled"]
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
RetakeResponse: TypeAlias = (
|
| 242 |
-
RetakeVideoResponse | RetakePayloadResponse | RetakeCancelledResponse
|
| 243 |
-
)
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
class IcLoraExtractResponse(BaseModel):
|
| 247 |
-
conditioning: str
|
| 248 |
-
original: str
|
| 249 |
-
conditioning_type: Literal["canny", "depth", "pose", "video"]
|
| 250 |
-
frame_time: float
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
class IcLoraGenerateCompleteResponse(BaseModel):
|
| 254 |
-
status: Literal["complete"]
|
| 255 |
-
video_path: str
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
class IcLoraGenerateCancelledResponse(BaseModel):
|
| 259 |
-
status: Literal["cancelled"]
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
IcLoraGenerateResponse: TypeAlias = (
|
| 263 |
-
IcLoraGenerateCompleteResponse | IcLoraGenerateCancelledResponse
|
| 264 |
-
)
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
class ModelDownloadStartResponse(BaseModel):
|
| 268 |
-
status: Literal["started"]
|
| 269 |
-
message: str
|
| 270 |
-
sessionId: str
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
class TextEncoderDownloadStartedResponse(BaseModel):
|
| 274 |
-
status: Literal["started"]
|
| 275 |
-
message: str
|
| 276 |
-
sessionId: str
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
class TextEncoderAlreadyDownloadedResponse(BaseModel):
|
| 280 |
-
status: Literal["already_downloaded"]
|
| 281 |
-
message: str
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
TextEncoderDownloadResponse: TypeAlias = (
|
| 285 |
-
TextEncoderDownloadStartedResponse | TextEncoderAlreadyDownloadedResponse
|
| 286 |
-
)
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
class StatusResponse(BaseModel):
|
| 290 |
-
status: str
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
class ErrorResponse(BaseModel):
|
| 294 |
-
error: str
|
| 295 |
-
message: str | None = None
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
# ============================================================
|
| 299 |
-
# Request Models
|
| 300 |
-
# ============================================================
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
class GenerateVideoRequest(BaseModel):
|
| 304 |
-
prompt: NonEmptyPrompt
|
| 305 |
-
resolution: str = "512p"
|
| 306 |
-
model: str = "fast"
|
| 307 |
-
cameraMotion: VideoCameraMotion = "none"
|
| 308 |
-
negativePrompt: str = ""
|
| 309 |
-
duration: str = "2"
|
| 310 |
-
fps: str = "24"
|
| 311 |
-
audio: str = "false"
|
| 312 |
-
imagePath: str | None = None
|
| 313 |
-
audioPath: str | None = None
|
| 314 |
-
startFramePath: str | None = None
|
| 315 |
-
endFramePath: str | None = None
|
| 316 |
-
# 多张图单次推理:latent 时间轴多锚点(Comfy LTXVAddGuideMulti 思路);≥2 路径时优先于首尾帧
|
| 317 |
-
keyframePaths: list[str] | None = None
|
| 318 |
-
# 与 keyframePaths 等长、0.1–1.0;不传则按 Comfy 类工作流自动降低中间帧强度,减轻闪烁
|
| 319 |
-
keyframeStrengths: list[float] | None = None
|
| 320 |
-
# 与 keyframePaths 等长,单位秒,落在 [0, 整段时长];全提供时按时间映射 latent,否则仍自动均分
|
| 321 |
-
keyframeTimes: list[float] | None = None
|
| 322 |
-
aspectRatio: str = "16:9"
|
| 323 |
-
customWidth: int | None = None
|
| 324 |
-
customHeight: int | None = None
|
| 325 |
-
modelPath: str | None = None
|
| 326 |
-
loraPath: str | None = None
|
| 327 |
-
loraStrength: float = 2.0
|
| 328 |
-
loraPaths: list[str] | None = None
|
| 329 |
-
loraStrengths: list[float] | None = None
|
| 330 |
-
seed: int | None = None
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
class GenerateImageRequest(BaseModel):
|
| 334 |
-
prompt: NonEmptyPrompt
|
| 335 |
-
width: int = 1024
|
| 336 |
-
height: int = 1024
|
| 337 |
-
numSteps: int = 4
|
| 338 |
-
numImages: int = 1
|
| 339 |
-
seed: int | None = None
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
def _default_model_types() -> set[ModelFileType]:
|
| 343 |
-
return set()
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
class ModelDownloadRequest(BaseModel):
|
| 347 |
-
modelTypes: set[ModelFileType] = Field(default_factory=_default_model_types)
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
class RequiredModelsResponse(BaseModel):
|
| 351 |
-
modelTypes: list[ModelFileType]
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
class SuggestGapPromptRequest(BaseModel):
|
| 355 |
-
beforePrompt: str = ""
|
| 356 |
-
afterPrompt: str = ""
|
| 357 |
-
beforeFrame: str | None = None
|
| 358 |
-
afterFrame: str | None = None
|
| 359 |
-
gapDuration: float = 5
|
| 360 |
-
mode: str = "t2v"
|
| 361 |
-
inputImage: str | None = None
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
class RetakeRequest(BaseModel):
|
| 365 |
-
video_path: str
|
| 366 |
-
start_time: float = 0
|
| 367 |
-
duration: float = 0
|
| 368 |
-
prompt: str = ""
|
| 369 |
-
mode: str = "replace_video_only"
|
| 370 |
-
width: int | None = None
|
| 371 |
-
height: int | None = None
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
class IcLoraExtractRequest(BaseModel):
|
| 375 |
-
video_path: str
|
| 376 |
-
conditioning_type: Literal["canny", "depth", "pose", "video"] = "canny"
|
| 377 |
-
frame_time: float = 0
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
class IcLoraImageInput(BaseModel):
|
| 381 |
-
path: str
|
| 382 |
-
frame: int = 0
|
| 383 |
-
strength: float = 1.0
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
def _default_ic_lora_images() -> list[IcLoraImageInput]:
|
| 387 |
-
return []
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
class IcLoraGenerateRequest(BaseModel):
|
| 391 |
-
video_path: str
|
| 392 |
-
conditioning_type: Literal["canny", "depth", "pose", "video"]
|
| 393 |
-
prompt: NonEmptyPrompt
|
| 394 |
-
conditioning_strength: float = 1.0
|
| 395 |
-
num_inference_steps: int = 30
|
| 396 |
-
cfg_guidance_scale: float = 1.0
|
| 397 |
-
negative_prompt: str = ""
|
| 398 |
-
images: list[IcLoraImageInput] = Field(default_factory=_default_ic_lora_images)
|
| 399 |
-
ic_lora_path: str | None = None
|
| 400 |
-
seed: int | None = None
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
ConditioningType: TypeAlias = Literal["canny", "depth", "pose", "video"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/app_factory.py
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
LTX2.3-1.0.4-new/patches/app_settings_patch.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
"""运行时补丁:给 AppSettings 添加 lora_dir 字段(如果不存在)。"""
|
| 2 |
-
|
| 3 |
-
import sys
|
| 4 |
-
import os
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
def patch_app_settings():
|
| 8 |
-
try:
|
| 9 |
-
from state.app_settings import AppSettings
|
| 10 |
-
from pydantic import Field
|
| 11 |
-
|
| 12 |
-
if "lora_dir" not in AppSettings.model_fields:
|
| 13 |
-
AppSettings.model_fields["lora_dir"] = Field(
|
| 14 |
-
default="", validation_alias="loraDir", serialization_alias="loraDir"
|
| 15 |
-
)
|
| 16 |
-
AppSettings.model_rebuild(_force=True)
|
| 17 |
-
print("[PATCH] AppSettings patched: added lora_dir field")
|
| 18 |
-
except Exception as e:
|
| 19 |
-
print(f"[PATCH] AppSettings patch failed: {e}")
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
patch_app_settings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/handlers/__pycache__/video_generation_handler.cpython-313.pyc
DELETED
|
Binary file (36.5 kB)
|
|
|
LTX2.3-1.0.4-new/patches/handlers/video_generation_handler.py
DELETED
|
@@ -1,882 +0,0 @@
|
|
| 1 |
-
"""Video generation orchestration handler."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import logging
|
| 6 |
-
import os
|
| 7 |
-
import tempfile
|
| 8 |
-
import time
|
| 9 |
-
import uuid
|
| 10 |
-
from datetime import datetime
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
from threading import RLock
|
| 13 |
-
from typing import TYPE_CHECKING
|
| 14 |
-
|
| 15 |
-
from PIL import Image
|
| 16 |
-
|
| 17 |
-
from api_types import (
|
| 18 |
-
GenerateVideoRequest,
|
| 19 |
-
GenerateVideoResponse,
|
| 20 |
-
ImageConditioningInput,
|
| 21 |
-
VideoCameraMotion,
|
| 22 |
-
)
|
| 23 |
-
from _routes._errors import HTTPError
|
| 24 |
-
from handlers.base import StateHandlerBase
|
| 25 |
-
from handlers.generation_handler import GenerationHandler
|
| 26 |
-
from handlers.pipelines_handler import PipelinesHandler
|
| 27 |
-
from handlers.text_handler import TextHandler
|
| 28 |
-
from runtime_config.model_download_specs import resolve_model_path
|
| 29 |
-
from server_utils.media_validation import (
|
| 30 |
-
normalize_optional_path,
|
| 31 |
-
validate_audio_file,
|
| 32 |
-
validate_image_file,
|
| 33 |
-
)
|
| 34 |
-
from services.interfaces import LTXAPIClient
|
| 35 |
-
from state.app_state_types import AppState
|
| 36 |
-
from state.app_settings import should_video_generate_with_ltx_api
|
| 37 |
-
|
| 38 |
-
if TYPE_CHECKING:
|
| 39 |
-
from runtime_config.runtime_config import RuntimeConfig
|
| 40 |
-
|
| 41 |
-
logger = logging.getLogger(__name__)
|
| 42 |
-
|
| 43 |
-
FORCED_API_MODEL_MAP: dict[str, str] = {
|
| 44 |
-
"fast": "ltx-2-3-fast",
|
| 45 |
-
"pro": "ltx-2-3-pro",
|
| 46 |
-
}
|
| 47 |
-
FORCED_API_RESOLUTION_MAP: dict[str, dict[str, str]] = {
|
| 48 |
-
"1080p": {"16:9": "1920x1080", "9:16": "1080x1920"},
|
| 49 |
-
"1440p": {"16:9": "2560x1440", "9:16": "1440x2560"},
|
| 50 |
-
"2160p": {"16:9": "3840x2160", "9:16": "2160x3840"},
|
| 51 |
-
}
|
| 52 |
-
A2V_FORCED_API_RESOLUTION = "1920x1080"
|
| 53 |
-
FORCED_API_ALLOWED_ASPECT_RATIOS = {"16:9", "9:16"}
|
| 54 |
-
FORCED_API_ALLOWED_FPS = {24, 25, 48, 50}
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def _get_allowed_durations(model_id: str, resolution_label: str, fps: int) -> set[int]:
|
| 58 |
-
if model_id == "ltx-2-3-fast" and resolution_label == "1080p" and fps in {24, 25}:
|
| 59 |
-
return {6, 8, 10, 12, 14, 16, 18, 20}
|
| 60 |
-
return {6, 8, 10}
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
class VideoGenerationHandler(StateHandlerBase):
|
| 64 |
-
def __init__(
|
| 65 |
-
self,
|
| 66 |
-
state: AppState,
|
| 67 |
-
lock: RLock,
|
| 68 |
-
generation_handler: GenerationHandler,
|
| 69 |
-
pipelines_handler: PipelinesHandler,
|
| 70 |
-
text_handler: TextHandler,
|
| 71 |
-
ltx_api_client: LTXAPIClient,
|
| 72 |
-
config: RuntimeConfig,
|
| 73 |
-
) -> None:
|
| 74 |
-
super().__init__(state, lock, config)
|
| 75 |
-
self._generation = generation_handler
|
| 76 |
-
self._pipelines = pipelines_handler
|
| 77 |
-
self._text = text_handler
|
| 78 |
-
self._ltx_api_client = ltx_api_client
|
| 79 |
-
|
| 80 |
-
def generate(self, req: GenerateVideoRequest) -> GenerateVideoResponse:
|
| 81 |
-
if should_video_generate_with_ltx_api(
|
| 82 |
-
force_api_generations=self.config.force_api_generations,
|
| 83 |
-
settings=self.state.app_settings,
|
| 84 |
-
):
|
| 85 |
-
return self._generate_forced_api(req)
|
| 86 |
-
|
| 87 |
-
if self._generation.is_generation_running():
|
| 88 |
-
raise HTTPError(409, "Generation already in progress")
|
| 89 |
-
|
| 90 |
-
resolution = req.resolution
|
| 91 |
-
|
| 92 |
-
duration = int(float(req.duration))
|
| 93 |
-
fps = int(float(req.fps))
|
| 94 |
-
|
| 95 |
-
audio_path = normalize_optional_path(req.audioPath)
|
| 96 |
-
if audio_path:
|
| 97 |
-
return self._generate_a2v(req, duration, fps, audio_path=audio_path)
|
| 98 |
-
|
| 99 |
-
logger.info("Resolution %s - using fast pipeline", resolution)
|
| 100 |
-
|
| 101 |
-
RESOLUTION_MAP_16_9: dict[str, tuple[int, int]] = {
|
| 102 |
-
"540p": (1024, 576),
|
| 103 |
-
"720p": (1280, 704),
|
| 104 |
-
"1080p": (1920, 1088),
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
def get_16_9_size(res: str) -> tuple[int, int]:
|
| 108 |
-
return RESOLUTION_MAP_16_9.get(res, (1280, 704))
|
| 109 |
-
|
| 110 |
-
def get_9_16_size(res: str) -> tuple[int, int]:
|
| 111 |
-
w, h = get_16_9_size(res)
|
| 112 |
-
return h, w
|
| 113 |
-
|
| 114 |
-
match req.aspectRatio:
|
| 115 |
-
case "9:16":
|
| 116 |
-
width, height = get_9_16_size(resolution)
|
| 117 |
-
case "16:9":
|
| 118 |
-
width, height = get_16_9_size(resolution)
|
| 119 |
-
|
| 120 |
-
num_frames = self._compute_num_frames(duration, fps)
|
| 121 |
-
|
| 122 |
-
image = None
|
| 123 |
-
image_path = normalize_optional_path(req.imagePath)
|
| 124 |
-
if image_path:
|
| 125 |
-
image = self._prepare_image(image_path, width, height)
|
| 126 |
-
logger.info("Image: %s -> %sx%s", image_path, width, height)
|
| 127 |
-
|
| 128 |
-
generation_id = self._make_generation_id()
|
| 129 |
-
seed = self._resolve_seed()
|
| 130 |
-
|
| 131 |
-
logger.info(
|
| 132 |
-
f"Request loraPath: '{req.loraPath}', loraStrength: {req.loraStrength}, inferenceSteps: {req.inferenceSteps}"
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
# 尝试支持自定义步数(实验性)
|
| 136 |
-
inference_steps = req.inferenceSteps
|
| 137 |
-
logger.info(f"Using inference steps: {inference_steps}")
|
| 138 |
-
|
| 139 |
-
loras = []
|
| 140 |
-
try:
|
| 141 |
-
import os
|
| 142 |
-
from ltx_core.loader import LoraPathStrengthAndSDOps
|
| 143 |
-
from ltx_core.loader.sd_ops import LTXV_LORA_COMFY_RENAMING_MAP
|
| 144 |
-
|
| 145 |
-
# Handle legacy single LoRA
|
| 146 |
-
if req.loraPath and req.loraPath.strip():
|
| 147 |
-
lora_path = req.loraPath.strip()
|
| 148 |
-
if os.path.exists(lora_path):
|
| 149 |
-
loras.append(
|
| 150 |
-
LoraPathStrengthAndSDOps(
|
| 151 |
-
path=lora_path,
|
| 152 |
-
strength=req.loraStrength,
|
| 153 |
-
sd_ops=LTXV_LORA_COMFY_RENAMING_MAP,
|
| 154 |
-
)
|
| 155 |
-
)
|
| 156 |
-
|
| 157 |
-
# Handle multiple LoRAs
|
| 158 |
-
if req.loraPaths and req.loraStrengths:
|
| 159 |
-
for lp, ls in zip(req.loraPaths, req.loraStrengths):
|
| 160 |
-
if lp and lp.strip():
|
| 161 |
-
p = lp.strip()
|
| 162 |
-
if os.path.exists(p):
|
| 163 |
-
# Avoid duplicates if single LoRA was also in paths
|
| 164 |
-
if not any(x.path == p for x in loras):
|
| 165 |
-
loras.append(
|
| 166 |
-
LoraPathStrengthAndSDOps(
|
| 167 |
-
path=p,
|
| 168 |
-
strength=float(ls),
|
| 169 |
-
sd_ops=LTXV_LORA_COMFY_RENAMING_MAP,
|
| 170 |
-
)
|
| 171 |
-
)
|
| 172 |
-
logger.info(f"Multi-LoRA prepared: {p} with strength {ls}")
|
| 173 |
-
else:
|
| 174 |
-
logger.warning(f"Multi-LoRA file not found: {p}")
|
| 175 |
-
except Exception as e:
|
| 176 |
-
logger.warning(f"Failed to load LoRAs: {e}")
|
| 177 |
-
import traceback
|
| 178 |
-
logger.warning(f"LoRA traceback: {traceback.format_exc()}")
|
| 179 |
-
loras = []
|
| 180 |
-
|
| 181 |
-
if not loras:
|
| 182 |
-
loras = None
|
| 183 |
-
|
| 184 |
-
if loras is not None:
|
| 185 |
-
sig_list = []
|
| 186 |
-
for item in sorted(loras, key=lambda x: x.path):
|
| 187 |
-
sig_list.extend([item.path, round(float(item.strength), 4)])
|
| 188 |
-
desired_sig = ("fast", tuple(sig_list))
|
| 189 |
-
else:
|
| 190 |
-
desired_sig = ("fast", "", 0.0)
|
| 191 |
-
|
| 192 |
-
try:
|
| 193 |
-
if loras is not None:
|
| 194 |
-
# 强制卸载并重新加载带LoRA的pipeline
|
| 195 |
-
logger.info("Unloading pipeline for LoRA...")
|
| 196 |
-
from keep_models_runtime import force_unload_gpu_pipeline
|
| 197 |
-
|
| 198 |
-
force_unload_gpu_pipeline(self._pipelines)
|
| 199 |
-
|
| 200 |
-
# 强制垃圾回收
|
| 201 |
-
import gc
|
| 202 |
-
|
| 203 |
-
gc.collect()
|
| 204 |
-
# 释放 CUDA 缓存,降低 LoRA 首次构建的显存峰值/碎片风险
|
| 205 |
-
try:
|
| 206 |
-
import torch
|
| 207 |
-
if torch.cuda.is_available():
|
| 208 |
-
torch.cuda.empty_cache()
|
| 209 |
-
torch.cuda.ipc_collect()
|
| 210 |
-
except Exception:
|
| 211 |
-
pass
|
| 212 |
-
|
| 213 |
-
gemma_root = self._pipelines._text_handler.resolve_gemma_root()
|
| 214 |
-
from runtime_config.model_download_specs import resolve_model_path
|
| 215 |
-
from services.fast_video_pipeline.ltx_fast_video_pipeline import (
|
| 216 |
-
LTXFastVideoPipeline,
|
| 217 |
-
)
|
| 218 |
-
|
| 219 |
-
checkpoint_path = str(
|
| 220 |
-
resolve_model_path(
|
| 221 |
-
self._pipelines.models_dir,
|
| 222 |
-
self._pipelines.config.model_download_specs,
|
| 223 |
-
"checkpoint",
|
| 224 |
-
)
|
| 225 |
-
)
|
| 226 |
-
upsampler_path = str(
|
| 227 |
-
resolve_model_path(
|
| 228 |
-
self._pipelines.models_dir,
|
| 229 |
-
self._pipelines.config.model_download_specs,
|
| 230 |
-
"upsampler",
|
| 231 |
-
)
|
| 232 |
-
)
|
| 233 |
-
|
| 234 |
-
logger.info(
|
| 235 |
-
f"Creating pipeline with LoRA: {loras}, steps: {inference_steps}"
|
| 236 |
-
)
|
| 237 |
-
from lora_injection import (
|
| 238 |
-
_lora_init_kwargs,
|
| 239 |
-
inject_loras_into_fast_pipeline,
|
| 240 |
-
)
|
| 241 |
-
|
| 242 |
-
lora_kw = _lora_init_kwargs(LTXFastVideoPipeline, loras)
|
| 243 |
-
pipeline = LTXFastVideoPipeline(
|
| 244 |
-
checkpoint_path,
|
| 245 |
-
gemma_root,
|
| 246 |
-
upsampler_path,
|
| 247 |
-
self._pipelines.config.device,
|
| 248 |
-
**lora_kw,
|
| 249 |
-
)
|
| 250 |
-
n_inj = inject_loras_into_fast_pipeline(pipeline, loras)
|
| 251 |
-
if hasattr(pipeline, "pipeline") and hasattr(
|
| 252 |
-
pipeline.pipeline, "model_ledger"
|
| 253 |
-
):
|
| 254 |
-
try:
|
| 255 |
-
pipeline.pipeline.model_ledger.loras = tuple(loras)
|
| 256 |
-
except Exception:
|
| 257 |
-
pass
|
| 258 |
-
logger.info(
|
| 259 |
-
"LoRA 注入: init_kw=%s, 注入点=%s, model_ledger.loras=%s",
|
| 260 |
-
list(lora_kw.keys()),
|
| 261 |
-
n_inj,
|
| 262 |
-
getattr(
|
| 263 |
-
getattr(pipeline.pipeline, "model_ledger", None),
|
| 264 |
-
"loras",
|
| 265 |
-
None,
|
| 266 |
-
),
|
| 267 |
-
)
|
| 268 |
-
|
| 269 |
-
from state.app_state_types import (
|
| 270 |
-
VideoPipelineState,
|
| 271 |
-
VideoPipelineWarmth,
|
| 272 |
-
GpuSlot,
|
| 273 |
-
)
|
| 274 |
-
|
| 275 |
-
state = VideoPipelineState(
|
| 276 |
-
pipeline=pipeline,
|
| 277 |
-
warmth=VideoPipelineWarmth.COLD,
|
| 278 |
-
is_compiled=False,
|
| 279 |
-
)
|
| 280 |
-
|
| 281 |
-
self._pipelines.state.gpu_slot = GpuSlot(
|
| 282 |
-
active_pipeline=state, generation=None
|
| 283 |
-
)
|
| 284 |
-
logger.info("Pipeline with LoRA loaded successfully")
|
| 285 |
-
else:
|
| 286 |
-
# 无论有没有LoRA,都尝试使用自定义步数重新加载pipeline
|
| 287 |
-
logger.info(f"Loading pipeline with {inference_steps} steps")
|
| 288 |
-
from keep_models_runtime import force_unload_gpu_pipeline
|
| 289 |
-
|
| 290 |
-
force_unload_gpu_pipeline(self._pipelines)
|
| 291 |
-
|
| 292 |
-
import gc
|
| 293 |
-
|
| 294 |
-
gc.collect()
|
| 295 |
-
|
| 296 |
-
gemma_root = self._pipelines._text_handler.resolve_gemma_root()
|
| 297 |
-
from runtime_config.model_download_specs import resolve_model_path
|
| 298 |
-
from services.fast_video_pipeline.ltx_fast_video_pipeline import (
|
| 299 |
-
LTXFastVideoPipeline,
|
| 300 |
-
)
|
| 301 |
-
|
| 302 |
-
checkpoint_path = str(
|
| 303 |
-
resolve_model_path(
|
| 304 |
-
self._pipelines.models_dir,
|
| 305 |
-
self._pipelines.config.model_download_specs,
|
| 306 |
-
"checkpoint",
|
| 307 |
-
)
|
| 308 |
-
)
|
| 309 |
-
upsampler_path = str(
|
| 310 |
-
resolve_model_path(
|
| 311 |
-
self._pipelines.models_dir,
|
| 312 |
-
self._pipelines.config.model_download_specs,
|
| 313 |
-
"upsampler",
|
| 314 |
-
)
|
| 315 |
-
)
|
| 316 |
-
|
| 317 |
-
pipeline = LTXFastVideoPipeline(
|
| 318 |
-
checkpoint_path,
|
| 319 |
-
gemma_root,
|
| 320 |
-
upsampler_path,
|
| 321 |
-
self._pipelines.config.device,
|
| 322 |
-
)
|
| 323 |
-
|
| 324 |
-
from state.app_state_types import (
|
| 325 |
-
VideoPipelineState,
|
| 326 |
-
VideoPipelineWarmth,
|
| 327 |
-
GpuSlot,
|
| 328 |
-
)
|
| 329 |
-
|
| 330 |
-
state = VideoPipelineState(
|
| 331 |
-
pipeline=pipeline,
|
| 332 |
-
warmth=VideoPipelineWarmth.COLD,
|
| 333 |
-
is_compiled=False,
|
| 334 |
-
)
|
| 335 |
-
|
| 336 |
-
self._pipelines.state.gpu_slot = GpuSlot(
|
| 337 |
-
active_pipeline=state, generation=None
|
| 338 |
-
)
|
| 339 |
-
|
| 340 |
-
self._pipelines._pipeline_signature = desired_sig
|
| 341 |
-
|
| 342 |
-
self._generation.start_generation(generation_id)
|
| 343 |
-
|
| 344 |
-
output_path = self.generate_video(
|
| 345 |
-
prompt=req.prompt,
|
| 346 |
-
image=image,
|
| 347 |
-
height=height,
|
| 348 |
-
width=width,
|
| 349 |
-
num_frames=num_frames,
|
| 350 |
-
fps=fps,
|
| 351 |
-
seed=seed,
|
| 352 |
-
camera_motion=req.cameraMotion,
|
| 353 |
-
negative_prompt=req.negativePrompt,
|
| 354 |
-
)
|
| 355 |
-
|
| 356 |
-
self._generation.complete_generation(output_path)
|
| 357 |
-
return GenerateVideoResponse(status="complete", video_path=output_path)
|
| 358 |
-
|
| 359 |
-
except Exception as e:
|
| 360 |
-
self._generation.fail_generation(str(e))
|
| 361 |
-
if "cancelled" in str(e).lower():
|
| 362 |
-
logger.info("Generation cancelled by user")
|
| 363 |
-
return GenerateVideoResponse(status="cancelled")
|
| 364 |
-
|
| 365 |
-
raise HTTPError(500, str(e)) from e
|
| 366 |
-
|
| 367 |
-
def generate_video(
|
| 368 |
-
self,
|
| 369 |
-
prompt: str,
|
| 370 |
-
image: Image.Image | None,
|
| 371 |
-
height: int,
|
| 372 |
-
width: int,
|
| 373 |
-
num_frames: int,
|
| 374 |
-
fps: float,
|
| 375 |
-
seed: int,
|
| 376 |
-
camera_motion: VideoCameraMotion,
|
| 377 |
-
negative_prompt: str,
|
| 378 |
-
) -> str:
|
| 379 |
-
t_total_start = time.perf_counter()
|
| 380 |
-
gen_mode = "i2v" if image is not None else "t2v"
|
| 381 |
-
logger.info(
|
| 382 |
-
"[%s] Generation started (model=fast, %dx%d, %d frames, %d fps)",
|
| 383 |
-
gen_mode,
|
| 384 |
-
width,
|
| 385 |
-
height,
|
| 386 |
-
num_frames,
|
| 387 |
-
int(fps),
|
| 388 |
-
)
|
| 389 |
-
|
| 390 |
-
if self._generation.is_generation_cancelled():
|
| 391 |
-
raise RuntimeError("Generation was cancelled")
|
| 392 |
-
|
| 393 |
-
if not resolve_model_path(
|
| 394 |
-
self.models_dir, self.config.model_download_specs, "checkpoint"
|
| 395 |
-
).exists():
|
| 396 |
-
raise RuntimeError(
|
| 397 |
-
"Models not downloaded. Please download the AI models first using the Model Status menu."
|
| 398 |
-
)
|
| 399 |
-
|
| 400 |
-
total_steps = 8
|
| 401 |
-
|
| 402 |
-
self._generation.update_progress("loading_model", 5, 0, total_steps)
|
| 403 |
-
t_load_start = time.perf_counter()
|
| 404 |
-
pipeline_state = self._pipelines.load_gpu_pipeline("fast", should_warm=False)
|
| 405 |
-
t_load_end = time.perf_counter()
|
| 406 |
-
logger.info("[%s] Pipeline load: %.2fs", gen_mode, t_load_end - t_load_start)
|
| 407 |
-
|
| 408 |
-
self._generation.update_progress("encoding_text", 10, 0, total_steps)
|
| 409 |
-
|
| 410 |
-
enhanced_prompt = prompt + self.config.camera_motion_prompts.get(
|
| 411 |
-
camera_motion, ""
|
| 412 |
-
)
|
| 413 |
-
|
| 414 |
-
images: list[ImageConditioningInput] = []
|
| 415 |
-
temp_image_path: str | None = None
|
| 416 |
-
if image is not None:
|
| 417 |
-
temp_image_path = tempfile.NamedTemporaryFile(
|
| 418 |
-
suffix=".png", delete=False
|
| 419 |
-
).name
|
| 420 |
-
image.save(temp_image_path)
|
| 421 |
-
images = [
|
| 422 |
-
ImageConditioningInput(path=temp_image_path, frame_idx=0, strength=1.0)
|
| 423 |
-
]
|
| 424 |
-
|
| 425 |
-
output_path = self._make_output_path()
|
| 426 |
-
|
| 427 |
-
try:
|
| 428 |
-
settings = self.state.app_settings
|
| 429 |
-
use_api_encoding = not self._text.should_use_local_encoding()
|
| 430 |
-
if image is not None:
|
| 431 |
-
enhance = use_api_encoding and settings.prompt_enhancer_enabled_i2v
|
| 432 |
-
else:
|
| 433 |
-
enhance = use_api_encoding and settings.prompt_enhancer_enabled_t2v
|
| 434 |
-
|
| 435 |
-
encoding_method = "api" if use_api_encoding else "local"
|
| 436 |
-
t_text_start = time.perf_counter()
|
| 437 |
-
self._text.prepare_text_encoding(enhanced_prompt, enhance_prompt=enhance)
|
| 438 |
-
t_text_end = time.perf_counter()
|
| 439 |
-
logger.info(
|
| 440 |
-
"[%s] Text encoding (%s): %.2fs",
|
| 441 |
-
gen_mode,
|
| 442 |
-
encoding_method,
|
| 443 |
-
t_text_end - t_text_start,
|
| 444 |
-
)
|
| 445 |
-
|
| 446 |
-
self._generation.update_progress("inference", 15, 0, total_steps)
|
| 447 |
-
|
| 448 |
-
height = round(height / 64) * 64
|
| 449 |
-
width = round(width / 64) * 64
|
| 450 |
-
|
| 451 |
-
t_inference_start = time.perf_counter()
|
| 452 |
-
pipeline_state.pipeline.generate(
|
| 453 |
-
prompt=enhanced_prompt,
|
| 454 |
-
seed=seed,
|
| 455 |
-
height=height,
|
| 456 |
-
width=width,
|
| 457 |
-
num_frames=num_frames,
|
| 458 |
-
frame_rate=fps,
|
| 459 |
-
images=images,
|
| 460 |
-
output_path=str(output_path),
|
| 461 |
-
)
|
| 462 |
-
t_inference_end = time.perf_counter()
|
| 463 |
-
logger.info(
|
| 464 |
-
"[%s] Inference: %.2fs", gen_mode, t_inference_end - t_inference_start
|
| 465 |
-
)
|
| 466 |
-
|
| 467 |
-
if self._generation.is_generation_cancelled():
|
| 468 |
-
if output_path.exists():
|
| 469 |
-
output_path.unlink()
|
| 470 |
-
raise RuntimeError("Generation was cancelled")
|
| 471 |
-
|
| 472 |
-
t_total_end = time.perf_counter()
|
| 473 |
-
logger.info(
|
| 474 |
-
"[%s] Total generation: %.2fs (load=%.2fs, text=%.2fs, inference=%.2fs)",
|
| 475 |
-
gen_mode,
|
| 476 |
-
t_total_end - t_total_start,
|
| 477 |
-
t_load_end - t_load_start,
|
| 478 |
-
t_text_end - t_text_start,
|
| 479 |
-
t_inference_end - t_inference_start,
|
| 480 |
-
)
|
| 481 |
-
|
| 482 |
-
self._generation.update_progress("complete", 100, total_steps, total_steps)
|
| 483 |
-
return str(output_path)
|
| 484 |
-
finally:
|
| 485 |
-
self._text.clear_api_embeddings()
|
| 486 |
-
if temp_image_path and os.path.exists(temp_image_path):
|
| 487 |
-
os.unlink(temp_image_path)
|
| 488 |
-
|
| 489 |
-
def _generate_a2v(
|
| 490 |
-
self, req: GenerateVideoRequest, duration: int, fps: int, *, audio_path: str
|
| 491 |
-
) -> GenerateVideoResponse:
|
| 492 |
-
if req.model != "pro":
|
| 493 |
-
logger.warning(
|
| 494 |
-
"A2V local requested with model=%s; A2V always uses pro pipeline",
|
| 495 |
-
req.model,
|
| 496 |
-
)
|
| 497 |
-
validated_audio_path = validate_audio_file(audio_path)
|
| 498 |
-
audio_path_str = str(validated_audio_path)
|
| 499 |
-
|
| 500 |
-
# 支持竖屏和横屏
|
| 501 |
-
RESOLUTION_MAP: dict[str, tuple[int, int]] = {
|
| 502 |
-
"540p": (1024, 576),
|
| 503 |
-
"720p": (1280, 704),
|
| 504 |
-
"1080p": (1920, 1088),
|
| 505 |
-
}
|
| 506 |
-
|
| 507 |
-
base_w, base_h = RESOLUTION_MAP.get(req.resolution, (1280, 704))
|
| 508 |
-
|
| 509 |
-
# 根据 aspectRatio 调整分辨率
|
| 510 |
-
if req.aspectRatio == "9:16":
|
| 511 |
-
width, height = base_h, base_w # 竖屏
|
| 512 |
-
else:
|
| 513 |
-
width, height = base_w, base_h # 横屏
|
| 514 |
-
|
| 515 |
-
num_frames = self._compute_num_frames(duration, fps)
|
| 516 |
-
|
| 517 |
-
image = None
|
| 518 |
-
temp_image_path: str | None = None
|
| 519 |
-
image_path = normalize_optional_path(req.imagePath)
|
| 520 |
-
if image_path:
|
| 521 |
-
image = self._prepare_image(image_path, width, height)
|
| 522 |
-
|
| 523 |
-
# 获取首尾帧
|
| 524 |
-
start_frame_path = normalize_optional_path(getattr(req, "startFramePath", None))
|
| 525 |
-
end_frame_path = normalize_optional_path(getattr(req, "endFramePath", None))
|
| 526 |
-
|
| 527 |
-
seed = self._resolve_seed()
|
| 528 |
-
|
| 529 |
-
generation_id = self._make_generation_id()
|
| 530 |
-
|
| 531 |
-
temp_image_paths: list[str] = []
|
| 532 |
-
try:
|
| 533 |
-
a2v_state = self._pipelines.load_a2v_pipeline()
|
| 534 |
-
self._generation.start_generation(generation_id)
|
| 535 |
-
|
| 536 |
-
enhanced_prompt = req.prompt + self.config.camera_motion_prompts.get(
|
| 537 |
-
req.cameraMotion, ""
|
| 538 |
-
)
|
| 539 |
-
neg = (
|
| 540 |
-
req.negativePrompt
|
| 541 |
-
if req.negativePrompt
|
| 542 |
-
else self.config.default_negative_prompt
|
| 543 |
-
)
|
| 544 |
-
|
| 545 |
-
images: list[ImageConditioningInput] = []
|
| 546 |
-
temp_image_paths: list[str] = []
|
| 547 |
-
|
| 548 |
-
# 首帧
|
| 549 |
-
if start_frame_path:
|
| 550 |
-
start_img = self._prepare_image(start_frame_path, width, height)
|
| 551 |
-
temp_start_path = tempfile.NamedTemporaryFile(
|
| 552 |
-
suffix=".png", delete=False
|
| 553 |
-
).name
|
| 554 |
-
start_img.save(temp_start_path)
|
| 555 |
-
temp_image_paths.append(temp_start_path)
|
| 556 |
-
images.append(
|
| 557 |
-
ImageConditioningInput(
|
| 558 |
-
path=temp_start_path, frame_idx=0, strength=1.0
|
| 559 |
-
)
|
| 560 |
-
)
|
| 561 |
-
|
| 562 |
-
# 中间图片(如果有)
|
| 563 |
-
if image is not None and not start_frame_path:
|
| 564 |
-
temp_image_path = tempfile.NamedTemporaryFile(
|
| 565 |
-
suffix=".png", delete=False
|
| 566 |
-
).name
|
| 567 |
-
image.save(temp_image_path)
|
| 568 |
-
temp_image_paths.append(temp_image_path)
|
| 569 |
-
images.append(
|
| 570 |
-
ImageConditioningInput(
|
| 571 |
-
path=temp_image_path, frame_idx=0, strength=1.0
|
| 572 |
-
)
|
| 573 |
-
)
|
| 574 |
-
|
| 575 |
-
# 尾帧
|
| 576 |
-
if end_frame_path:
|
| 577 |
-
last_latent_idx = (num_frames - 1) // 8 + 1 - 1
|
| 578 |
-
end_img = self._prepare_image(end_frame_path, width, height)
|
| 579 |
-
temp_end_path = tempfile.NamedTemporaryFile(
|
| 580 |
-
suffix=".png", delete=False
|
| 581 |
-
).name
|
| 582 |
-
end_img.save(temp_end_path)
|
| 583 |
-
temp_image_paths.append(temp_end_path)
|
| 584 |
-
images.append(
|
| 585 |
-
ImageConditioningInput(
|
| 586 |
-
path=temp_end_path, frame_idx=last_latent_idx, strength=1.0
|
| 587 |
-
)
|
| 588 |
-
)
|
| 589 |
-
|
| 590 |
-
output_path = self._make_output_path()
|
| 591 |
-
|
| 592 |
-
total_steps = 11 # distilled: 8 steps (stage 1) + 3 steps (stage 2)
|
| 593 |
-
|
| 594 |
-
a2v_settings = self.state.app_settings
|
| 595 |
-
a2v_use_api = not self._text.should_use_local_encoding()
|
| 596 |
-
if image is not None:
|
| 597 |
-
a2v_enhance = a2v_use_api and a2v_settings.prompt_enhancer_enabled_i2v
|
| 598 |
-
else:
|
| 599 |
-
a2v_enhance = a2v_use_api and a2v_settings.prompt_enhancer_enabled_t2v
|
| 600 |
-
|
| 601 |
-
self._generation.update_progress("loading_model", 5, 0, total_steps)
|
| 602 |
-
self._generation.update_progress("encoding_text", 10, 0, total_steps)
|
| 603 |
-
self._text.prepare_text_encoding(
|
| 604 |
-
enhanced_prompt, enhance_prompt=a2v_enhance
|
| 605 |
-
)
|
| 606 |
-
self._generation.update_progress("inference", 15, 0, total_steps)
|
| 607 |
-
|
| 608 |
-
a2v_state.pipeline.generate(
|
| 609 |
-
prompt=enhanced_prompt,
|
| 610 |
-
negative_prompt=neg,
|
| 611 |
-
seed=seed,
|
| 612 |
-
height=height,
|
| 613 |
-
width=width,
|
| 614 |
-
num_frames=num_frames,
|
| 615 |
-
frame_rate=fps,
|
| 616 |
-
num_inference_steps=total_steps,
|
| 617 |
-
images=images,
|
| 618 |
-
audio_path=audio_path_str,
|
| 619 |
-
audio_start_time=0.0,
|
| 620 |
-
audio_max_duration=None,
|
| 621 |
-
output_path=str(output_path),
|
| 622 |
-
)
|
| 623 |
-
|
| 624 |
-
if self._generation.is_generation_cancelled():
|
| 625 |
-
if output_path.exists():
|
| 626 |
-
output_path.unlink()
|
| 627 |
-
raise RuntimeError("Generation was cancelled")
|
| 628 |
-
|
| 629 |
-
self._generation.update_progress("complete", 100, total_steps, total_steps)
|
| 630 |
-
self._generation.complete_generation(str(output_path))
|
| 631 |
-
return GenerateVideoResponse(status="complete", video_path=str(output_path))
|
| 632 |
-
|
| 633 |
-
except Exception as e:
|
| 634 |
-
self._generation.fail_generation(str(e))
|
| 635 |
-
if "cancelled" in str(e).lower():
|
| 636 |
-
logger.info("Generation cancelled by user")
|
| 637 |
-
return GenerateVideoResponse(status="cancelled")
|
| 638 |
-
raise HTTPError(500, str(e)) from e
|
| 639 |
-
finally:
|
| 640 |
-
self._text.clear_api_embeddings()
|
| 641 |
-
# 清理所有临时图片
|
| 642 |
-
for tmp_path in temp_image_paths:
|
| 643 |
-
if tmp_path and os.path.exists(tmp_path):
|
| 644 |
-
try:
|
| 645 |
-
os.unlink(tmp_path)
|
| 646 |
-
except Exception:
|
| 647 |
-
pass
|
| 648 |
-
if temp_image_path and os.path.exists(temp_image_path):
|
| 649 |
-
try:
|
| 650 |
-
os.unlink(temp_image_path)
|
| 651 |
-
except Exception:
|
| 652 |
-
pass
|
| 653 |
-
|
| 654 |
-
def _prepare_image(self, image_path: str, width: int, height: int) -> Image.Image:
|
| 655 |
-
validated_path = validate_image_file(image_path)
|
| 656 |
-
try:
|
| 657 |
-
img = Image.open(validated_path).convert("RGB")
|
| 658 |
-
except Exception:
|
| 659 |
-
raise HTTPError(400, f"Invalid image file: {image_path}") from None
|
| 660 |
-
img_w, img_h = img.size
|
| 661 |
-
target_ratio = width / height
|
| 662 |
-
img_ratio = img_w / img_h
|
| 663 |
-
if img_ratio > target_ratio:
|
| 664 |
-
new_h = height
|
| 665 |
-
new_w = int(img_w * (height / img_h))
|
| 666 |
-
else:
|
| 667 |
-
new_w = width
|
| 668 |
-
new_h = int(img_h * (width / img_w))
|
| 669 |
-
resized = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
|
| 670 |
-
left = (new_w - width) // 2
|
| 671 |
-
top = (new_h - height) // 2
|
| 672 |
-
return resized.crop((left, top, left + width, top + height))
|
| 673 |
-
|
| 674 |
-
@staticmethod
|
| 675 |
-
def _make_generation_id() -> str:
|
| 676 |
-
return uuid.uuid4().hex[:8]
|
| 677 |
-
|
| 678 |
-
@staticmethod
|
| 679 |
-
def _compute_num_frames(duration: int, fps: int) -> int:
|
| 680 |
-
n = ((duration * fps) // 8) * 8 + 1
|
| 681 |
-
return max(n, 9)
|
| 682 |
-
|
| 683 |
-
def _resolve_seed(self) -> int:
|
| 684 |
-
settings = self.state.app_settings
|
| 685 |
-
if settings.seed_locked:
|
| 686 |
-
logger.info("Using locked seed: %s", settings.locked_seed)
|
| 687 |
-
return settings.locked_seed
|
| 688 |
-
return int(time.time()) % 2147483647
|
| 689 |
-
|
| 690 |
-
def _make_output_path(self) -> Path:
|
| 691 |
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 692 |
-
return (
|
| 693 |
-
self.config.outputs_dir
|
| 694 |
-
/ f"ltx2_video_{timestamp}_{self._make_generation_id()}.mp4"
|
| 695 |
-
)
|
| 696 |
-
|
| 697 |
-
def _generate_forced_api(self, req: GenerateVideoRequest) -> GenerateVideoResponse:
|
| 698 |
-
if self._generation.is_generation_running():
|
| 699 |
-
raise HTTPError(409, "Generation already in progress")
|
| 700 |
-
|
| 701 |
-
generation_id = self._make_generation_id()
|
| 702 |
-
self._generation.start_api_generation(generation_id)
|
| 703 |
-
|
| 704 |
-
audio_path = normalize_optional_path(req.audioPath)
|
| 705 |
-
image_path = normalize_optional_path(req.imagePath)
|
| 706 |
-
has_input_audio = bool(audio_path)
|
| 707 |
-
has_input_image = bool(image_path)
|
| 708 |
-
|
| 709 |
-
try:
|
| 710 |
-
self._generation.update_progress("validating_request", 5, None, None)
|
| 711 |
-
|
| 712 |
-
api_key = self.state.app_settings.ltx_api_key.strip()
|
| 713 |
-
logger.info(
|
| 714 |
-
"Forced API generation route selected (key_present=%s)", bool(api_key)
|
| 715 |
-
)
|
| 716 |
-
if not api_key:
|
| 717 |
-
raise HTTPError(400, "PRO_API_KEY_REQUIRED")
|
| 718 |
-
|
| 719 |
-
requested_model = req.model.strip().lower()
|
| 720 |
-
api_model_id = FORCED_API_MODEL_MAP.get(requested_model)
|
| 721 |
-
if api_model_id is None:
|
| 722 |
-
raise HTTPError(400, "INVALID_FORCED_API_MODEL")
|
| 723 |
-
|
| 724 |
-
resolution_label = req.resolution
|
| 725 |
-
resolution_by_aspect = FORCED_API_RESOLUTION_MAP.get(resolution_label)
|
| 726 |
-
if resolution_by_aspect is None:
|
| 727 |
-
raise HTTPError(400, "INVALID_FORCED_API_RESOLUTION")
|
| 728 |
-
|
| 729 |
-
aspect_ratio = req.aspectRatio.strip()
|
| 730 |
-
if aspect_ratio not in FORCED_API_ALLOWED_ASPECT_RATIOS:
|
| 731 |
-
raise HTTPError(400, "INVALID_FORCED_API_ASPECT_RATIO")
|
| 732 |
-
|
| 733 |
-
api_resolution = resolution_by_aspect[aspect_ratio]
|
| 734 |
-
|
| 735 |
-
prompt = req.prompt
|
| 736 |
-
|
| 737 |
-
if self._generation.is_generation_cancelled():
|
| 738 |
-
raise RuntimeError("Generation was cancelled")
|
| 739 |
-
|
| 740 |
-
if has_input_audio:
|
| 741 |
-
if requested_model != "pro":
|
| 742 |
-
logger.warning(
|
| 743 |
-
"A2V requested with model=%s; overriding to 'pro'",
|
| 744 |
-
requested_model,
|
| 745 |
-
)
|
| 746 |
-
api_model_id = FORCED_API_MODEL_MAP["pro"]
|
| 747 |
-
if api_resolution != A2V_FORCED_API_RESOLUTION:
|
| 748 |
-
logger.warning(
|
| 749 |
-
"A2V requested with resolution=%s; overriding to '%s'",
|
| 750 |
-
api_resolution,
|
| 751 |
-
A2V_FORCED_API_RESOLUTION,
|
| 752 |
-
)
|
| 753 |
-
api_resolution = A2V_FORCED_API_RESOLUTION
|
| 754 |
-
validated_audio_path = validate_audio_file(audio_path)
|
| 755 |
-
validated_image_path: Path | None = None
|
| 756 |
-
if image_path is not None:
|
| 757 |
-
validated_image_path = validate_image_file(image_path)
|
| 758 |
-
|
| 759 |
-
self._generation.update_progress("uploading_audio", 20, None, None)
|
| 760 |
-
audio_uri = self._ltx_api_client.upload_file(
|
| 761 |
-
api_key=api_key,
|
| 762 |
-
file_path=str(validated_audio_path),
|
| 763 |
-
)
|
| 764 |
-
image_uri: str | None = None
|
| 765 |
-
if validated_image_path is not None:
|
| 766 |
-
self._generation.update_progress("uploading_image", 35, None, None)
|
| 767 |
-
image_uri = self._ltx_api_client.upload_file(
|
| 768 |
-
api_key=api_key,
|
| 769 |
-
file_path=str(validated_image_path),
|
| 770 |
-
)
|
| 771 |
-
self._generation.update_progress("inference", 55, None, None)
|
| 772 |
-
video_bytes = self._ltx_api_client.generate_audio_to_video(
|
| 773 |
-
api_key=api_key,
|
| 774 |
-
prompt=prompt,
|
| 775 |
-
audio_uri=audio_uri,
|
| 776 |
-
image_uri=image_uri,
|
| 777 |
-
model=api_model_id,
|
| 778 |
-
resolution=api_resolution,
|
| 779 |
-
)
|
| 780 |
-
self._generation.update_progress("downloading_output", 85, None, None)
|
| 781 |
-
elif has_input_image:
|
| 782 |
-
validated_image_path = validate_image_file(image_path)
|
| 783 |
-
|
| 784 |
-
duration = self._parse_forced_numeric_field(
|
| 785 |
-
req.duration, "INVALID_FORCED_API_DURATION"
|
| 786 |
-
)
|
| 787 |
-
fps = self._parse_forced_numeric_field(
|
| 788 |
-
req.fps, "INVALID_FORCED_API_FPS"
|
| 789 |
-
)
|
| 790 |
-
if fps not in FORCED_API_ALLOWED_FPS:
|
| 791 |
-
raise HTTPError(400, "INVALID_FORCED_API_FPS")
|
| 792 |
-
if duration not in _get_allowed_durations(
|
| 793 |
-
api_model_id, resolution_label, fps
|
| 794 |
-
):
|
| 795 |
-
raise HTTPError(400, "INVALID_FORCED_API_DURATION")
|
| 796 |
-
|
| 797 |
-
generate_audio = self._parse_audio_flag(req.audio)
|
| 798 |
-
self._generation.update_progress("uploading_image", 20, None, None)
|
| 799 |
-
image_uri = self._ltx_api_client.upload_file(
|
| 800 |
-
api_key=api_key,
|
| 801 |
-
file_path=str(validated_image_path),
|
| 802 |
-
)
|
| 803 |
-
self._generation.update_progress("inference", 55, None, None)
|
| 804 |
-
video_bytes = self._ltx_api_client.generate_image_to_video(
|
| 805 |
-
api_key=api_key,
|
| 806 |
-
prompt=prompt,
|
| 807 |
-
image_uri=image_uri,
|
| 808 |
-
model=api_model_id,
|
| 809 |
-
resolution=api_resolution,
|
| 810 |
-
duration=float(duration),
|
| 811 |
-
fps=float(fps),
|
| 812 |
-
generate_audio=generate_audio,
|
| 813 |
-
camera_motion=req.cameraMotion,
|
| 814 |
-
)
|
| 815 |
-
self._generation.update_progress("downloading_output", 85, None, None)
|
| 816 |
-
else:
|
| 817 |
-
duration = self._parse_forced_numeric_field(
|
| 818 |
-
req.duration, "INVALID_FORCED_API_DURATION"
|
| 819 |
-
)
|
| 820 |
-
fps = self._parse_forced_numeric_field(
|
| 821 |
-
req.fps, "INVALID_FORCED_API_FPS"
|
| 822 |
-
)
|
| 823 |
-
if fps not in FORCED_API_ALLOWED_FPS:
|
| 824 |
-
raise HTTPError(400, "INVALID_FORCED_API_FPS")
|
| 825 |
-
if duration not in _get_allowed_durations(
|
| 826 |
-
api_model_id, resolution_label, fps
|
| 827 |
-
):
|
| 828 |
-
raise HTTPError(400, "INVALID_FORCED_API_DURATION")
|
| 829 |
-
|
| 830 |
-
generate_audio = self._parse_audio_flag(req.audio)
|
| 831 |
-
self._generation.update_progress("inference", 55, None, None)
|
| 832 |
-
video_bytes = self._ltx_api_client.generate_text_to_video(
|
| 833 |
-
api_key=api_key,
|
| 834 |
-
prompt=prompt,
|
| 835 |
-
model=api_model_id,
|
| 836 |
-
resolution=api_resolution,
|
| 837 |
-
duration=float(duration),
|
| 838 |
-
fps=float(fps),
|
| 839 |
-
generate_audio=generate_audio,
|
| 840 |
-
camera_motion=req.cameraMotion,
|
| 841 |
-
)
|
| 842 |
-
self._generation.update_progress("downloading_output", 85, None, None)
|
| 843 |
-
|
| 844 |
-
if self._generation.is_generation_cancelled():
|
| 845 |
-
raise RuntimeError("Generation was cancelled")
|
| 846 |
-
|
| 847 |
-
output_path = self._write_forced_api_video(video_bytes)
|
| 848 |
-
if self._generation.is_generation_cancelled():
|
| 849 |
-
output_path.unlink(missing_ok=True)
|
| 850 |
-
raise RuntimeError("Generation was cancelled")
|
| 851 |
-
|
| 852 |
-
self._generation.update_progress("complete", 100, None, None)
|
| 853 |
-
self._generation.complete_generation(str(output_path))
|
| 854 |
-
return GenerateVideoResponse(status="complete", video_path=str(output_path))
|
| 855 |
-
except HTTPError as e:
|
| 856 |
-
self._generation.fail_generation(e.detail)
|
| 857 |
-
raise
|
| 858 |
-
except Exception as e:
|
| 859 |
-
self._generation.fail_generation(str(e))
|
| 860 |
-
if "cancelled" in str(e).lower():
|
| 861 |
-
logger.info("Generation cancelled by user")
|
| 862 |
-
return GenerateVideoResponse(status="cancelled")
|
| 863 |
-
raise HTTPError(500, str(e)) from e
|
| 864 |
-
|
| 865 |
-
def _write_forced_api_video(self, video_bytes: bytes) -> Path:
|
| 866 |
-
output_path = self._make_output_path()
|
| 867 |
-
output_path.write_bytes(video_bytes)
|
| 868 |
-
return output_path
|
| 869 |
-
|
| 870 |
-
@staticmethod
|
| 871 |
-
def _parse_forced_numeric_field(raw_value: str, error_detail: str) -> int:
|
| 872 |
-
try:
|
| 873 |
-
return int(float(raw_value))
|
| 874 |
-
except (TypeError, ValueError):
|
| 875 |
-
raise HTTPError(400, error_detail) from None
|
| 876 |
-
|
| 877 |
-
@staticmethod
|
| 878 |
-
def _parse_audio_flag(audio_value: str | bool) -> bool:
|
| 879 |
-
if isinstance(audio_value, bool):
|
| 880 |
-
return audio_value
|
| 881 |
-
normalized = audio_value.strip().lower()
|
| 882 |
-
return normalized in {"1", "true", "yes", "on"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/keep_models_runtime.py
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
"""仅提供强制卸载 GPU 管线。「保持模型加载」功能已移除。"""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from typing import Any
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
def force_unload_gpu_pipeline(pipelines: Any) -> None:
|
| 9 |
-
"""释放推理管线占用的显存(切换 GPU、清理、LoRA 重建等场景)。"""
|
| 10 |
-
try:
|
| 11 |
-
pipelines.unload_gpu_pipeline()
|
| 12 |
-
except Exception:
|
| 13 |
-
try:
|
| 14 |
-
type(pipelines).unload_gpu_pipeline(pipelines)
|
| 15 |
-
except Exception:
|
| 16 |
-
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/launcher.py
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import sys
|
| 3 |
-
import os
|
| 4 |
-
|
| 5 |
-
patch_dir = r"C:\Users\1-xuanran\Desktop\LTX队列\patches"
|
| 6 |
-
backend_dir = r"C:\Program Files\LTX Desktop\resources\backend"
|
| 7 |
-
|
| 8 |
-
# 防御性清除:强行剥离所有的默认 backend_dir 引用
|
| 9 |
-
sys.path = [p for p in sys.path if p and os.path.normpath(p) != os.path.normpath(backend_dir)]
|
| 10 |
-
sys.path = [p for p in sys.path if p and p != "." and p != ""]
|
| 11 |
-
|
| 12 |
-
# 绝对插队注入:优先搜索 PATCHES_DIR
|
| 13 |
-
sys.path.insert(0, patch_dir)
|
| 14 |
-
sys.path.insert(1, backend_dir)
|
| 15 |
-
|
| 16 |
-
import uvicorn
|
| 17 |
-
from ltx2_server import app
|
| 18 |
-
|
| 19 |
-
if __name__ == '__main__':
|
| 20 |
-
uvicorn.run(app, host="0.0.0.0", port=3000, log_level="info", access_log=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/lora_build_hook.py
DELETED
|
@@ -1,172 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
在 SingleGPUModelBuilder.build() 时合并「当前请求」的用户 LoRA。
|
| 3 |
-
|
| 4 |
-
桌面版 Fast 管线往往只在 model_ledger 上挂 loras,真正 load 权重时仍用
|
| 5 |
-
初始化时的空 loras Builder;此处对 DiT/Transformer 的 Builder 在 build 前注入。
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
import contextvars
|
| 11 |
-
import logging
|
| 12 |
-
from dataclasses import replace
|
| 13 |
-
from typing import Any
|
| 14 |
-
|
| 15 |
-
import torch
|
| 16 |
-
|
| 17 |
-
logger = logging.getLogger(__name__)
|
| 18 |
-
|
| 19 |
-
# 当前 HTTP 请求/生成任务中要额外融合的 LoRA(LoraPathStrengthAndSDOps 元组)
|
| 20 |
-
_pending_user_loras: contextvars.ContextVar[tuple[Any, ...] | None] = contextvars.ContextVar(
|
| 21 |
-
"ltx_pending_user_loras", default=None
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
_HOOK_INSTALLED = False
|
| 25 |
-
_FP8_LORA_PATCH_INSTALLED = False
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def pending_loras_token(loras: tuple[Any, ...] | None):
|
| 29 |
-
"""返回 contextvar Token,供 finally reset;loras 为 None 表示本任务不用额外 LoRA。"""
|
| 30 |
-
return _pending_user_loras.set(loras)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def reset_pending_loras(token: contextvars.Token | None) -> None:
|
| 34 |
-
if token is not None:
|
| 35 |
-
_pending_user_loras.reset(token)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def _get_pending() -> tuple[Any, ...] | None:
|
| 39 |
-
return _pending_user_loras.get()
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def _is_ltx_diffusion_transformer_builder(builder: Any) -> bool:
|
| 43 |
-
"""避免给 Gemma / VAE / Upsampler 的 Builder 误加视频 LoRA。"""
|
| 44 |
-
cfg = getattr(builder, "model_class_configurator", None)
|
| 45 |
-
if cfg is None:
|
| 46 |
-
return False
|
| 47 |
-
name = getattr(cfg, "__name__", "") or ""
|
| 48 |
-
# 排除明显非 DiT 的
|
| 49 |
-
for bad in (
|
| 50 |
-
"Gemma",
|
| 51 |
-
"VideoEncoder",
|
| 52 |
-
"VideoDecoder",
|
| 53 |
-
"AudioEncoder",
|
| 54 |
-
"AudioDecoder",
|
| 55 |
-
"Vocoder",
|
| 56 |
-
"EmbeddingsProcessor",
|
| 57 |
-
"LatentUpsampler",
|
| 58 |
-
):
|
| 59 |
-
if bad in name:
|
| 60 |
-
return False
|
| 61 |
-
try:
|
| 62 |
-
from ltx_core.model.transformer import LTXModelConfigurator
|
| 63 |
-
|
| 64 |
-
if isinstance(cfg, type):
|
| 65 |
-
try:
|
| 66 |
-
if issubclass(cfg, LTXModelConfigurator):
|
| 67 |
-
return True
|
| 68 |
-
except TypeError:
|
| 69 |
-
pass
|
| 70 |
-
if cfg is LTXModelConfigurator:
|
| 71 |
-
return True
|
| 72 |
-
except ImportError:
|
| 73 |
-
pass
|
| 74 |
-
# 兜底:LTX 主 transformer 配置器命名习惯(排除已列出的 VAE/Gemma)
|
| 75 |
-
return "LTX" in name and "ModelConfigurator" in name
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def _install_fp8_lora_fusion_patch() -> None:
|
| 79 |
-
"""Make LTX's scaled-FP8 LoRA fusion tolerant of checkpoint layout variants."""
|
| 80 |
-
global _FP8_LORA_PATCH_INSTALLED
|
| 81 |
-
if _FP8_LORA_PATCH_INSTALLED:
|
| 82 |
-
return
|
| 83 |
-
try:
|
| 84 |
-
import ltx_core.loader.fuse_loras as fuse_mod
|
| 85 |
-
except ImportError:
|
| 86 |
-
return
|
| 87 |
-
|
| 88 |
-
_orig_scaled = getattr(fuse_mod, "_fuse_delta_with_scaled_fp8", None)
|
| 89 |
-
if _orig_scaled is None:
|
| 90 |
-
return
|
| 91 |
-
|
| 92 |
-
def _quantize_preserve_layout(tensor: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 93 |
-
tensor_fp32 = tensor.to(torch.float32)
|
| 94 |
-
fp8_min = torch.finfo(torch.float8_e4m3fn).min
|
| 95 |
-
fp8_max = torch.finfo(torch.float8_e4m3fn).max
|
| 96 |
-
max_abs = torch.amax(torch.abs(tensor_fp32))
|
| 97 |
-
if max_abs == 0:
|
| 98 |
-
max_abs = torch.ones((), dtype=torch.float32, device=tensor_fp32.device)
|
| 99 |
-
scale = fp8_max / max_abs
|
| 100 |
-
quantized = torch.clamp(tensor_fp32 * scale, min=fp8_min, max=fp8_max).to(torch.float8_e4m3fn)
|
| 101 |
-
return quantized, scale.reciprocal()
|
| 102 |
-
|
| 103 |
-
def _patched_scaled(deltas: torch.Tensor, weight: torch.Tensor, key: str, scale_key: str, model_sd: Any) -> dict[str, torch.Tensor]:
|
| 104 |
-
weight_scale = model_sd.sd[scale_key].to(device=weight.device)
|
| 105 |
-
delta = deltas.to(device=weight.device, dtype=torch.float32)
|
| 106 |
-
weight_fp32 = weight.to(torch.float32)
|
| 107 |
-
|
| 108 |
-
# Standard LTX scaled-FP8 layout: checkpoint stores (in, out), LoRA delta is (out, in).
|
| 109 |
-
normal_layout = weight_fp32.t() * weight_scale
|
| 110 |
-
if normal_layout.shape == delta.shape:
|
| 111 |
-
new_weight = normal_layout + delta
|
| 112 |
-
new_fp8_weight, new_weight_scale = fuse_mod.quantize_weight_to_fp8_per_tensor(new_weight)
|
| 113 |
-
return {key: new_fp8_weight, scale_key: new_weight_scale}
|
| 114 |
-
if normal_layout.shape == delta.t().shape:
|
| 115 |
-
new_weight = normal_layout + delta.t()
|
| 116 |
-
new_fp8_weight, new_weight_scale = fuse_mod.quantize_weight_to_fp8_per_tensor(new_weight)
|
| 117 |
-
return {key: new_fp8_weight, scale_key: new_weight_scale}
|
| 118 |
-
|
| 119 |
-
# Some FP8 checkpoints already arrive in the module/storage layout.
|
| 120 |
-
storage_layout = weight_fp32 * weight_scale
|
| 121 |
-
if storage_layout.shape == delta.shape:
|
| 122 |
-
new_weight = storage_layout + delta
|
| 123 |
-
new_fp8_weight, new_weight_scale = _quantize_preserve_layout(new_weight)
|
| 124 |
-
return {key: new_fp8_weight, scale_key: new_weight_scale}
|
| 125 |
-
if storage_layout.shape == delta.t().shape:
|
| 126 |
-
new_weight = storage_layout + delta.t()
|
| 127 |
-
new_fp8_weight, new_weight_scale = _quantize_preserve_layout(new_weight)
|
| 128 |
-
return {key: new_fp8_weight, scale_key: new_weight_scale}
|
| 129 |
-
|
| 130 |
-
print(
|
| 131 |
-
"[PATCH] FP8 LoRA shape mismatch, skip layer: "
|
| 132 |
-
f"{key}, weight={tuple(weight.shape)}, delta={tuple(deltas.shape)}, "
|
| 133 |
-
f"normal={tuple(normal_layout.shape)}, storage={tuple(storage_layout.shape)}"
|
| 134 |
-
)
|
| 135 |
-
return {}
|
| 136 |
-
|
| 137 |
-
fuse_mod._fuse_delta_with_scaled_fp8 = _patched_scaled
|
| 138 |
-
_FP8_LORA_PATCH_INSTALLED = True
|
| 139 |
-
logger.info("lora_build_hook: 已挂载 scaled-FP8 LoRA 融合兼容补丁")
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
def install_lora_build_hook() -> None:
|
| 143 |
-
global _HOOK_INSTALLED
|
| 144 |
-
_install_fp8_lora_fusion_patch()
|
| 145 |
-
if _HOOK_INSTALLED:
|
| 146 |
-
return
|
| 147 |
-
try:
|
| 148 |
-
from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder
|
| 149 |
-
except ImportError:
|
| 150 |
-
logger.warning("lora_build_hook: 无法导入 SingleGPUModelBuilder,跳过")
|
| 151 |
-
return
|
| 152 |
-
|
| 153 |
-
_orig_build = SingleGPUModelBuilder.build
|
| 154 |
-
|
| 155 |
-
def build(self: Any, *args: Any, **kwargs: Any) -> Any:
|
| 156 |
-
extra = _get_pending()
|
| 157 |
-
if extra and _is_ltx_diffusion_transformer_builder(self):
|
| 158 |
-
have = {getattr(x, "path", None) for x in self.loras}
|
| 159 |
-
add = tuple(x for x in extra if getattr(x, "path", None) not in have)
|
| 160 |
-
if add:
|
| 161 |
-
merged = (*tuple(self.loras), *add)
|
| 162 |
-
self = replace(self, loras=merged)
|
| 163 |
-
logger.info(
|
| 164 |
-
"lora_build_hook: 已向 DiT Builder 合并 %d 个用户 LoRA: %s",
|
| 165 |
-
len(add),
|
| 166 |
-
[getattr(x, "path", x) for x in add],
|
| 167 |
-
)
|
| 168 |
-
return _orig_build(self, *args, **kwargs)
|
| 169 |
-
|
| 170 |
-
SingleGPUModelBuilder.build = build # type: ignore[method-assign]
|
| 171 |
-
_HOOK_INSTALLED = True
|
| 172 |
-
logger.info("lora_build_hook: 已挂载 SingleGPUModelBuilder.build")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/lora_injection.py
DELETED
|
@@ -1,139 +0,0 @@
|
|
| 1 |
-
"""将用户 LoRA 注入 Fast 视频管线:兼容 ModelLedger 与 LTX-2 DiffusionStage/Builder。"""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import inspect
|
| 6 |
-
import logging
|
| 7 |
-
from typing import Any
|
| 8 |
-
|
| 9 |
-
logger = logging.getLogger(__name__)
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
def _lora_init_kwargs(
|
| 13 |
-
pipeline_cls: type, loras: list[Any] | tuple[Any, ...]
|
| 14 |
-
) -> dict[str, Any]:
|
| 15 |
-
if not loras:
|
| 16 |
-
return {}
|
| 17 |
-
try:
|
| 18 |
-
sig = inspect.signature(pipeline_cls.__init__)
|
| 19 |
-
names = sig.parameters.keys()
|
| 20 |
-
except (TypeError, ValueError):
|
| 21 |
-
return {}
|
| 22 |
-
tup = tuple(loras)
|
| 23 |
-
for key in ("loras", "lora", "extra_loras", "user_loras"):
|
| 24 |
-
if key in names:
|
| 25 |
-
return {key: tup}
|
| 26 |
-
return {}
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def inject_loras_into_fast_pipeline(ltx_pipe: Any, loras: list[Any] | tuple[Any, ...]) -> int:
|
| 30 |
-
"""在已构造的管线上尽量把 LoRA 写进会参与 build 的 Builder / ledger。返回成功写入的处数。"""
|
| 31 |
-
if not loras:
|
| 32 |
-
return 0
|
| 33 |
-
tup = tuple(loras)
|
| 34 |
-
patched = 0
|
| 35 |
-
visited: set[int] = set()
|
| 36 |
-
|
| 37 |
-
def visit(obj: Any, depth: int) -> None:
|
| 38 |
-
nonlocal patched
|
| 39 |
-
if obj is None or depth > 10:
|
| 40 |
-
return
|
| 41 |
-
oid = id(obj)
|
| 42 |
-
if oid in visited:
|
| 43 |
-
return
|
| 44 |
-
visited.add(oid)
|
| 45 |
-
|
| 46 |
-
# ModelLedger.loras(旧桌面)
|
| 47 |
-
ml = getattr(obj, "model_ledger", None)
|
| 48 |
-
if ml is not None:
|
| 49 |
-
try:
|
| 50 |
-
ml.loras = tup
|
| 51 |
-
patched += 1
|
| 52 |
-
logger.info("LoRA: 已设置 model_ledger.loras")
|
| 53 |
-
except Exception as e:
|
| 54 |
-
logger.debug("model_ledger.loras: %s", e)
|
| 55 |
-
|
| 56 |
-
# SingleGPUModelBuilder.with_loras(常见与变体属性名)
|
| 57 |
-
for holder in (obj, ml):
|
| 58 |
-
if holder is None:
|
| 59 |
-
continue
|
| 60 |
-
candidates: list[Any] = []
|
| 61 |
-
for attr in (
|
| 62 |
-
"_transformer_builder",
|
| 63 |
-
"transformer_builder",
|
| 64 |
-
"_model_builder",
|
| 65 |
-
"model_builder",
|
| 66 |
-
):
|
| 67 |
-
tb = getattr(holder, attr, None)
|
| 68 |
-
if tb is not None:
|
| 69 |
-
candidates.append((attr, tb))
|
| 70 |
-
try:
|
| 71 |
-
for attr in dir(holder):
|
| 72 |
-
al = attr.lower()
|
| 73 |
-
if "transformer" in al and "builder" in al and attr not in (
|
| 74 |
-
"_transformer_builder",
|
| 75 |
-
"transformer_builder",
|
| 76 |
-
):
|
| 77 |
-
tb = getattr(holder, attr, None)
|
| 78 |
-
if tb is not None:
|
| 79 |
-
candidates.append((attr, tb))
|
| 80 |
-
except Exception:
|
| 81 |
-
pass
|
| 82 |
-
for attr, tb in candidates:
|
| 83 |
-
if hasattr(tb, "with_loras"):
|
| 84 |
-
try:
|
| 85 |
-
new_tb = tb.with_loras(tup)
|
| 86 |
-
setattr(holder, attr, new_tb)
|
| 87 |
-
patched += 1
|
| 88 |
-
logger.info("LoRA: 已更新 %s.with_loras", attr)
|
| 89 |
-
except Exception as e:
|
| 90 |
-
logger.debug("with_loras %s: %s", attr, e)
|
| 91 |
-
|
| 92 |
-
# DiffusionStage(类名或 isinstance)
|
| 93 |
-
is_diffusion = type(obj).__name__ == "DiffusionStage"
|
| 94 |
-
if not is_diffusion:
|
| 95 |
-
try:
|
| 96 |
-
from ltx_pipelines.utils.blocks import DiffusionStage as _DS
|
| 97 |
-
|
| 98 |
-
is_diffusion = isinstance(obj, _DS)
|
| 99 |
-
except ImportError:
|
| 100 |
-
pass
|
| 101 |
-
if is_diffusion:
|
| 102 |
-
tb = getattr(obj, "_transformer_builder", None)
|
| 103 |
-
if tb is not None and hasattr(tb, "with_loras"):
|
| 104 |
-
try:
|
| 105 |
-
obj._transformer_builder = tb.with_loras(tup)
|
| 106 |
-
patched += 1
|
| 107 |
-
logger.info("LoRA: 已写入 DiffusionStage._transformer_builder")
|
| 108 |
-
except Exception as e:
|
| 109 |
-
logger.debug("DiffusionStage: %s", e)
|
| 110 |
-
|
| 111 |
-
# 常见嵌套属性
|
| 112 |
-
for name in (
|
| 113 |
-
"pipeline",
|
| 114 |
-
"inner",
|
| 115 |
-
"_inner",
|
| 116 |
-
"fast_pipeline",
|
| 117 |
-
"_pipeline",
|
| 118 |
-
"stage_1",
|
| 119 |
-
"stage_2",
|
| 120 |
-
"stage",
|
| 121 |
-
"_stage",
|
| 122 |
-
"stages",
|
| 123 |
-
"diffusion",
|
| 124 |
-
"_diffusion",
|
| 125 |
-
):
|
| 126 |
-
try:
|
| 127 |
-
ch = getattr(obj, name, None)
|
| 128 |
-
except Exception:
|
| 129 |
-
continue
|
| 130 |
-
if ch is not None and ch is not obj:
|
| 131 |
-
visit(ch, depth + 1)
|
| 132 |
-
|
| 133 |
-
if isinstance(obj, (list, tuple)):
|
| 134 |
-
for item in obj[:8]:
|
| 135 |
-
visit(item, depth + 1)
|
| 136 |
-
|
| 137 |
-
root = getattr(ltx_pipe, "pipeline", ltx_pipe)
|
| 138 |
-
visit(root, 0)
|
| 139 |
-
return patched
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/low_vram_runtime.py
DELETED
|
@@ -1,264 +0,0 @@
|
|
| 1 |
-
"""低显存模式:尽量降峰值显存(以速度换显存);效果取决于官方管线是否支持 offload。"""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import gc
|
| 6 |
-
import logging
|
| 7 |
-
import os
|
| 8 |
-
import types
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
from typing import Any
|
| 11 |
-
|
| 12 |
-
logger = logging.getLogger("ltx_low_vram")
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def _ltx_desktop_config_dir() -> Path:
|
| 16 |
-
p = (
|
| 17 |
-
Path(os.environ.get("LOCALAPPDATA", os.path.expanduser("~/AppData/Local")))
|
| 18 |
-
/ "LTXDesktop"
|
| 19 |
-
)
|
| 20 |
-
p.mkdir(parents=True, exist_ok=True)
|
| 21 |
-
return p.resolve()
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def low_vram_pref_path() -> Path:
|
| 25 |
-
return _ltx_desktop_config_dir() / "low_vram_mode.pref"
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def read_low_vram_pref() -> bool:
|
| 29 |
-
f = low_vram_pref_path()
|
| 30 |
-
if not f.is_file():
|
| 31 |
-
return False
|
| 32 |
-
return f.read_text(encoding="utf-8").strip().lower() in ("1", "true", "yes", "on")
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def write_low_vram_pref(enabled: bool) -> None:
|
| 36 |
-
low_vram_pref_path().write_text(
|
| 37 |
-
"true\n" if enabled else "false\n", encoding="utf-8"
|
| 38 |
-
)
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def apply_low_vram_config_tweaks(handler: Any) -> None:
|
| 42 |
-
"""在官方 RuntimeConfig 上尽量关闭 fast 超分等(若字段存在)。"""
|
| 43 |
-
cfg = getattr(handler, "config", None)
|
| 44 |
-
if cfg is None:
|
| 45 |
-
return
|
| 46 |
-
fm = getattr(cfg, "fast_model", None)
|
| 47 |
-
if fm is None:
|
| 48 |
-
return
|
| 49 |
-
try:
|
| 50 |
-
if hasattr(fm, "model_copy"):
|
| 51 |
-
updated = fm.model_copy(update={"use_upscaler": False})
|
| 52 |
-
setattr(cfg, "fast_model", updated)
|
| 53 |
-
elif hasattr(fm, "use_upscaler"):
|
| 54 |
-
setattr(fm, "use_upscaler", False)
|
| 55 |
-
except Exception as e:
|
| 56 |
-
logger.debug("low_vram: 无法关闭 fast_model.use_upscaler: %s", e)
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def restore_full_vram_config_tweaks(handler: Any) -> None:
|
| 60 |
-
"""显存上限为 0 时恢复速度优先配置。"""
|
| 61 |
-
cfg = getattr(handler, "config", None)
|
| 62 |
-
if cfg is None:
|
| 63 |
-
return
|
| 64 |
-
fm = getattr(cfg, "fast_model", None)
|
| 65 |
-
if fm is None:
|
| 66 |
-
return
|
| 67 |
-
try:
|
| 68 |
-
if hasattr(fm, "model_copy"):
|
| 69 |
-
updated = fm.model_copy(update={"use_upscaler": True})
|
| 70 |
-
setattr(cfg, "fast_model", updated)
|
| 71 |
-
elif hasattr(fm, "use_upscaler"):
|
| 72 |
-
setattr(fm, "use_upscaler", True)
|
| 73 |
-
except Exception as e:
|
| 74 |
-
logger.debug("low_vram: 无法恢复 fast_model.use_upscaler: %s", e)
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def install_low_vram_on_pipelines(handler: Any) -> None:
|
| 78 |
-
"""启动时读取偏好,挂到 pipelines 上供各补丁读取。"""
|
| 79 |
-
pl = handler.pipelines
|
| 80 |
-
low = read_low_vram_pref() and should_use_cpu_offload()
|
| 81 |
-
setattr(pl, "low_vram_mode", bool(low))
|
| 82 |
-
if low:
|
| 83 |
-
apply_low_vram_config_tweaks(handler)
|
| 84 |
-
logger.info(
|
| 85 |
-
"low_vram_mode: 已开启(尝试关闭 fast 超分;若显存仍高,多为权重常驻 GPU,需降分辨率/时长或 FP8 权重)"
|
| 86 |
-
)
|
| 87 |
-
else:
|
| 88 |
-
restore_full_vram_config_tweaks(handler)
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def install_low_vram_pipeline_hooks(pl: Any) -> None:
|
| 92 |
-
"""在 load_gpu_pipeline / load_a2v 返回后尝试 Diffusers 式 CPU offload(无则静默)。"""
|
| 93 |
-
if getattr(pl, "_ltx_low_vram_hooks_installed", False):
|
| 94 |
-
return
|
| 95 |
-
pl._ltx_low_vram_hooks_installed = True
|
| 96 |
-
|
| 97 |
-
if hasattr(pl, "load_gpu_pipeline"):
|
| 98 |
-
_orig_gpu = pl.load_gpu_pipeline
|
| 99 |
-
pl._ltx_orig_load_gpu_for_low_vram = _orig_gpu
|
| 100 |
-
|
| 101 |
-
def _load_gpu_wrapped(self: Any, *a: Any, **kw: Any) -> Any:
|
| 102 |
-
r = _orig_gpu(*a, **kw)
|
| 103 |
-
if getattr(self, "low_vram_mode", False):
|
| 104 |
-
try_sequential_offload_on_pipeline_state(r)
|
| 105 |
-
return r
|
| 106 |
-
|
| 107 |
-
pl.load_gpu_pipeline = types.MethodType(_load_gpu_wrapped, pl)
|
| 108 |
-
|
| 109 |
-
if hasattr(pl, "load_a2v_pipeline"):
|
| 110 |
-
_orig_a2v = pl.load_a2v_pipeline
|
| 111 |
-
pl._ltx_orig_load_a2v_for_low_vram = _orig_a2v
|
| 112 |
-
|
| 113 |
-
def _load_a2v_wrapped(self: Any, *a: Any, **kw: Any) -> Any:
|
| 114 |
-
r = _orig_a2v(*a, **kw)
|
| 115 |
-
if getattr(self, "low_vram_mode", False):
|
| 116 |
-
try_sequential_offload_on_pipeline_state(r)
|
| 117 |
-
return r
|
| 118 |
-
|
| 119 |
-
pl.load_a2v_pipeline = types.MethodType(_load_a2v_wrapped, pl)
|
| 120 |
-
|
| 121 |
-
# Monkey patch: 接管 1.0.3 新增的底层 layer streaming 来实现完美的线性显存控制
|
| 122 |
-
if not getattr(pl, "_ltx_layer_streaming_patched", False):
|
| 123 |
-
pl._ltx_layer_streaming_patched = True
|
| 124 |
-
try:
|
| 125 |
-
def _patch_pipeline_class(cls_name, mod_name):
|
| 126 |
-
import importlib
|
| 127 |
-
try:
|
| 128 |
-
mod = importlib.import_module(mod_name)
|
| 129 |
-
pipeline_cls = getattr(mod, cls_name)
|
| 130 |
-
_orig_call = pipeline_cls.__call__
|
| 131 |
-
|
| 132 |
-
def _patched_call(self, *args, **kwargs):
|
| 133 |
-
lim = get_vram_limit()
|
| 134 |
-
if lim is not None:
|
| 135 |
-
count = get_streaming_prefetch_count()
|
| 136 |
-
kwargs["streaming_prefetch_count"] = count
|
| 137 |
-
if count is None:
|
| 138 |
-
logger.info(
|
| 139 |
-
"low_vram_mode: VRAM limit is unlimited/high. Disabled layer streaming."
|
| 140 |
-
)
|
| 141 |
-
else:
|
| 142 |
-
logger.info(
|
| 143 |
-
"low_vram_mode: Dynamically tuned layer streaming prefetch count to %s for %sGB limit.",
|
| 144 |
-
count,
|
| 145 |
-
lim,
|
| 146 |
-
)
|
| 147 |
-
|
| 148 |
-
return _orig_call(self, *args, **kwargs)
|
| 149 |
-
|
| 150 |
-
pipeline_cls.__call__ = _patched_call
|
| 151 |
-
logger.info(f"low_vram_mode: Successfully patched {cls_name} to override streaming_prefetch_count")
|
| 152 |
-
except Exception as e:
|
| 153 |
-
pass
|
| 154 |
-
|
| 155 |
-
_patch_pipeline_class("DistilledPipeline", "ltx_pipelines.distilled")
|
| 156 |
-
_patch_pipeline_class("TI2VidTwoStagesPipeline", "ltx_pipelines.ti2vid_two_stages")
|
| 157 |
-
_patch_pipeline_class("LTXRetakePipeline", "services.retake_pipeline.ltx_retake_pipeline")
|
| 158 |
-
_patch_pipeline_class("ICLoRAPipeline", "services.ic_lora_pipeline.ltx_ic_lora_pipeline")
|
| 159 |
-
_patch_pipeline_class("A2VPipeline", "services.a2v_pipeline.distilled_a2v_pipeline")
|
| 160 |
-
except Exception:
|
| 161 |
-
pass
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
def get_vram_limit() -> float | None:
|
| 165 |
-
try:
|
| 166 |
-
import json
|
| 167 |
-
from pathlib import Path
|
| 168 |
-
settings_file = _ltx_desktop_config_dir() / "settings.json"
|
| 169 |
-
if settings_file.exists():
|
| 170 |
-
with open(settings_file, "r", encoding="utf-8") as f:
|
| 171 |
-
data = json.load(f)
|
| 172 |
-
if "vram_limit" in data:
|
| 173 |
-
lim = data["vram_limit"]
|
| 174 |
-
if lim != "":
|
| 175 |
-
return float(lim)
|
| 176 |
-
except Exception:
|
| 177 |
-
pass
|
| 178 |
-
return None
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
def get_streaming_prefetch_count() -> int | None:
|
| 182 |
-
"""把设置里的显存上限映射为 layer streaming 强度。
|
| 183 |
-
|
| 184 |
-
``0`` 或留空表示纯 GPU / 速度优先,不启用层流式加载。
|
| 185 |
-
"""
|
| 186 |
-
lim = get_vram_limit()
|
| 187 |
-
if lim is None or lim == 0:
|
| 188 |
-
return None
|
| 189 |
-
if lim <= 10.0:
|
| 190 |
-
return 1
|
| 191 |
-
if lim >= 25.0:
|
| 192 |
-
return None
|
| 193 |
-
extra_gb = float(lim) - 10.0
|
| 194 |
-
return max(1, min(32, 1 + round(extra_gb / 0.67)))
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
def should_use_cpu_offload() -> bool:
|
| 198 |
-
"""只有设置了大于 0 的显存上限时才启用 CPU/offload 慢速兼容路径。"""
|
| 199 |
-
lim = get_vram_limit()
|
| 200 |
-
return lim is not None and lim > 0
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
def try_sequential_offload_on_pipeline_state(state: Any) -> None:
|
| 204 |
-
"""按设定最高显存分配,爆显存后写入系统内存"""
|
| 205 |
-
if state is None:
|
| 206 |
-
return
|
| 207 |
-
if not should_use_cpu_offload():
|
| 208 |
-
logger.info(
|
| 209 |
-
"low_vram_mode: VRAM limit is 0/blank. Skip CPU offload for pure GPU speed."
|
| 210 |
-
)
|
| 211 |
-
return
|
| 212 |
-
root = getattr(state, "pipeline", state)
|
| 213 |
-
candidates: list[Any] = [root]
|
| 214 |
-
inner = getattr(root, "pipeline", None)
|
| 215 |
-
if inner is not None and inner is not root:
|
| 216 |
-
candidates.append(inner)
|
| 217 |
-
|
| 218 |
-
# Capped-VRAM mode applies macro offload so T5/VAE can leave GPU while DiT runs.
|
| 219 |
-
# Pure GPU mode returns above and keeps the old fast path.
|
| 220 |
-
for obj in candidates:
|
| 221 |
-
for method_name in (
|
| 222 |
-
"enable_model_cpu_offload",
|
| 223 |
-
"enable_sequential_cpu_offload",
|
| 224 |
-
):
|
| 225 |
-
fn = getattr(obj, method_name, None)
|
| 226 |
-
if callable(fn):
|
| 227 |
-
try:
|
| 228 |
-
fn()
|
| 229 |
-
logger.info(
|
| 230 |
-
"low_vram_mode: 已对管线调用 %s()",
|
| 231 |
-
method_name,
|
| 232 |
-
)
|
| 233 |
-
return
|
| 234 |
-
except Exception as e:
|
| 235 |
-
logger.debug(
|
| 236 |
-
"low_vram_mode: %s() 失败(可忽略): %s",
|
| 237 |
-
method_name,
|
| 238 |
-
e,
|
| 239 |
-
)
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
def maybe_release_pipeline_after_task(handler: Any) -> None:
|
| 243 |
-
"""单次生成结束后:低显存模式下强制卸载管线并回收缓存。"""
|
| 244 |
-
pl = getattr(handler, "pipelines", None) or getattr(handler, "_pipelines", None)
|
| 245 |
-
if pl is None or not getattr(pl, "low_vram_mode", False):
|
| 246 |
-
return
|
| 247 |
-
try:
|
| 248 |
-
from keep_models_runtime import force_unload_gpu_pipeline
|
| 249 |
-
|
| 250 |
-
force_unload_gpu_pipeline(pl)
|
| 251 |
-
except Exception as e:
|
| 252 |
-
logger.debug("low_vram_mode: 任务后卸载失败: %s", e)
|
| 253 |
-
try:
|
| 254 |
-
pl._pipeline_signature = None
|
| 255 |
-
except Exception:
|
| 256 |
-
pass
|
| 257 |
-
gc.collect()
|
| 258 |
-
try:
|
| 259 |
-
import torch
|
| 260 |
-
|
| 261 |
-
if torch.cuda.is_available():
|
| 262 |
-
torch.cuda.empty_cache()
|
| 263 |
-
except Exception:
|
| 264 |
-
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/ltx_dev_video_pipeline.py
DELETED
|
@@ -1,156 +0,0 @@
|
|
| 1 |
-
"""Patch-side wrapper for LTX dev checkpoints.
|
| 2 |
-
|
| 3 |
-
The desktop Fast wrapper is built around ``DistilledPipeline``. Dev checkpoints
|
| 4 |
-
need the full TI2V two-stage pipeline; otherwise LoRA keys can match the wrong
|
| 5 |
-
stage shape and fail during FP8 fusion.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
from collections.abc import Iterator
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
from typing import Final
|
| 13 |
-
|
| 14 |
-
import torch
|
| 15 |
-
|
| 16 |
-
from api_types import ImageConditioningInput
|
| 17 |
-
from services.ltx_pipeline_common import (
|
| 18 |
-
default_tiling_config,
|
| 19 |
-
encode_video_output,
|
| 20 |
-
video_chunks_number,
|
| 21 |
-
)
|
| 22 |
-
from services.services_utils import AudioOrNone, TilingConfigType, device_supports_fp8
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
class LTXDevVideoPipeline:
|
| 26 |
-
pipeline_kind: Final = "dev"
|
| 27 |
-
|
| 28 |
-
def __init__(
|
| 29 |
-
self,
|
| 30 |
-
checkpoint_path: str,
|
| 31 |
-
gemma_root: str | None,
|
| 32 |
-
upsampler_path: str,
|
| 33 |
-
distilled_lora_path: str,
|
| 34 |
-
device: torch.device,
|
| 35 |
-
loras: list[object] | tuple[object, ...] | None = None,
|
| 36 |
-
) -> None:
|
| 37 |
-
from ltx_core.loader import LoraPathStrengthAndSDOps
|
| 38 |
-
from ltx_core.quantization import QuantizationPolicy
|
| 39 |
-
from ltx_pipelines.ti2vid_two_stages import TI2VidTwoStagesPipeline
|
| 40 |
-
from ltx_pipelines.utils.constants import detect_params
|
| 41 |
-
|
| 42 |
-
self._checkpoint_path = checkpoint_path
|
| 43 |
-
self._device = device
|
| 44 |
-
self._params = detect_params(checkpoint_path)
|
| 45 |
-
|
| 46 |
-
quantization = None
|
| 47 |
-
if "fp8" in checkpoint_path.lower() and device_supports_fp8(device):
|
| 48 |
-
try:
|
| 49 |
-
quantization = QuantizationPolicy.fp8_scaled_mm()
|
| 50 |
-
except Exception as exc:
|
| 51 |
-
print(f"[PATCH] Dev FP8 scaled-mm 不可用,回退 fp8_cast: {exc}")
|
| 52 |
-
quantization = QuantizationPolicy.fp8_cast()
|
| 53 |
-
|
| 54 |
-
distilled_lora = []
|
| 55 |
-
checkpoint_name = Path(checkpoint_path).name.lower()
|
| 56 |
-
distilled_lora_name = Path(distilled_lora_path).name.lower() if distilled_lora_path else ""
|
| 57 |
-
incompatible_builtin_lora = (
|
| 58 |
-
"2.3" in checkpoint_name
|
| 59 |
-
and ("2-19b" in distilled_lora_name or "19b" in distilled_lora_name)
|
| 60 |
-
)
|
| 61 |
-
if incompatible_builtin_lora:
|
| 62 |
-
print(
|
| 63 |
-
"[PATCH] Dev two-stage: 跳过不匹配的内置 distilled LoRA "
|
| 64 |
-
f"({distilled_lora_name}),当前 checkpoint 是 {checkpoint_name}"
|
| 65 |
-
)
|
| 66 |
-
elif distilled_lora_path and Path(distilled_lora_path).is_file():
|
| 67 |
-
distilled_lora = [
|
| 68 |
-
LoraPathStrengthAndSDOps(
|
| 69 |
-
path=distilled_lora_path,
|
| 70 |
-
strength=1.0,
|
| 71 |
-
sd_ops=None,
|
| 72 |
-
)
|
| 73 |
-
]
|
| 74 |
-
elif distilled_lora_path:
|
| 75 |
-
print(
|
| 76 |
-
"[PATCH] Dev two-stage: distilled LoRA 不存在,跳过内置 stage-2 distilled LoRA: "
|
| 77 |
-
f"{distilled_lora_path}"
|
| 78 |
-
)
|
| 79 |
-
|
| 80 |
-
self.pipeline = TI2VidTwoStagesPipeline(
|
| 81 |
-
checkpoint_path=checkpoint_path,
|
| 82 |
-
distilled_lora=distilled_lora,
|
| 83 |
-
spatial_upsampler_path=upsampler_path,
|
| 84 |
-
gemma_root=gemma_root or "",
|
| 85 |
-
loras=tuple(loras or ()),
|
| 86 |
-
device=device,
|
| 87 |
-
quantization=quantization,
|
| 88 |
-
)
|
| 89 |
-
|
| 90 |
-
def _run_inference(
|
| 91 |
-
self,
|
| 92 |
-
prompt: str,
|
| 93 |
-
seed: int,
|
| 94 |
-
height: int,
|
| 95 |
-
width: int,
|
| 96 |
-
num_frames: int,
|
| 97 |
-
frame_rate: float,
|
| 98 |
-
images: list[ImageConditioningInput],
|
| 99 |
-
tiling_config: TilingConfigType,
|
| 100 |
-
) -> tuple[torch.Tensor | Iterator[torch.Tensor], AudioOrNone]:
|
| 101 |
-
from ltx_pipelines.utils.args import ImageConditioningInput as _LtxImageInput
|
| 102 |
-
try:
|
| 103 |
-
from low_vram_runtime import get_streaming_prefetch_count
|
| 104 |
-
|
| 105 |
-
streaming_prefetch_count = get_streaming_prefetch_count()
|
| 106 |
-
except Exception:
|
| 107 |
-
streaming_prefetch_count = None
|
| 108 |
-
|
| 109 |
-
params = self._params
|
| 110 |
-
return self.pipeline(
|
| 111 |
-
prompt=prompt,
|
| 112 |
-
negative_prompt="",
|
| 113 |
-
seed=seed,
|
| 114 |
-
height=height,
|
| 115 |
-
width=width,
|
| 116 |
-
num_frames=num_frames,
|
| 117 |
-
frame_rate=frame_rate,
|
| 118 |
-
num_inference_steps=params.num_inference_steps,
|
| 119 |
-
video_guider_params=params.video_guider_params,
|
| 120 |
-
audio_guider_params=params.audio_guider_params,
|
| 121 |
-
images=[_LtxImageInput(img.path, img.frame_idx, img.strength) for img in images],
|
| 122 |
-
tiling_config=tiling_config,
|
| 123 |
-
streaming_prefetch_count=streaming_prefetch_count,
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
@torch.inference_mode()
|
| 127 |
-
def generate(
|
| 128 |
-
self,
|
| 129 |
-
prompt: str,
|
| 130 |
-
seed: int,
|
| 131 |
-
height: int,
|
| 132 |
-
width: int,
|
| 133 |
-
num_frames: int,
|
| 134 |
-
frame_rate: float,
|
| 135 |
-
images: list[ImageConditioningInput],
|
| 136 |
-
output_path: str,
|
| 137 |
-
) -> None:
|
| 138 |
-
tiling_config = default_tiling_config()
|
| 139 |
-
video, audio = self._run_inference(
|
| 140 |
-
prompt=prompt,
|
| 141 |
-
seed=seed,
|
| 142 |
-
height=height,
|
| 143 |
-
width=width,
|
| 144 |
-
num_frames=num_frames,
|
| 145 |
-
frame_rate=frame_rate,
|
| 146 |
-
images=images,
|
| 147 |
-
tiling_config=tiling_config,
|
| 148 |
-
)
|
| 149 |
-
chunks = video_chunks_number(num_frames, tiling_config)
|
| 150 |
-
encode_video_output(
|
| 151 |
-
video=video,
|
| 152 |
-
audio=audio,
|
| 153 |
-
fps=int(frame_rate),
|
| 154 |
-
output_path=output_path,
|
| 155 |
-
video_chunks_number_value=chunks,
|
| 156 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/ltx_fp8_video_pipeline.py
DELETED
|
@@ -1,269 +0,0 @@
|
|
| 1 |
-
"""Fast pipeline wrapper for pre-quantized FP8 distilled checkpoints.
|
| 2 |
-
|
| 3 |
-
The stock desktop wrapper uses ``QuantizationPolicy.fp8_cast()``, which is
|
| 4 |
-
meant to cast BF16 checkpoints to FP8 while loading. Pre-quantized FP8
|
| 5 |
-
checkpoints already contain FP8 weights plus weight/input scales, so casting
|
| 6 |
-
them again can produce valid-looking inference that decodes to black frames.
|
| 7 |
-
|
| 8 |
-
This wrapper loads those checkpoints with the scaled-FP8 state-dict/module
|
| 9 |
-
layout. When TensorRT-LLM is not available, its FP8Linear forward falls back
|
| 10 |
-
to a PyTorch dequantize-then-linear path, keeping FP8 storage while avoiding
|
| 11 |
-
black output.
|
| 12 |
-
"""
|
| 13 |
-
|
| 14 |
-
from __future__ import annotations
|
| 15 |
-
|
| 16 |
-
from collections.abc import Iterator
|
| 17 |
-
import os
|
| 18 |
-
from types import SimpleNamespace
|
| 19 |
-
from typing import Any, Final, cast
|
| 20 |
-
|
| 21 |
-
import torch
|
| 22 |
-
from torch import nn
|
| 23 |
-
|
| 24 |
-
from api_types import ImageConditioningInput
|
| 25 |
-
from services.ltx_pipeline_common import (
|
| 26 |
-
default_tiling_config,
|
| 27 |
-
encode_video_output,
|
| 28 |
-
video_chunks_number,
|
| 29 |
-
)
|
| 30 |
-
from services.services_utils import AudioOrNone, TilingConfigType
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
_FP8_FALLBACK_INSTALLED = False
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def _install_fp8linear_torch_fallback() -> None:
|
| 37 |
-
global _FP8_FALLBACK_INSTALLED
|
| 38 |
-
if _FP8_FALLBACK_INSTALLED:
|
| 39 |
-
return
|
| 40 |
-
|
| 41 |
-
from ltx_core.quantization.fp8_scaled_mm import FP8Linear
|
| 42 |
-
|
| 43 |
-
def _fallback_forward(self: Any, x: torch.Tensor) -> torch.Tensor:
|
| 44 |
-
weight_scale = self.weight_scale.to(dtype=x.dtype, device=x.device)
|
| 45 |
-
weight = (self.weight.to(dtype=x.dtype) * weight_scale).t().contiguous()
|
| 46 |
-
bias = self.bias
|
| 47 |
-
if bias is not None and bias.dtype != x.dtype:
|
| 48 |
-
bias = bias.to(dtype=x.dtype, device=x.device)
|
| 49 |
-
return torch.nn.functional.linear(x, weight, bias)
|
| 50 |
-
|
| 51 |
-
# TensorRT-LLM is not bundled in LTX Desktop, so use a deterministic PyTorch
|
| 52 |
-
# fallback for scaled FP8 checkpoints instead of silently ignoring scales.
|
| 53 |
-
FP8Linear.forward = _fallback_forward # type: ignore[method-assign]
|
| 54 |
-
_FP8_FALLBACK_INSTALLED = True
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def _fp8_layer_names(checkpoint_path: str) -> frozenset[str]:
|
| 58 |
-
from safetensors import safe_open
|
| 59 |
-
|
| 60 |
-
names: set[str] = set()
|
| 61 |
-
with safe_open(checkpoint_path, framework="pt", device="cpu") as handle:
|
| 62 |
-
for key in handle.keys():
|
| 63 |
-
if not key.endswith(".weight_scale"):
|
| 64 |
-
continue
|
| 65 |
-
layer_name = key.removeprefix("model.diffusion_model.").removesuffix(
|
| 66 |
-
".weight_scale"
|
| 67 |
-
)
|
| 68 |
-
if layer_name.startswith("transformer_blocks."):
|
| 69 |
-
names.add(layer_name)
|
| 70 |
-
return frozenset(names)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def _scaled_fp8_quantization_policy(checkpoint_path: str) -> Any:
|
| 74 |
-
from ltx_core.loader.module_ops import ModuleOps
|
| 75 |
-
from ltx_core.loader.sd_ops import KeyValueOperationResult, SDOps
|
| 76 |
-
from ltx_core.model.transformer import LTXModel
|
| 77 |
-
from ltx_core.quantization.fp8_scaled_mm import FP8Linear
|
| 78 |
-
|
| 79 |
-
fp8_layers = _fp8_layer_names(checkpoint_path)
|
| 80 |
-
|
| 81 |
-
def transpose_fp8_weight(
|
| 82 |
-
key: str, value: torch.Tensor
|
| 83 |
-
) -> list[KeyValueOperationResult]:
|
| 84 |
-
layer_name = key.removesuffix(".weight")
|
| 85 |
-
if layer_name in fp8_layers and value.dim() == 2:
|
| 86 |
-
return [KeyValueOperationResult(key, value.t())]
|
| 87 |
-
return [KeyValueOperationResult(key, value)]
|
| 88 |
-
|
| 89 |
-
def convert_fp8_layers(model: nn.Module) -> nn.Module:
|
| 90 |
-
if not isinstance(model, LTXModel):
|
| 91 |
-
return model
|
| 92 |
-
replacements: list[tuple[nn.Module, str, nn.Linear]] = []
|
| 93 |
-
for name, module in model.named_modules():
|
| 94 |
-
if name not in fp8_layers or not isinstance(module, nn.Linear):
|
| 95 |
-
continue
|
| 96 |
-
parent_name, attr_name = name.rsplit(".", 1)
|
| 97 |
-
replacements.append((model.get_submodule(parent_name), attr_name, module))
|
| 98 |
-
for parent, attr_name, linear in replacements:
|
| 99 |
-
setattr(
|
| 100 |
-
parent,
|
| 101 |
-
attr_name,
|
| 102 |
-
FP8Linear(
|
| 103 |
-
in_features=linear.in_features,
|
| 104 |
-
out_features=linear.out_features,
|
| 105 |
-
bias=linear.bias is not None,
|
| 106 |
-
device=linear.weight.device,
|
| 107 |
-
),
|
| 108 |
-
)
|
| 109 |
-
return model
|
| 110 |
-
|
| 111 |
-
_install_fp8linear_torch_fallback()
|
| 112 |
-
return SimpleNamespace(
|
| 113 |
-
sd_ops=SDOps("fp8_selected_layers_transpose").with_kv_operation(
|
| 114 |
-
transpose_fp8_weight,
|
| 115 |
-
key_prefix="transformer_blocks.",
|
| 116 |
-
key_suffix=".weight",
|
| 117 |
-
),
|
| 118 |
-
module_ops=(
|
| 119 |
-
ModuleOps(
|
| 120 |
-
name="fp8_prepare_selected_layers_for_loading",
|
| 121 |
-
matcher=lambda model: isinstance(model, LTXModel),
|
| 122 |
-
mutator=convert_fp8_layers,
|
| 123 |
-
),
|
| 124 |
-
),
|
| 125 |
-
)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
class LTXFp8VideoPipeline:
|
| 129 |
-
pipeline_kind: Final = "fast-fp8"
|
| 130 |
-
|
| 131 |
-
@staticmethod
|
| 132 |
-
def create(
|
| 133 |
-
checkpoint_path: str,
|
| 134 |
-
gemma_root: str | None,
|
| 135 |
-
upsampler_path: str,
|
| 136 |
-
device: torch.device,
|
| 137 |
-
) -> "LTXFp8VideoPipeline":
|
| 138 |
-
return LTXFp8VideoPipeline(
|
| 139 |
-
checkpoint_path=checkpoint_path,
|
| 140 |
-
gemma_root=gemma_root,
|
| 141 |
-
upsampler_path=upsampler_path,
|
| 142 |
-
device=device,
|
| 143 |
-
)
|
| 144 |
-
|
| 145 |
-
def __init__(
|
| 146 |
-
self,
|
| 147 |
-
checkpoint_path: str,
|
| 148 |
-
gemma_root: str | None,
|
| 149 |
-
upsampler_path: str,
|
| 150 |
-
device: torch.device,
|
| 151 |
-
**_ignored: Any,
|
| 152 |
-
) -> None:
|
| 153 |
-
from ltx_pipelines.distilled import DistilledPipeline
|
| 154 |
-
|
| 155 |
-
self._checkpoint_path = checkpoint_path
|
| 156 |
-
self._gemma_root = gemma_root
|
| 157 |
-
self._upsampler_path = upsampler_path
|
| 158 |
-
self._device = device
|
| 159 |
-
self._quantization = _scaled_fp8_quantization_policy(checkpoint_path)
|
| 160 |
-
|
| 161 |
-
self.pipeline = DistilledPipeline(
|
| 162 |
-
distilled_checkpoint_path=checkpoint_path,
|
| 163 |
-
gemma_root=cast(str, gemma_root),
|
| 164 |
-
spatial_upsampler_path=upsampler_path,
|
| 165 |
-
loras=[],
|
| 166 |
-
device=device,
|
| 167 |
-
quantization=self._quantization,
|
| 168 |
-
)
|
| 169 |
-
|
| 170 |
-
def _run_inference(
|
| 171 |
-
self,
|
| 172 |
-
prompt: str,
|
| 173 |
-
seed: int,
|
| 174 |
-
height: int,
|
| 175 |
-
width: int,
|
| 176 |
-
num_frames: int,
|
| 177 |
-
frame_rate: float,
|
| 178 |
-
images: list[ImageConditioningInput],
|
| 179 |
-
tiling_config: TilingConfigType,
|
| 180 |
-
) -> tuple[torch.Tensor | Iterator[torch.Tensor], AudioOrNone]:
|
| 181 |
-
from ltx_pipelines.utils.args import ImageConditioningInput as _LtxImageInput
|
| 182 |
-
|
| 183 |
-
return self.pipeline(
|
| 184 |
-
prompt=prompt,
|
| 185 |
-
seed=seed,
|
| 186 |
-
height=height,
|
| 187 |
-
width=width,
|
| 188 |
-
num_frames=num_frames,
|
| 189 |
-
frame_rate=frame_rate,
|
| 190 |
-
images=[
|
| 191 |
-
_LtxImageInput(img.path, img.frame_idx, img.strength)
|
| 192 |
-
for img in images
|
| 193 |
-
],
|
| 194 |
-
tiling_config=tiling_config,
|
| 195 |
-
streaming_prefetch_count=2,
|
| 196 |
-
)
|
| 197 |
-
|
| 198 |
-
@torch.inference_mode()
|
| 199 |
-
def generate(
|
| 200 |
-
self,
|
| 201 |
-
prompt: str,
|
| 202 |
-
seed: int,
|
| 203 |
-
height: int,
|
| 204 |
-
width: int,
|
| 205 |
-
num_frames: int,
|
| 206 |
-
frame_rate: float,
|
| 207 |
-
images: list[ImageConditioningInput],
|
| 208 |
-
output_path: str,
|
| 209 |
-
) -> None:
|
| 210 |
-
tiling_config = default_tiling_config()
|
| 211 |
-
video, audio = self._run_inference(
|
| 212 |
-
prompt=prompt,
|
| 213 |
-
seed=seed,
|
| 214 |
-
height=height,
|
| 215 |
-
width=width,
|
| 216 |
-
num_frames=num_frames,
|
| 217 |
-
frame_rate=frame_rate,
|
| 218 |
-
images=images,
|
| 219 |
-
tiling_config=tiling_config,
|
| 220 |
-
)
|
| 221 |
-
chunks = video_chunks_number(num_frames, tiling_config)
|
| 222 |
-
encode_video_output(
|
| 223 |
-
video=video,
|
| 224 |
-
audio=audio,
|
| 225 |
-
fps=int(frame_rate),
|
| 226 |
-
output_path=output_path,
|
| 227 |
-
video_chunks_number_value=chunks,
|
| 228 |
-
)
|
| 229 |
-
|
| 230 |
-
@torch.inference_mode()
|
| 231 |
-
def warmup(self, output_path: str) -> None:
|
| 232 |
-
warmup_frames = 9
|
| 233 |
-
tiling_config = default_tiling_config()
|
| 234 |
-
|
| 235 |
-
try:
|
| 236 |
-
video, audio = self._run_inference(
|
| 237 |
-
prompt="test warmup",
|
| 238 |
-
seed=42,
|
| 239 |
-
height=256,
|
| 240 |
-
width=384,
|
| 241 |
-
num_frames=warmup_frames,
|
| 242 |
-
frame_rate=8,
|
| 243 |
-
images=[],
|
| 244 |
-
tiling_config=tiling_config,
|
| 245 |
-
)
|
| 246 |
-
chunks = video_chunks_number(warmup_frames, tiling_config)
|
| 247 |
-
encode_video_output(
|
| 248 |
-
video=video,
|
| 249 |
-
audio=audio,
|
| 250 |
-
fps=8,
|
| 251 |
-
output_path=output_path,
|
| 252 |
-
video_chunks_number_value=chunks,
|
| 253 |
-
)
|
| 254 |
-
finally:
|
| 255 |
-
if os.path.exists(output_path):
|
| 256 |
-
os.unlink(output_path)
|
| 257 |
-
|
| 258 |
-
def compile_transformer(self) -> None:
|
| 259 |
-
from ltx_pipelines.distilled import DistilledPipeline
|
| 260 |
-
|
| 261 |
-
self.pipeline = DistilledPipeline(
|
| 262 |
-
distilled_checkpoint_path=self._checkpoint_path,
|
| 263 |
-
gemma_root=cast(str, self._gemma_root),
|
| 264 |
-
spatial_upsampler_path=self._upsampler_path,
|
| 265 |
-
loras=[],
|
| 266 |
-
device=self._device,
|
| 267 |
-
quantization=self._quantization,
|
| 268 |
-
torch_compile=True,
|
| 269 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/runtime_policy.py
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
"""Runtime policy decisions for forced API mode."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
def decide_force_api_generations(
|
| 7 |
-
system: str, cuda_available: bool, vram_gb: int | None
|
| 8 |
-
) -> bool:
|
| 9 |
-
"""Return whether API-only generation must be forced for this runtime."""
|
| 10 |
-
if system == "Darwin":
|
| 11 |
-
return True
|
| 12 |
-
|
| 13 |
-
if system in ("Windows", "Linux"):
|
| 14 |
-
if not cuda_available:
|
| 15 |
-
return True
|
| 16 |
-
if vram_gb is None:
|
| 17 |
-
return True
|
| 18 |
-
return vram_gb < 6
|
| 19 |
-
|
| 20 |
-
# Fail closed for non-target platforms unless explicitly relaxed.
|
| 21 |
-
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/settings.json
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"use_torch_compile": false,
|
| 3 |
-
"load_on_startup": false,
|
| 4 |
-
"ltx_api_key": "",
|
| 5 |
-
"user_prefers_ltx_api_video_generations": false,
|
| 6 |
-
"fal_api_key": "",
|
| 7 |
-
"use_local_text_encoder": true,
|
| 8 |
-
"fast_model": {
|
| 9 |
-
"use_upscaler": true
|
| 10 |
-
},
|
| 11 |
-
"pro_model": {
|
| 12 |
-
"steps": 20,
|
| 13 |
-
"use_upscaler": true
|
| 14 |
-
},
|
| 15 |
-
"prompt_cache_size": 100,
|
| 16 |
-
"prompt_enhancer_enabled_t2v": true,
|
| 17 |
-
"prompt_enhancer_enabled_i2v": false,
|
| 18 |
-
"gemini_api_key": "",
|
| 19 |
-
"seed_locked": false,
|
| 20 |
-
"locked_seed": 42,
|
| 21 |
-
"models_dir": "",
|
| 22 |
-
"lora_dir": ""
|
| 23 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/patches/tts_worker.py
DELETED
|
@@ -1,222 +0,0 @@
|
|
| 1 |
-
"""Standalone TTS worker process for VoxCPM-based generation."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
# === [核心修复] 彻底封印 PyTorch 的所有动态编译机制 ===
|
| 6 |
-
import os
|
| 7 |
-
# 1. 禁用 Dynamo 编译器 (PyTorch 2.x)
|
| 8 |
-
os.environ["TORCH_COMPILE_DISABLE"] = "1"
|
| 9 |
-
# 2. 禁用 TorchScript JIT 编译器 (解决 nvrtc 报错)
|
| 10 |
-
os.environ["PYTORCH_JIT"] = "0"
|
| 11 |
-
# 3. 禁用底层算子融合器 NvFuser
|
| 12 |
-
os.environ["NVFUSER_DISABLE"] = "1"
|
| 13 |
-
|
| 14 |
-
import torch
|
| 15 |
-
import torch._dynamo
|
| 16 |
-
torch._dynamo.config.disable = True
|
| 17 |
-
|
| 18 |
-
# 如果环境支持,强行在代码层关闭 nvfuser
|
| 19 |
-
try:
|
| 20 |
-
if hasattr(torch._C, '_jit_set_nvfuser_enabled'):
|
| 21 |
-
torch._C._jit_set_nvfuser_enabled(False)
|
| 22 |
-
except Exception:
|
| 23 |
-
pass
|
| 24 |
-
# ==============================================================
|
| 25 |
-
|
| 26 |
-
import argparse
|
| 27 |
-
import json
|
| 28 |
-
import tempfile
|
| 29 |
-
from pathlib import Path
|
| 30 |
-
|
| 31 |
-
import numpy as np
|
| 32 |
-
import soundfile as sf
|
| 33 |
-
|
| 34 |
-
_MODEL_CACHE: dict[str, object] = {}
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def _to_1d_float32(audio: np.ndarray) -> np.ndarray:
|
| 40 |
-
arr = np.asarray(audio)
|
| 41 |
-
orig_dtype = arr.dtype
|
| 42 |
-
|
| 43 |
-
if arr.ndim == 0:
|
| 44 |
-
arr = arr.reshape(1)
|
| 45 |
-
elif arr.ndim == 2:
|
| 46 |
-
# Prefer channel-average while keeping the time axis.
|
| 47 |
-
if arr.shape[0] <= 8 and arr.shape[1] > arr.shape[0]:
|
| 48 |
-
arr = arr.mean(axis=0)
|
| 49 |
-
else:
|
| 50 |
-
arr = arr.mean(axis=1)
|
| 51 |
-
elif arr.ndim > 2:
|
| 52 |
-
arr = np.squeeze(arr)
|
| 53 |
-
if arr.ndim != 1:
|
| 54 |
-
arr = arr.reshape(-1)
|
| 55 |
-
|
| 56 |
-
if np.issubdtype(orig_dtype, np.integer):
|
| 57 |
-
scale = float(max(abs(np.iinfo(orig_dtype).min), np.iinfo(orig_dtype).max))
|
| 58 |
-
arr = arr.astype(np.float32) / max(scale, 1.0)
|
| 59 |
-
else:
|
| 60 |
-
arr = arr.astype(np.float32, copy=False)
|
| 61 |
-
|
| 62 |
-
arr = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)
|
| 63 |
-
if arr.size == 0:
|
| 64 |
-
return np.zeros(1, dtype=np.float32)
|
| 65 |
-
|
| 66 |
-
# Remove obvious DC offset.
|
| 67 |
-
arr = arr - float(np.mean(arr))
|
| 68 |
-
return arr
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def _resample_linear(audio: np.ndarray, src_sr: int, dst_sr: int) -> np.ndarray:
|
| 72 |
-
if src_sr == dst_sr:
|
| 73 |
-
return audio
|
| 74 |
-
if audio.size <= 1:
|
| 75 |
-
return audio
|
| 76 |
-
dst_len = max(1, int(round(audio.size * float(dst_sr) / float(src_sr))))
|
| 77 |
-
x_old = np.arange(audio.size, dtype=np.float64)
|
| 78 |
-
x_new = np.linspace(0.0, float(audio.size - 1), dst_len, dtype=np.float64)
|
| 79 |
-
out = np.interp(x_new, x_old, audio.astype(np.float64))
|
| 80 |
-
return out.astype(np.float32)
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def _read_audio_any(path: str) -> tuple[np.ndarray, int]:
|
| 84 |
-
try:
|
| 85 |
-
data, sr = sf.read(path, always_2d=False)
|
| 86 |
-
return np.asarray(data), int(sr)
|
| 87 |
-
except Exception:
|
| 88 |
-
try:
|
| 89 |
-
import librosa
|
| 90 |
-
except Exception as exc:
|
| 91 |
-
raise RuntimeError(
|
| 92 |
-
"参考音频无法解码(建议上传 WAV,或安装 librosa 以支持更多格式)"
|
| 93 |
-
) from exc
|
| 94 |
-
data, sr = librosa.load(path, sr=None, mono=False)
|
| 95 |
-
return np.asarray(data), int(sr)
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
def _prepare_reference_audio(
|
| 99 |
-
path: str, out_dir: Path, target_sr: int, stem: str
|
| 100 |
-
) -> str:
|
| 101 |
-
data, sr = _read_audio_any(path)
|
| 102 |
-
mono = _to_1d_float32(data)
|
| 103 |
-
mono = _resample_linear(mono, sr, target_sr)
|
| 104 |
-
|
| 105 |
-
peak = float(np.max(np.abs(mono))) if mono.size else 0.0
|
| 106 |
-
if peak > 0:
|
| 107 |
-
mono = mono / peak * 0.95
|
| 108 |
-
|
| 109 |
-
out_path = out_dir / f"{stem}.wav"
|
| 110 |
-
sf.write(str(out_path), mono, target_sr, subtype="PCM_16")
|
| 111 |
-
return str(out_path)
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def _normalize_generated_audio(wav: object) -> np.ndarray:
|
| 115 |
-
if hasattr(wav, "detach") and callable(getattr(wav, "detach")):
|
| 116 |
-
wav = wav.detach().cpu().numpy()
|
| 117 |
-
arr = _to_1d_float32(np.asarray(wav))
|
| 118 |
-
|
| 119 |
-
peak = float(np.max(np.abs(arr))) if arr.size else 0.0
|
| 120 |
-
if peak <= 1e-9:
|
| 121 |
-
return np.zeros(1, dtype=np.float32)
|
| 122 |
-
|
| 123 |
-
# Prevent clipping/noise if model output scale drifts.
|
| 124 |
-
if peak > 1.0:
|
| 125 |
-
arr = arr / peak
|
| 126 |
-
arr = np.clip(arr, -0.98, 0.98)
|
| 127 |
-
return arr.astype(np.float32)
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
def _get_model(model_dir: str):
|
| 131 |
-
if model_dir not in _MODEL_CACHE:
|
| 132 |
-
from voxcpm import VoxCPM
|
| 133 |
-
|
| 134 |
-
_MODEL_CACHE[model_dir] = VoxCPM.from_pretrained(model_dir, load_denoiser=False)
|
| 135 |
-
return _MODEL_CACHE[model_dir]
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
def run_generate(req: dict[str, object]) -> dict[str, object]:
|
| 139 |
-
text = str(req.get("text") or "").strip()
|
| 140 |
-
if not text:
|
| 141 |
-
raise RuntimeError("text 不能为空")
|
| 142 |
-
|
| 143 |
-
mode = str(req.get("mode") or "text_only").strip() or "text_only"
|
| 144 |
-
model_dir = str(req.get("model_dir") or "").strip()
|
| 145 |
-
output_dir = Path(str(req.get("output_dir") or ".")).resolve()
|
| 146 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 147 |
-
|
| 148 |
-
cfg_value = float(req.get("cfg_value") or 2.0)
|
| 149 |
-
inference_timesteps = int(req.get("inference_timesteps") or 10)
|
| 150 |
-
|
| 151 |
-
model = _get_model(model_dir)
|
| 152 |
-
sample_rate = int(getattr(getattr(model, "tts_model", None), "sample_rate", 24000))
|
| 153 |
-
|
| 154 |
-
ref_in = req.get("reference_wav_path")
|
| 155 |
-
prompt_in = req.get("prompt_wav_path")
|
| 156 |
-
prompt_text = str(req.get("prompt_text") or "")
|
| 157 |
-
|
| 158 |
-
temp_dir = Path(tempfile.mkdtemp(prefix="ltx_tts_"))
|
| 159 |
-
ref_ready = None
|
| 160 |
-
prompt_ready = None
|
| 161 |
-
try:
|
| 162 |
-
if isinstance(ref_in, str) and ref_in.strip():
|
| 163 |
-
ref_ready = _prepare_reference_audio(
|
| 164 |
-
ref_in.strip(), temp_dir, sample_rate, "reference"
|
| 165 |
-
)
|
| 166 |
-
if isinstance(prompt_in, str) and prompt_in.strip():
|
| 167 |
-
prompt_ready = _prepare_reference_audio(
|
| 168 |
-
prompt_in.strip(), temp_dir, sample_rate, "prompt"
|
| 169 |
-
)
|
| 170 |
-
|
| 171 |
-
if mode in {"clone", "ultimate_clone"} and not ref_ready:
|
| 172 |
-
raise RuntimeError("克隆模式必须提供参考音频")
|
| 173 |
-
|
| 174 |
-
gen_kwargs: dict[str, object] = {
|
| 175 |
-
"text": text,
|
| 176 |
-
"cfg_value": cfg_value,
|
| 177 |
-
"inference_timesteps": inference_timesteps,
|
| 178 |
-
}
|
| 179 |
-
if mode == "clone":
|
| 180 |
-
gen_kwargs["reference_wav_path"] = ref_ready
|
| 181 |
-
elif mode == "ultimate_clone":
|
| 182 |
-
gen_kwargs["reference_wav_path"] = ref_ready
|
| 183 |
-
if prompt_ready:
|
| 184 |
-
gen_kwargs["prompt_wav_path"] = prompt_ready
|
| 185 |
-
if prompt_text:
|
| 186 |
-
gen_kwargs["prompt_text"] = prompt_text
|
| 187 |
-
|
| 188 |
-
wav = model.generate(**gen_kwargs)
|
| 189 |
-
out = _normalize_generated_audio(wav)
|
| 190 |
-
|
| 191 |
-
import uuid
|
| 192 |
-
|
| 193 |
-
fname = f"tts_{uuid.uuid4().hex[:8]}.wav"
|
| 194 |
-
out_path = output_dir / fname
|
| 195 |
-
sf.write(str(out_path), out, sample_rate, subtype="PCM_16")
|
| 196 |
-
return {"status": "complete", "audio_path": fname, "sample_rate": sample_rate}
|
| 197 |
-
finally:
|
| 198 |
-
try:
|
| 199 |
-
for p in temp_dir.glob("*"):
|
| 200 |
-
try:
|
| 201 |
-
p.unlink()
|
| 202 |
-
except Exception:
|
| 203 |
-
pass
|
| 204 |
-
temp_dir.rmdir()
|
| 205 |
-
except Exception:
|
| 206 |
-
pass
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
def main() -> int:
|
| 210 |
-
parser = argparse.ArgumentParser()
|
| 211 |
-
parser.add_argument("--request-json", required=True, help="Path to request json")
|
| 212 |
-
args = parser.parse_args()
|
| 213 |
-
|
| 214 |
-
req_path = Path(args.request_json)
|
| 215 |
-
req = json.loads(req_path.read_text(encoding="utf-8"))
|
| 216 |
-
result = run_generate(req)
|
| 217 |
-
print(json.dumps(result, ensure_ascii=False))
|
| 218 |
-
return 0
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
if __name__ == "__main__":
|
| 222 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/run.bat
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
@echo off
|
| 2 |
-
title LTX-2 Cinematic Workstation
|
| 3 |
-
|
| 4 |
-
echo =========================================================
|
| 5 |
-
echo LTX-2 Cinematic UI Booting...
|
| 6 |
-
echo =========================================================
|
| 7 |
-
echo.
|
| 8 |
-
|
| 9 |
-
set "LTX_PY=%USERPROFILE%\AppData\Local\LTXDesktop\python\python.exe"
|
| 10 |
-
set "LTX_UI_URL=http://127.0.0.1:4000/"
|
| 11 |
-
|
| 12 |
-
if exist "%LTX_PY%" (
|
| 13 |
-
echo [SUCCESS] LTX Bundled Python environment detected!
|
| 14 |
-
echo [INFO] Browser will open automatically when UI is ready...
|
| 15 |
-
start "" powershell -NoProfile -WindowStyle Hidden -Command "$ProgressPreference='SilentlyContinue'; $deadline=(Get-Date).AddSeconds(60); while((Get-Date) -lt $deadline){ try { Invoke-WebRequest -UseBasicParsing '%LTX_UI_URL%' -TimeoutSec 2 | Out-Null; Start-Process '%LTX_UI_URL%'; exit 0 } catch { Start-Sleep -Seconds 1 } }"
|
| 16 |
-
echo [INFO] Starting workspace natively...
|
| 17 |
-
echo ---------------------------------------------------------
|
| 18 |
-
"%LTX_PY%" main.py
|
| 19 |
-
pause
|
| 20 |
-
exit /b
|
| 21 |
-
)
|
| 22 |
-
|
| 23 |
-
python --version >nul 2>&1
|
| 24 |
-
if %errorlevel% equ 0 (
|
| 25 |
-
echo [WARNING] LTX Bundled Python not found.
|
| 26 |
-
echo [INFO] Browser will open automatically when UI is ready...
|
| 27 |
-
start "" powershell -NoProfile -WindowStyle Hidden -Command "$ProgressPreference='SilentlyContinue'; $deadline=(Get-Date).AddSeconds(60); while((Get-Date) -lt $deadline){ try { Invoke-WebRequest -UseBasicParsing '%LTX_UI_URL%' -TimeoutSec 2 | Out-Null; Start-Process '%LTX_UI_URL%'; exit 0 } catch { Start-Sleep -Seconds 1 } }"
|
| 28 |
-
echo [INFO] Falling back to global Python environment...
|
| 29 |
-
echo ---------------------------------------------------------
|
| 30 |
-
python main.py
|
| 31 |
-
pause
|
| 32 |
-
exit /b
|
| 33 |
-
)
|
| 34 |
-
|
| 35 |
-
echo [ERROR] FATAL: No Python interpreter found on this system.
|
| 36 |
-
echo [INFO] Please run install.bat to download and set up Python!
|
| 37 |
-
echo.
|
| 38 |
-
pause
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LTX2.3-1.0.4-new/安装TTS环境.txt
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
# 0. 进入 LTX Python 目录 (使用环境变量,自动适配任何用户的电脑)
|
| 2 |
-
cd $env:LOCALAPPDATA\LTXDesktop\python
|
| 3 |
-
|
| 4 |
-
# 1. 下载官方的 pip 安装脚本
|
| 5 |
-
Invoke-WebRequest -Uri https://bootstrap.pypa.io/get-pip.py -OutFile get-pip.py
|
| 6 |
-
|
| 7 |
-
# 2. 用 LTX 的 Python 运行这个脚本来修复/更新 pip
|
| 8 |
-
.\python.exe get-pip.py
|
| 9 |
-
|
| 10 |
-
# 3. 临时设置 MSVC 编译器的环境变量为 UTF-8 (以防又遇到刚才的 C++ 乱码)
|
| 11 |
-
$env:CL="/utf-8"
|
| 12 |
-
|
| 13 |
-
# 4. 指定用 LTX 的 Python 安装 editdistance
|
| 14 |
-
.\python.exe -m pip install editdistance
|
| 15 |
-
|
| 16 |
-
# 5. 【核心防御】指定用 LTX 的 Python 安装主包,并强制指定 GPU 镜像源防覆盖
|
| 17 |
-
.\python.exe -m pip install voxcpm --extra-index-url https://download.pytorch.org/whl/cu118
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|