dx8152 commited on
Commit
9da23c2
·
verified ·
1 Parent(s): a1e8f21

Delete LTX2.3-1.0.4-new

Browse files
Files changed (34) hide show
  1. LTX2.3-1.0.4-new/API issues-API问题办法.txt +0 -50
  2. LTX2.3-1.0.4-new/LTX_Shortcut/LTX Desktop.lnk +0 -0
  3. LTX2.3-1.0.4-new/UI/i18n.js +0 -646
  4. LTX2.3-1.0.4-new/UI/index.css +0 -982
  5. LTX2.3-1.0.4-new/UI/index.html +0 -604
  6. LTX2.3-1.0.4-new/UI/index.js +0 -0
  7. LTX2.3-1.0.4-new/main.py +0 -266
  8. LTX2.3-1.0.4-new/patches/API模式问题修复说明.md +0 -41
  9. LTX2.3-1.0.4-new/patches/__pycache__/api_types.cpython-313.pyc +0 -0
  10. LTX2.3-1.0.4-new/patches/__pycache__/app_factory.cpython-313.pyc +0 -3
  11. LTX2.3-1.0.4-new/patches/__pycache__/keep_models_runtime.cpython-313.pyc +0 -0
  12. LTX2.3-1.0.4-new/patches/__pycache__/lora_build_hook.cpython-313.pyc +0 -0
  13. LTX2.3-1.0.4-new/patches/__pycache__/lora_injection.cpython-313.pyc +0 -0
  14. LTX2.3-1.0.4-new/patches/__pycache__/low_vram_runtime.cpython-313.pyc +0 -0
  15. LTX2.3-1.0.4-new/patches/__pycache__/ltx_dev_video_pipeline.cpython-313.pyc +0 -0
  16. LTX2.3-1.0.4-new/patches/__pycache__/ltx_fp8_video_pipeline.cpython-313.pyc +0 -0
  17. LTX2.3-1.0.4-new/patches/__pycache__/tts_worker.cpython-313.pyc +0 -0
  18. LTX2.3-1.0.4-new/patches/api_types.py +0 -403
  19. LTX2.3-1.0.4-new/patches/app_factory.py +0 -0
  20. LTX2.3-1.0.4-new/patches/app_settings_patch.py +0 -22
  21. LTX2.3-1.0.4-new/patches/handlers/__pycache__/video_generation_handler.cpython-313.pyc +0 -0
  22. LTX2.3-1.0.4-new/patches/handlers/video_generation_handler.py +0 -882
  23. LTX2.3-1.0.4-new/patches/keep_models_runtime.py +0 -16
  24. LTX2.3-1.0.4-new/patches/launcher.py +0 -20
  25. LTX2.3-1.0.4-new/patches/lora_build_hook.py +0 -172
  26. LTX2.3-1.0.4-new/patches/lora_injection.py +0 -139
  27. LTX2.3-1.0.4-new/patches/low_vram_runtime.py +0 -264
  28. LTX2.3-1.0.4-new/patches/ltx_dev_video_pipeline.py +0 -156
  29. LTX2.3-1.0.4-new/patches/ltx_fp8_video_pipeline.py +0 -269
  30. LTX2.3-1.0.4-new/patches/runtime_policy.py +0 -21
  31. LTX2.3-1.0.4-new/patches/settings.json +0 -23
  32. LTX2.3-1.0.4-new/patches/tts_worker.py +0 -222
  33. LTX2.3-1.0.4-new/run.bat +0 -38
  34. 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