dx8152 commited on
Commit
600679a
·
verified ·
1 Parent(s): b316f14

Delete LTX2.3-1.0.4-new

Browse files
Files changed (32) 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 -478
  4. LTX2.3-1.0.4-new/UI/index.css +0 -811
  5. LTX2.3-1.0.4-new/UI/index.html +0 -521
  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__/tts_worker.cpython-313.pyc +0 -0
  17. LTX2.3-1.0.4-new/patches/api_types.py +0 -397
  18. LTX2.3-1.0.4-new/patches/app_factory.py +0 -0
  19. LTX2.3-1.0.4-new/patches/app_settings_patch.py +0 -22
  20. LTX2.3-1.0.4-new/patches/handlers/__pycache__/video_generation_handler.cpython-313.pyc +0 -0
  21. LTX2.3-1.0.4-new/patches/handlers/video_generation_handler.py +0 -882
  22. LTX2.3-1.0.4-new/patches/keep_models_runtime.py +0 -16
  23. LTX2.3-1.0.4-new/patches/launcher.py +0 -20
  24. LTX2.3-1.0.4-new/patches/lora_build_hook.py +0 -172
  25. LTX2.3-1.0.4-new/patches/lora_injection.py +0 -139
  26. LTX2.3-1.0.4-new/patches/low_vram_runtime.py +0 -244
  27. LTX2.3-1.0.4-new/patches/ltx_dev_video_pipeline.py +0 -156
  28. LTX2.3-1.0.4-new/patches/runtime_policy.py +0 -21
  29. LTX2.3-1.0.4-new/patches/settings.json +0 -23
  30. LTX2.3-1.0.4-new/patches/tts_worker.py +0 -222
  31. LTX2.3-1.0.4-new/run.bat +0 -38
  32. 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,478 +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
- tabImage: '图像生成',
13
- promptLabel: '视觉描述词 (Prompt)',
14
- promptPlaceholder: '在此输入视觉描述词 (Prompt)...',
15
- clearVram: '释放显存',
16
- clearingVram: '清理中...',
17
- settingsTitle: '系统高级设置',
18
- langToggleAriaZh: '切换为 English',
19
- langToggleAriaEn: 'Switch to 中文',
20
- sysScanning: '正在扫描 GPU...',
21
- sysBusy: '运算中...',
22
- sysOnline: '在线 / 就绪',
23
- sysStarting: '启动中...',
24
- sysOffline: '未检测到后端 (Port 3000)',
25
- advancedSettings: '高级设置',
26
- deviceSelect: '工作设备选择',
27
- gpuDetecting: '正在检测 GPU...',
28
- outputPath: '输出与上传存储路径',
29
- outputPathPh: '例如: D:\\LTX_outputs',
30
- savePath: '保存路径',
31
- outputPathHint:
32
- '系统默认会在 C 盘保留输出文件。请输入新路径后点击保存按钮。',
33
- lowVram: '低显存优化',
34
- lowVramDesc:
35
- '尽量关闭 fast 超分、在加载管线后尝试 CPU 分层卸载(仅当引擎提供 Diffusers 式 API 才可能生效)。每次生成结束会卸载管线。说明:整模型常驻 GPU 时占用仍可能接近满配(例如约 24GB),要明显降占用需更短时长/更低分辨率或 FP8 等小权重。',
36
- vramLimitLabel: '可用最高显存上限 (GB, 0为全开优先显存)',
37
- vramLimitPh: '例如: 12 (0表示无限制)',
38
- saveLabel: '保存',
39
- modelLoraSettings: '模型与LoRA设置',
40
- modelFolder: '模型文件夹',
41
- modelFolderPh: '例如: F:\\LTX2.3\\models',
42
- loraFolder: 'LoRA文件夹',
43
- loraFolderPh: '例如: F:\\LTX2.3\\loras',
44
- loraFolderPath: 'LoRA 文件夹路径',
45
- loraFolderPathPlaceholder: '留空使用默认路径',
46
- saveScan: '保存并扫描',
47
- loraPlacementHintWithDir:
48
- '将 LoRA 文件放到默认模型目录: <code>{dir}</code>\\loras',
49
- basicEngine: '基础画面 / Basic EngineSpecs',
50
- qualityLevel: '清晰度级别',
51
- aspectRatio: '画幅比例',
52
- ratio169: '16:9 电影宽幅',
53
- ratio916: '9:16 移动竖屏',
54
- resPreviewPrefix: '最终发送规格',
55
- fpsLabel: '帧率 (FPS)',
56
- durationLabel: '时长 (秒)',
57
- cameraMotion: '镜头运动方式',
58
- motionStatic: 'Static (静止机位)',
59
- motionDollyIn: 'Dolly In (推近)',
60
- motionDollyOut: 'Dolly Out (拉远)',
61
- motionDollyLeft: 'Dolly Left (向左)',
62
- motionDollyRight: 'Dolly Right (向右)',
63
- motionJibUp: 'Jib Up (升臂)',
64
- motionJibDown: 'Jib Down (降臂)',
65
- motionFocus: 'Focus Shift (焦点)',
66
- audioGen: '生成 AI 环境音 (Audio Gen)',
67
- selectModel: '选择模型',
68
- selectLora: '选择 LoRA',
69
- defaultModel: '使用默认模型',
70
- noLora: '不使用 LoRA',
71
- loraStrength: 'LoRA 强度',
72
- genSource: '生成媒介 / Generation Source',
73
- startFrame: '起始帧 (首帧)',
74
- endFrame: '结束帧 (尾帧)',
75
- uploadStart: '上传首帧',
76
- uploadEnd: '上传尾帧 (可选)',
77
- refAudio: '参考音频 (A2V)',
78
- uploadAudio: '点击上传音频',
79
- sourceHint:
80
- '💡 若仅上传首帧 = 图生视频/音视频;若同时上传首尾帧 = 首尾插帧。',
81
- imgPreset: '预设分辨率 (Presets)',
82
- imgOptSquare: '1:1 Square (1024x1024)',
83
- imgOptLand: '16:9 Landscape (1280x720)',
84
- imgOptPort: '9:16 Portrait (720x1280)',
85
- imgOptCustom: 'Custom 自定义...',
86
- width: '宽度',
87
- height: '高度',
88
- samplingSteps: '采样步数 (Steps)',
89
- smartMultiFrameGroup: '智能多帧',
90
- workflowModeLabel: '工作流模式(点击切换)',
91
- wfSingle: '单次多关键帧',
92
- wfSegments: '分段拼接',
93
- uploadImages: '上传图片',
94
- uploadMulti1: '点击或拖入多张图片',
95
- uploadMulti2: '支持一次选多张,可多次添加',
96
- batchStripTitle: '已选图片 · 顺序 = 播放先后',
97
- batchStripHint: '在缩略图上按住拖动排序;松手落入虚线框位置',
98
- batchFfmpegHint:
99
- '💡 <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 完整路径。',
100
- bgmLabel: '成片配乐(可选,统一音轨)',
101
- bgmUploadHint: '上传一条完整 BGM(生成完成后会替换整段成片的音轨)',
102
- mainRender: '开始渲染',
103
- waitingTask: '等待分配渲染任务...',
104
- libHistory: '历史资产 / ASSETS',
105
- libLog: '系统日志 / LOGS',
106
- refresh: '刷新',
107
- logReady: '> LTX-2 Studio Ready. Expecting commands...',
108
- resizeHandleTitle: '拖动调整面板高度',
109
- batchNeedTwo: '💡 请上传至少2张图片',
110
- batchSegTitle: '视频片段设置(分段拼接)',
111
- batchSegClip: '片段',
112
- batchSegDuration: '时长',
113
- batchSegSec: '秒',
114
- batchSegPrompt: '片段提示词',
115
- batchSegPromptPh: '此片段的提示词,如:跳舞、吃饭...',
116
- batchKfPanelTitle: '单次多关键帧 · 时间轴',
117
- batchTotalDur: '总时长',
118
- batchTotalSec: '秒',
119
- batchPanelHint:
120
- '为每一张图设置独立持续时间:第 1 张从 0 s 开始,下一张在上一张持续结束后接上,最后一张也会延续自己的时长。因后端按<strong>整数秒</strong>建序列,实际请求里的整段时长为合计秒数<strong>向上取整</strong>(至少 2),略长于小数合计时属正常。镜头与 FPS 仍用左侧「视频生成」。',
121
- batchKfTitle: '关键帧',
122
- batchStrength: '引导强度',
123
- batchFrameDuration: '持续时长',
124
- batchFrameDurationTitle: '这一张图在时间轴上占用的时长(秒);总时长 = 每张图持续时长之和',
125
- batchGapTitle: '间隔',
126
- batchSec: '秒',
127
- batchAnchorStart: '片头',
128
- batchAnchorEnd: '片尾',
129
- batchAnchorLast: '最后一帧开始',
130
- batchThumbDrag: '按住拖动排序',
131
- batchThumbRemove: '删除',
132
- batchAddMore: '+ 继续添加',
133
- batchGapInputTitle: '这一张图在时间轴上占用的时长(秒);总时长 = 每张图持续时长之和',
134
- batchStrengthTitle: '与 Comfy guide strength 类似,中间帧可调低(如 0.2)减轻闪烁',
135
- batchTotalPillTitle: '等于下方各「持续时长」之和,无需单独填写',
136
- defaultPath: '默认路径',
137
- phase_loading_model: '加载权重',
138
- phase_encoding_text: 'T5 编码',
139
- phase_validating_request: '校验请求',
140
- phase_uploading_audio: '上传音频',
141
- phase_uploading_image: '上传图像',
142
- phase_inference: 'AI 推理',
143
- phase_downloading_output: '下载结果',
144
- phase_complete: '完成',
145
- gpuBusyPrefix: 'GPU 运算中',
146
- progressStepUnit: '步',
147
- loaderGpuAlloc: 'GPU 正在分配资源...',
148
- warnGenerating: '⚠️ 当前正在生成中,请等待完成',
149
- warnBatchPrompt: '⚠️ 智能多帧请至少填写:顶部主提示词、本页全局补充词或某一「片段提示词」',
150
- warnNeedPrompt: '⚠️ 请输入提示词后再开始渲染',
151
- warnVideoLong: '⚠️ 时长设定为 {n}s 极长,可能导致显存溢出或耗时较久。',
152
- errBatchMinImages: '请上传至少2张图片',
153
- errSingleKfPrompt: '单次多关键帧请至少填写顶部主提示词或本页全局补充词',
154
- loraNoneLabel: '无',
155
- modelDefaultLabel: '默认',
156
- tabTts: 'TTS 语音',
157
- ttsStatusBarDetecting: '🔍 正在检测 TTS 模型...',
158
- ttsTextTitle: '合成文本 / Text',
159
- ttsTextHint: '支持在文本开头加英文括号描述声音,例如:<code style="font-size:10px;">(年轻女声,温柔甜美)</code>',
160
- ttsTextPlaceholder: '输入要合成的文本内容...',
161
- ttsModeTitle: '合成模式 / Mode',
162
- ttsModeTextOnly: '🗣️ 文字转语音(含声音设计)',
163
- ttsModeClone: '🎙️ 声音克隆',
164
- ttsModeUltimate: '⭐ 终极克隆(最高还原度)',
165
- ttsRefLabel: '📎 参考音频(Reference)',
166
- ttsRefUploadHint: '点击上传参考音频 (.wav / .mp3)',
167
- ttsUltimateLabel: '📝 参考音频对应的文本转录(可选)',
168
- ttsUltimatePlaceholder: '与参考音频完全一致的文本内容...',
169
- ttsParamsTitle: '高级参数 / Parameters',
170
- ttsCfgLabel: 'CFG 强度',
171
- ttsStepsLabel: '推理步数',
172
- ttsResultTitle: '生成结果 / Output',
173
- ttsDownload: '⬇️ 下载音频',
174
- ttsGenBtn: '🎙️ 开始生成语音',
175
- ttsGenBusy: '⏳ 生成中...',
176
- ttsErrNoText: '❌ TTS: 请输入合成文本',
177
- ttsErrNoRef: '❌ TTS: 声音克隆模式需要上传参考音频',
178
- ttsStatusReady: '✅ VoxCPM2 就绪 — 模型目录: ',
179
- ttsStatusNoPkq: '❌ voxcpm 包未安装,请在 LTX Python 环境中运行: pip install voxcpm',
180
- ttsStatusNoDir: '❌ 模型目录不存在: ',
181
- ttsStatusNotAvail: '⚠️ TTS 不可用,请检查配置',
182
- ttsStatusConnErr: '❌ 无法连接后端 TTS 接口: ',
183
- },
184
- en: {
185
- tabVideo: 'Video',
186
- tabBatch: 'Multi-frame',
187
- tabImage: 'Image',
188
- promptLabel: 'Prompt',
189
- promptPlaceholder: 'Describe the scene...',
190
- clearVram: 'Clear VRAM',
191
- clearingVram: 'Clearing...',
192
- settingsTitle: 'Advanced settings',
193
- langToggleAriaZh: 'Switch to English',
194
- langToggleAriaEn: 'Switch to Chinese',
195
- sysScanning: 'Scanning GPU...',
196
- sysBusy: 'Busy...',
197
- sysOnline: 'Online / Ready',
198
- sysStarting: 'Starting...',
199
- sysOffline: 'Backend offline (port 3000)',
200
- advancedSettings: 'Advanced',
201
- deviceSelect: 'GPU device',
202
- gpuDetecting: 'Detecting GPU...',
203
- outputPath: 'Output & upload folder',
204
- outputPathPh: 'e.g. D:\\LTX_outputs',
205
- savePath: 'Save path',
206
- outputPathHint:
207
- 'Outputs default to C: drive. Enter a folder and click Save.',
208
- lowVram: 'Low-VRAM mode',
209
- lowVramDesc:
210
- 'Tries to reduce VRAM (engine-dependent). Shorter duration / lower resolution helps more.',
211
- vramLimitLabel: 'Max VRAM Limit (GB, 0 for unlimited)',
212
- vramLimitPh: 'e.g. 12 (0 for unlimited)',
213
- saveLabel: 'Save',
214
- modelLoraSettings: 'Model & LoRA folders',
215
- modelFolder: 'Models folder',
216
- modelFolderPh: 'e.g. F:\\LTX2.3\\models',
217
- loraFolder: 'LoRAs folder',
218
- loraFolderPh: 'e.g. F:\\LTX2.3\\loras',
219
- loraFolderPath: 'LoRA folder path',
220
- loraFolderPathPlaceholder: 'Leave empty for default path',
221
- saveScan: 'Save & scan',
222
- loraHint: 'Put .safetensors / .ckpt LoRAs here, then refresh lists.',
223
- basicEngine: 'Basic / Engine',
224
- qualityLevel: 'Quality',
225
- aspectRatio: 'Aspect ratio',
226
- ratio169: '16:9 widescreen',
227
- ratio916: '9:16 portrait',
228
- resPreviewPrefix: 'Output',
229
- fpsLabel: 'FPS',
230
- durationLabel: 'Duration (s)',
231
- cameraMotion: 'Camera motion',
232
- motionStatic: 'Static',
233
- motionDollyIn: 'Dolly in',
234
- motionDollyOut: 'Dolly out',
235
- motionDollyLeft: 'Dolly left',
236
- motionDollyRight: 'Dolly right',
237
- motionJibUp: 'Jib up',
238
- motionJibDown: 'Jib down',
239
- motionFocus: 'Focus shift',
240
- audioGen: 'AI ambient audio',
241
- selectModel: 'Model',
242
- selectLora: 'LoRA',
243
- defaultModel: 'Default model',
244
- noLora: 'No LoRA',
245
- loraStrength: 'LoRA strength',
246
- genSource: 'Source media',
247
- startFrame: 'Start frame',
248
- endFrame: 'End frame (optional)',
249
- uploadStart: 'Upload start',
250
- uploadEnd: 'Upload end (opt.)',
251
- refAudio: 'Reference audio (A2V)',
252
- uploadAudio: 'Upload audio',
253
- sourceHint:
254
- '💡 Start only = I2V / A2V; start + end = interpolation.',
255
- imgPreset: 'Resolution presets',
256
- imgOptSquare: '1:1 (1024×1024)',
257
- imgOptLand: '16:9 (1280×720)',
258
- imgOptPort: '9:16 (720×1280)',
259
- imgOptCustom: 'Custom...',
260
- width: 'Width',
261
- height: 'Height',
262
- samplingSteps: 'Steps',
263
- smartMultiFrameGroup: 'Smart multi-frame',
264
- workflowModeLabel: 'Workflow',
265
- wfSingle: 'Single pass',
266
- wfSegments: 'Segments',
267
- uploadImages: 'Upload images',
268
- uploadMulti1: 'Click or drop multiple images',
269
- uploadMulti2: 'Multi-select OK; add more anytime.',
270
- batchStripTitle: 'Order = playback',
271
- batchStripHint: 'Drag thumbnails to reorder.',
272
- batchFfmpegHint:
273
- '💡 <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.',
274
- bgmLabel: 'Full-length BGM (optional)',
275
- bgmUploadHint: 'Replaces final mix audio after generation.',
276
- mainRender: 'Render',
277
- waitingTask: 'Waiting for task...',
278
- libHistory: 'Assets',
279
- libLog: 'Logs',
280
- refresh: 'Refresh',
281
- logReady: '> LTX-2 Studio ready.',
282
- resizeHandleTitle: 'Drag to resize panel',
283
- batchNeedTwo: '💡 Upload at least 2 images',
284
- batchSegTitle: 'Segment settings',
285
- batchSegClip: 'Clip',
286
- batchSegDuration: 'Duration',
287
- batchSegSec: 's',
288
- batchSegPrompt: 'Prompt',
289
- batchSegPromptPh: 'e.g. dancing, walking...',
290
- batchKfPanelTitle: 'Single pass · timeline',
291
- batchTotalDur: 'Total',
292
- batchTotalSec: 's',
293
- batchPanelHint:
294
- '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.',
295
- batchKfTitle: 'Keyframe',
296
- batchStrength: 'Strength',
297
- batchFrameDuration: 'Duration',
298
- batchFrameDurationTitle: 'How long this image occupies on the timeline; total = sum of all image durations',
299
- batchGapTitle: 'Gap',
300
- batchSec: 's',
301
- batchAnchorStart: 'start',
302
- batchAnchorEnd: 'end',
303
- batchAnchorLast: 'last starts',
304
- batchThumbDrag: 'Drag to reorder',
305
- batchThumbRemove: 'Remove',
306
- batchAddMore: '+ Add more',
307
- batchGapInputTitle: 'How long this image occupies on the timeline; total = sum of all image durations',
308
- batchStrengthTitle: 'Guide strength (lower on middle keys may reduce flicker)',
309
- batchTotalPillTitle: 'Equals the sum of durations below',
310
- defaultPath: 'default',
311
- phase_loading_model: 'Loading weights',
312
- phase_encoding_text: 'T5 encode',
313
- phase_validating_request: 'Validating',
314
- phase_uploading_audio: 'Uploading audio',
315
- phase_uploading_image: 'Uploading image',
316
- phase_inference: 'Inference',
317
- phase_downloading_output: 'Downloading',
318
- phase_complete: 'Done',
319
- gpuBusyPrefix: 'GPU',
320
- progressStepUnit: 'steps',
321
- loaderGpuAlloc: 'Allocating GPU...',
322
- warnGenerating: '⚠️ Already generating, please wait.',
323
- warnBatchPrompt: '⚠️ Enter main prompt, page extra prompt, or a segment prompt.',
324
- warnNeedPrompt: '⚠️ Enter a prompt first.',
325
- warnVideoLong: '⚠️ Duration {n}s is very long; may OOM or take a long time.',
326
- errBatchMinImages: 'Upload at least 2 images.',
327
- errSingleKfNeedPrompt: 'Enter main or page extra prompt for single-pass keyframes.',
328
- loraNoneLabel: 'none',
329
- modelDefaultLabel: 'default',
330
- loraPlacementHintWithDir:
331
- 'Place LoRAs into the default models directory: <code>{dir}</code>\\loras',
332
- tabTts: 'TTS Voice',
333
- ttsStatusBarDetecting: '🔍 Detecting TTS model...',
334
- ttsTextTitle: 'Synthesis Text',
335
- ttsTextHint: 'Supports descriptors in brackets at start, e.g. <code style="font-size:10px;">(Young female, soft and sweet)</code>',
336
- ttsTextPlaceholder: 'Enter text to synthesize...',
337
- ttsModeTitle: 'Synthesis Mode',
338
- ttsModeTextOnly: '🗣️ Text to Speech (with sound design)',
339
- ttsModeClone: '🎙️ Voice Cloning',
340
- ttsModeUltimate: '⭐ Ultimate Clone (Max similarity)',
341
- ttsRefLabel: '📎 Reference Audio',
342
- ttsRefUploadHint: 'Click to upload reference (.wav / .mp3)',
343
- ttsUltimateLabel: '📝 Audio Transcript (Optional)',
344
- ttsUltimatePlaceholder: 'Text content matches reference audio exactly...',
345
- ttsParamsTitle: 'Parameters',
346
- ttsCfgLabel: 'CFG Strength',
347
- ttsStepsLabel: 'Inference Steps',
348
- ttsResultTitle: 'Output',
349
- ttsDownload: '⬇️ Download Audio',
350
- ttsGenBtn: '🎙️ Start Synthesis',
351
- ttsGenBusy: '⏳ Generating...',
352
- ttsErrNoText: '❌ TTS: Please enter synthesis text',
353
- ttsErrNoRef: '❌ TTS: Voice cloning requires reference audio',
354
- ttsStatusReady: '✅ VoxCPM2 Ready — Model dir: ',
355
- ttsStatusNoPkq: '❌ voxcpm package not installed. Run: pip install voxcpm',
356
- ttsStatusNoDir: '❌ Model directory does not exist: ',
357
- ttsStatusNotAvail: '⚠️ TTS unavailable, check config',
358
- ttsStatusConnErr: '❌ Cannot connect to TTS API: ',
359
- },
360
- };
361
-
362
- function getLang() {
363
- return localStorage.getItem(STORAGE_KEY) === 'en' ? 'en' : 'zh';
364
- }
365
-
366
- function setLang(lang) {
367
- const L = lang === 'en' ? 'en' : 'zh';
368
- localStorage.setItem(STORAGE_KEY, L);
369
- document.documentElement.lang = L === 'en' ? 'en' : 'zh-CN';
370
- try {
371
- applyI18n();
372
- } catch (err) {
373
- console.error('[i18n] applyI18n failed:', err);
374
- }
375
- updateLangButton();
376
- if (typeof global.onUiLanguageChanged === 'function') {
377
- try {
378
- global.onUiLanguageChanged();
379
- } catch (e) {
380
- console.warn('onUiLanguageChanged', e);
381
- }
382
- }
383
- }
384
-
385
- function t(key) {
386
- const L = getLang();
387
- const table = STR[L] || STR.zh;
388
- if (Object.prototype.hasOwnProperty.call(table, key)) return table[key];
389
- if (Object.prototype.hasOwnProperty.call(STR.zh, key)) return STR.zh[key];
390
- return key;
391
- }
392
-
393
- function applyI18n(root) {
394
- root = root || document;
395
- root.querySelectorAll('[data-i18n]').forEach(function (el) {
396
- var key = el.getAttribute('data-i18n');
397
- if (!key) return;
398
- if (el.tagName === 'OPTION') {
399
- el.textContent = t(key);
400
- } else {
401
- el.textContent = t(key);
402
- }
403
- });
404
- root.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) {
405
- var key = el.getAttribute('data-i18n-placeholder');
406
- if (key) el.placeholder = t(key);
407
- });
408
- root.querySelectorAll('[data-i18n-title]').forEach(function (el) {
409
- var key = el.getAttribute('data-i18n-title');
410
- if (key) el.title = t(key);
411
- });
412
- root.querySelectorAll('[data-i18n-html]').forEach(function (el) {
413
- var key = el.getAttribute('data-i18n-html');
414
- if (key) el.innerHTML = t(key);
415
- });
416
- root.querySelectorAll('[data-i18n-value]').forEach(function (el) {
417
- var key = el.getAttribute('data-i18n-value');
418
- if (key && (el.tagName === 'INPUT' || el.tagName === 'BUTTON')) {
419
- el.value = t(key);
420
- }
421
- });
422
- }
423
-
424
- function updateLangButton() {
425
- var btn = document.getElementById('lang-toggle-btn');
426
- if (!btn) return;
427
- btn.textContent = getLang() === 'zh' ? 'EN' : '中';
428
- btn.setAttribute(
429
- 'aria-label',
430
- getLang() === 'zh' ? t('langToggleAriaZh') : t('langToggleAriaEn')
431
- );
432
- btn.classList.toggle('active', getLang() === 'en');
433
- }
434
-
435
- function toggleUiLanguage() {
436
- try {
437
- setLang(getLang() === 'zh' ? 'en' : 'zh');
438
- } catch (err) {
439
- console.error('[i18n] toggleUiLanguage failed:', err);
440
- }
441
- }
442
-
443
- /** 避免 CSP 拦截内联 onclick;确保按钮一定能触发 */
444
- function bindLangToggleButton() {
445
- var btn = document.getElementById('lang-toggle-btn');
446
- if (!btn || btn.dataset.i18nBound === '1') return;
447
- btn.dataset.i18nBound = '1';
448
- btn.removeAttribute('onclick');
449
- btn.addEventListener('click', function (ev) {
450
- ev.preventDefault();
451
- toggleUiLanguage();
452
- });
453
- }
454
-
455
- function boot() {
456
- document.documentElement.lang = getLang() === 'en' ? 'en' : 'zh-CN';
457
- try {
458
- applyI18n();
459
- } catch (err) {
460
- console.error('[i18n] applyI18n failed:', err);
461
- }
462
- updateLangButton();
463
- bindLangToggleButton();
464
- }
465
-
466
- global.getUiLang = getLang;
467
- global.setUiLang = setLang;
468
- global.t = t;
469
- global.applyI18n = applyI18n;
470
- global.toggleUiLanguage = toggleUiLanguage;
471
- global.updateLangToggleButton = updateLangButton;
472
-
473
- if (document.readyState === 'loading') {
474
- document.addEventListener('DOMContentLoaded', boot);
475
- } else {
476
- boot();
477
- }
478
- })(typeof window !== 'undefined' ? window : global);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
LTX2.3-1.0.4-new/UI/index.css DELETED
@@ -1,811 +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
-
96
- .label-group { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
97
- label { display: block; font-size: 11px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
98
- .val-badge { font-size: 11px; color: var(--accent); font-family: "SF Mono", ui-monospace, monospace; font-weight: 600; }
99
-
100
- input[type="text"], input[type="number"], select, textarea {
101
- width: 100%; background: var(--panel-2);
102
- border: 1px solid var(--border);
103
- border-radius: 7px; color: var(--text);
104
- padding: 8px 11px; font-size: 12.5px; outline: none; margin-bottom: 9px;
105
- /* Only transition border/shadow – NOT background-image to prevent arrow flicker */
106
- transition: border-color 0.15s, box-shadow 0.15s;
107
- }
108
- input:focus, select:focus, textarea:focus {
109
- border-color: var(--accent);
110
- box-shadow: 0 0 0 2px var(--accent-ring);
111
- }
112
- select {
113
- -webkit-appearance: none; -moz-appearance: none; appearance: none;
114
- /* Stable grey arrow – no background shorthand so it won't animate */
115
- background-color: var(--panel-2);
116
- 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");
117
- background-repeat: no-repeat;
118
- background-position: right 10px center;
119
- background-size: 12px;
120
- padding-right: 28px;
121
- cursor: pointer;
122
- /* Explicitly do NOT transition background properties */
123
- transition: border-color 0.15s, box-shadow 0.15s;
124
- }
125
- select:focus { background-color: var(--panel-2); }
126
- select option { background: #27272A; color: var(--text); }
127
- textarea { resize: vertical; min-height: 78px; font-family: inherit; }
128
-
129
- .slider-container { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
130
- input[type="range"] { flex: 1; accent-color: var(--accent); height: 4px; cursor: pointer; border-radius: 2px; }
131
-
132
- .upload-zone {
133
- border: 1px dashed var(--border); border-radius: 10px;
134
- padding: 18px 10px; text-align: center; cursor: pointer;
135
- background: rgba(255,255,255,0.03); margin-bottom: 10px; position: relative;
136
- transition: all 0.2s;
137
- }
138
- .upload-zone:hover, .upload-zone.dragover { background: var(--accent-dim); border-color: var(--accent); }
139
- .upload-zone.has-images {
140
- padding: 12px; background: rgba(255,255,255,0.025);
141
- }
142
- .upload-zone.has-images .upload-placeholder-mini {
143
- display: flex; align-items: center; gap: 8px; justify-content: center;
144
- color: var(--text-dim); font-size: 11px;
145
- }
146
- .upload-zone.has-images .upload-placeholder-mini span {
147
- background: var(--item); padding: 6px 12px; border-radius: 6px;
148
- }
149
- #batch-images-placeholder { display: block; }
150
- .upload-zone.has-images #batch-images-placeholder { display: none; }
151
-
152
- /* 批量模式:上传区下方的横向缩略图条 */
153
- .batch-thumb-strip-wrap {
154
- margin-top: 10px;
155
- margin-bottom: 4px;
156
- }
157
- .batch-thumb-strip-head {
158
- display: flex;
159
- flex-direction: column;
160
- gap: 2px;
161
- margin-bottom: 8px;
162
- }
163
- .batch-thumb-strip-title {
164
- font-size: 11px;
165
- font-weight: 700;
166
- color: var(--text-sub);
167
- }
168
- .batch-thumb-strip-hint {
169
- font-size: 10px;
170
- color: var(--text-dim);
171
- }
172
- .batch-images-container {
173
- display: flex;
174
- flex-direction: row;
175
- flex-wrap: nowrap;
176
- gap: 10px;
177
- overflow-x: auto;
178
- overflow-y: visible;
179
- padding: 6px 4px 14px;
180
- margin: 0 -4px;
181
- scrollbar-width: thin;
182
- scrollbar-color: var(--border) transparent;
183
- align-items: center;
184
- }
185
- .batch-images-container::-webkit-scrollbar { height: 6px; }
186
- .batch-images-container::-webkit-scrollbar-thumb {
187
- background: var(--border);
188
- border-radius: 3px;
189
- }
190
- .batch-image-wrapper {
191
- flex: 0 0 72px;
192
- width: 72px;
193
- height: 72px;
194
- position: relative;
195
- border-radius: 10px;
196
- overflow: hidden;
197
- background: var(--item);
198
- border: 1px solid var(--border);
199
- cursor: grab;
200
- touch-action: none;
201
- user-select: none;
202
- -webkit-user-select: none;
203
- transition:
204
- flex-basis 0.38s cubic-bezier(0.22, 1, 0.36, 1),
205
- width 0.38s cubic-bezier(0.22, 1, 0.36, 1),
206
- min-width 0.38s cubic-bezier(0.22, 1, 0.36, 1),
207
- margin 0.38s cubic-bezier(0.22, 1, 0.36, 1),
208
- opacity 0.25s ease,
209
- border-color 0.2s ease,
210
- box-shadow 0.2s ease,
211
- transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
212
- }
213
- .batch-image-wrapper:active { cursor: grabbing; }
214
- .batch-image-wrapper.batch-thumb--source {
215
- flex: 0 0 0;
216
- width: 0;
217
- min-width: 0;
218
- height: 72px;
219
- margin: 0;
220
- padding: 0;
221
- border: none;
222
- overflow: hidden;
223
- opacity: 0;
224
- background: transparent;
225
- box-shadow: none;
226
- pointer-events: none;
227
- /* 收起必须瞬时:若与占位框同时用 0.38s 过渡,右侧缩略图会与「突然出现」的槽位不同步而闪一下 */
228
- transition: none !important;
229
- }
230
- /* 按下瞬间:冻结其它卡片与槽位动画,避免「槽位插入 + 邻居过渡」两帧打架 */
231
- .batch-images-container.is-batch-settling .batch-image-wrapper:not(.batch-thumb--source) {
232
- transition: none !important;
233
- }
234
- .batch-images-container.is-batch-settling .batch-thumb-drop-slot {
235
- animation: none;
236
- opacity: 1;
237
- }
238
- /* 拖动时跟手的浮动缩略图(避免原槽位透明后光标下像「黑块」) */
239
- .batch-thumb-floating-ghost {
240
- position: fixed;
241
- left: 0;
242
- top: 0;
243
- z-index: 99999;
244
- width: 76px;
245
- height: 76px;
246
- border-radius: 12px;
247
- overflow: hidden;
248
- pointer-events: none;
249
- will-change: transform;
250
- box-shadow:
251
- 0 20px 50px rgba(0, 0, 0, 0.45),
252
- 0 10px 28px rgba(0, 0, 0, 0.28),
253
- 0 0 0 1px rgba(255, 255, 255, 0.18);
254
- transform: translate3d(0, 0, 0) scale(1.06) rotate(-1deg);
255
- }
256
- .batch-thumb-floating-ghost img {
257
- width: 100%;
258
- height: 100%;
259
- object-fit: cover;
260
- display: block;
261
- pointer-events: none;
262
- }
263
- .batch-thumb-drop-slot {
264
- flex: 0 0 72px;
265
- width: 72px;
266
- height: 72px;
267
- box-sizing: border-box;
268
- border-radius: 12px;
269
- border: 2px dashed rgba(255, 255, 255, 0.22);
270
- background: linear-gradient(145deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.03));
271
- pointer-events: none;
272
- transition: border-color 0.35s ease, box-shadow 0.35s ease, opacity 0.35s ease;
273
- animation: batch-slot-breathe 2.4s ease-in-out infinite;
274
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
275
- }
276
- @keyframes batch-slot-breathe {
277
- 0%, 100% { opacity: 0.88; }
278
- 50% { opacity: 1; }
279
- }
280
- .batch-image-wrapper .batch-thumb-img-wrap {
281
- width: 100%;
282
- height: 100%;
283
- border-radius: 9px;
284
- overflow: hidden;
285
- /* 必须让事件落到外层 .batch-image-wrapper,否则 HTML5 drag 无法从 draggable 父级启动 */
286
- pointer-events: none;
287
- }
288
- .batch-image-wrapper .batch-thumb-img {
289
- width: 100%;
290
- height: 100%;
291
- object-fit: cover;
292
- display: block;
293
- pointer-events: none;
294
- user-select: none;
295
- -webkit-user-drag: none;
296
- }
297
- .batch-thumb-remove {
298
- position: absolute;
299
- top: 3px;
300
- right: 3px;
301
- z-index: 5;
302
- box-sizing: border-box;
303
- min-width: 22px;
304
- height: 22px;
305
- padding: 0 5px;
306
- margin: 0;
307
- border: 1px solid rgba(255, 255, 255, 0.12);
308
- border-radius: 6px;
309
- background: rgba(0, 0, 0, 0.5);
310
- font-family: inherit;
311
- font-size: 14px;
312
- font-weight: 400;
313
- line-height: 1;
314
- color: rgba(255, 255, 255, 0.9);
315
- opacity: 0.72;
316
- cursor: pointer;
317
- display: flex;
318
- align-items: center;
319
- justify-content: center;
320
- transition: background 0.12s, opacity 0.12s, border-color 0.12s;
321
- pointer-events: auto;
322
- }
323
- .batch-image-wrapper:hover .batch-thumb-remove {
324
- opacity: 1;
325
- background: rgba(0, 0, 0, 0.68);
326
- border-color: rgba(255, 255, 255, 0.2);
327
- }
328
- .batch-thumb-remove:hover {
329
- background: rgba(80, 20, 20, 0.75) !important;
330
- border-color: rgba(255, 180, 180, 0.35);
331
- color: #fff;
332
- }
333
- .batch-thumb-remove:focus-visible {
334
- opacity: 1;
335
- outline: 2px solid var(--accent-dim, rgba(120, 160, 255, 0.6));
336
- outline-offset: 1px;
337
- }
338
- .upload-icon { font-size: 18px; margin-bottom: 6px; opacity: 0.45; }
339
- .upload-text { font-size: 11px; color: var(--text); }
340
- .upload-hint { font-size: 10px; color: var(--text-dim); margin-top: 3px; }
341
- .preview-thumb { width: 100%; height: auto; max-height: 100px; object-fit: contain; border-radius: 8px; display: none; margin-top: 10px; }
342
- .clear-img-overlay {
343
- position: absolute; top: 8px; right: 8px; background: rgba(255,59,48,0.85); color: white;
344
- width: 20px; height: 20px; border-radius: 10px; display: none; align-items: center; justify-content: center;
345
- font-size: 11px; cursor: pointer; z-index: 5;
346
- }
347
-
348
- .btn-outline {
349
- background: var(--panel-2);
350
- border: 1px solid var(--border);
351
- color: var(--text-sub); padding: 5px 12px; border-radius: 7px;
352
- font-size: 11.5px; font-weight: 600; cursor: pointer;
353
- transition: background 0.15s, border-color 0.15s, color 0.15s;
354
- display: inline-flex; align-items: center; justify-content: center; gap: 5px;
355
- white-space: nowrap;
356
- }
357
- .btn-outline:hover:not(:disabled) { background: rgba(255,255,255,0.08); color: var(--text); border-color: rgba(255,255,255,0.18); }
358
- .btn-outline:active { opacity: 0.7; }
359
- .btn-outline:disabled { opacity: 0.3; cursor: not-allowed; }
360
-
361
- .btn-icon {
362
- padding: 5px; background: transparent; border: none; color: var(--text-dim);
363
- border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center;
364
- transition: color 0.15s, background 0.15s;
365
- }
366
- .btn-icon:hover { color: var(--text-sub); background: rgba(255,255,255,0.07); }
367
-
368
- .btn-primary {
369
- width: 100%; padding: 13px;
370
- background: var(--accent); border: none;
371
- border-radius: 9px; color: #fff; font-weight: 700; font-size: 13.5px;
372
- letter-spacing: 0.2px; cursor: pointer; margin-top: 14px;
373
- transition: background 0.15s;
374
- }
375
- .btn-primary:hover:not(:disabled) { background: var(--accent-hover); }
376
- .btn-primary:active { opacity: 0.82; }
377
- .btn-primary:disabled { background: rgba(255,255,255,0.08); color: var(--text-dim); cursor: not-allowed; }
378
-
379
- .btn-danger {
380
- width: 100%; padding: 12px; background: #DC2626; border: none;
381
- border-radius: 9px; color: #fff; font-weight: 700; font-size: 13.5px;
382
- cursor: pointer; margin-top: 8px; display: none; transition: background 0.15s;
383
- }
384
- .btn-danger:hover { background: #EF4444; }
385
-
386
- /* Workspace */
387
- .workspace { flex: 1; display: flex; flex-direction: column; background: #0A0A0A; position: relative; overflow: hidden; }
388
- .viewer { flex: 2; display: flex; align-items: center; justify-content: center; padding: 16px; background: #0A0A0A; position: relative; min-height: 40vh; }
389
- .monitor {
390
- width: 100%; height: 100%; max-width: 1650px; border-radius: 10px; border: 1px solid var(--border);
391
- overflow: hidden; position: relative; background: #070707;
392
- display: flex; align-items: center; justify-content: center;
393
- background-image: radial-gradient(rgba(255,255,255,0.02) 1px, transparent 1px);
394
- background-size: 18px 18px;
395
- }
396
- .monitor img, .monitor video {
397
- width: auto; height: auto; max-width: 100%; max-height: 100%;
398
- object-fit: contain; display: none; z-index: 2; border-radius: 3px;
399
- }
400
-
401
- .progress-container { position: absolute; bottom: 0; left: 0; width: 100%; height: 2px; background: var(--border-2); z-index: 10; }
402
- #progress-fill { width: 0%; height: 100%; background: var(--accent); transition: width 0.5s; }
403
- #loading-txt { font-size: 12px; color: var(--text-sub); font-weight: 600; z-index: 5; position: absolute; display: none; }
404
-
405
-
406
-
407
- .spinner {
408
- width: 12px; height: 12px;
409
- border: 2px solid rgba(255,255,255,0.2);
410
- border-top-color: currentColor;
411
- border-radius: 50%;
412
- animation: spin 1s linear infinite;
413
- }
414
- @keyframes spin { to { transform: rotate(360deg); } }
415
-
416
- .loading-card {
417
- display: flex; align-items: center; justify-content: center;
418
- flex-direction: column; gap: 6px; color: var(--text-dim); font-size: 10px;
419
- background: rgba(37,99,235,0.07) !important;
420
- border-color: rgba(37,99,235,0.3) !important;
421
- }
422
- .loading-card .spinner { width: 28px; height: 28px; border-width: 3px; color: var(--accent); }
423
- .loading-card:hover { background: rgba(37,99,235,0.14) !important; border-color: var(--accent) !important; }
424
-
425
- .library { flex: 1.5; border-top: 1px solid var(--border); padding: 14px 20px; display: flex; flex-direction: column; background: #0F0F11; overflow-y: hidden; }
426
- #log-container { flex: 1; overflow-y: auto; padding-right: 4px; }
427
- #log { font-family: ui-monospace, "SF Mono", monospace; font-size: 10.5px; color: var(--text-dim); line-height: 1.7; }
428
-
429
- /* History wrapper: scrollable area for thumbnails only */
430
- #history-wrapper {
431
- flex: 1;
432
- overflow-y: auto;
433
- min-height: 110px; /* always show at least one row */
434
- padding-right: 4px;
435
- }
436
- #history-container {
437
- display: grid;
438
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
439
- justify-content: start;
440
- gap: 10px; align-content: flex-start;
441
- padding-bottom: 4px;
442
- }
443
- /* Pagination row: hidden, using infinite scroll instead */
444
- #pagination-bar {
445
- display: none;
446
- }
447
-
448
- .history-card {
449
- width: 100%; max-width: 200px; aspect-ratio: 16 / 9;
450
- background: #1A1A1E; border-radius: 7px;
451
- overflow: hidden; border: 1px solid var(--border);
452
- cursor: pointer; position: relative; transition: border-color 0.15s, transform 0.15s;
453
- }
454
- .history-card:hover { border-color: var(--accent); transform: translateY(-1px); }
455
- .history-card img, .history-card video {
456
- width: 100%; height: 100%; object-fit: cover;
457
- background: #1A1A1E;
458
- }
459
- .history-audio-thumb {
460
- width: 100%; height: 100%; display: flex; flex-direction: column;
461
- align-items: center; justify-content: center; gap: 6px;
462
- color: var(--text); background: linear-gradient(135deg, rgba(35,35,40,0.95), rgba(18,18,22,0.98));
463
- font-size: 11px; font-weight: 700; text-align: center; padding: 10px; box-sizing: border-box;
464
- }
465
- .history-audio-icon { font-size: 24px; line-height: 1; }
466
- #audio-wrapper {
467
- position: absolute; inset: 0; width: 100%; height: 100%; min-height: 0;
468
- display: none; flex-direction: column; align-items: stretch; justify-content: stretch; z-index: 2;
469
- background: #070707;
470
- background-image: radial-gradient(rgba(255,255,255,0.035) 1px, transparent 1px);
471
- background-size: 18px 18px;
472
- }
473
- .audio-preview-art {
474
- position: absolute; inset: 0 0 54px 0; display: flex; flex-direction: column;
475
- align-items: center; justify-content: center; gap: 12px; padding: 24px;
476
- color: var(--text); cursor: pointer; user-select: none; z-index: 2;
477
- }
478
- .audio-preview-icon {
479
- width: 86px; height: 86px; display: flex; align-items: center; justify-content: center;
480
- border: 1px solid var(--border); border-radius: 50%; background: rgba(255,255,255,0.04);
481
- font-size: 46px; font-weight: 800; color: var(--accent);
482
- }
483
- #audio-preview-title {
484
- max-width: min(620px, 86%); font-size: 13px; font-weight: 800; color: var(--text);
485
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: center;
486
- }
487
- #audio-wrapper .plyr {
488
- position: absolute; left: 0; right: 0; bottom: 0; top: auto;
489
- width: 100%; height: auto !important; min-height: 0; z-index: 3;
490
- border-radius: 0;
491
- }
492
- #audio-wrapper .plyr--audio .plyr__controls {
493
- border-radius: 0; border-top: 1px solid var(--border); background: rgba(12,12,14,0.96);
494
- }
495
- #res-audio { width: 100%; }
496
- /* 解码/加载完成前避免视频黑块猛闪,与卡片底色一致;就绪后淡入 */
497
- .history-card .history-thumb-media {
498
- opacity: 0;
499
- transition: opacity 0.28s ease;
500
- }
501
- .history-card .history-thumb-media.history-thumb-ready {
502
- opacity: 1;
503
- }
504
- .history-type-badge {
505
- position: absolute; top: 5px; left: 5px; font-size: 8px; padding: 1px 5px; border-radius: 3px;
506
- background: rgba(0,0,0,0.8); color: var(--text-sub); border: 1px solid rgba(255,255,255,0.06);
507
- z-index: 2; font-weight: 700; letter-spacing: 0.4px;
508
- }
509
- .history-delete-btn {
510
- position: absolute; top: 5px; right: 5px; width: 20px; height: 20px;
511
- border-radius: 50%; border: none; background: rgba(255,50,50,0.8); color: #fff;
512
- font-size: 10px; cursor: pointer; z-index: 3; display: flex; align-items: center; justify-content: center;
513
- opacity: 0; transition: opacity 0.2s;
514
- }
515
- .history-card:hover .history-delete-btn { opacity: 1; }
516
- .history-delete-btn:hover { background: rgba(255,0,0,0.9); }
517
-
518
- .vram-bar { width: 160px; height: 5px; background: rgba(255,255,255,0.08); border-radius: 999px; overflow: hidden; display: inline-block; vertical-align: middle; }
519
- .vram-used { height: 100%; background: var(--accent); width: 0%; transition: width 0.5s; }
520
-
521
- /* 智能多帧:工作流模式卡片式单选 */
522
- .smart-param-mode-label {
523
- font-size: 10px;
524
- color: var(--text-dim);
525
- font-weight: 700;
526
- margin-bottom: 8px;
527
- letter-spacing: 0.04em;
528
- text-transform: uppercase;
529
- }
530
- .smart-param-modes {
531
- display: flex;
532
- flex-direction: row;
533
- align-items: stretch;
534
- gap: 0;
535
- padding: 3px;
536
- margin-bottom: 12px;
537
- background: var(--panel-2);
538
- border-radius: 8px;
539
- border: 1px solid var(--border);
540
- }
541
- .smart-param-mode-opt {
542
- display: flex;
543
- align-items: center;
544
- justify-content: center;
545
- flex: 1;
546
- min-width: 0;
547
- gap: 0;
548
- margin: 0;
549
- padding: 6px 8px;
550
- border-radius: 6px;
551
- border: none;
552
- background: transparent;
553
- cursor: pointer;
554
- transition: background 0.15s, color 0.15s;
555
- position: relative;
556
- }
557
- .smart-param-mode-opt:hover:not(:has(input:checked)) {
558
- background: rgba(255, 255, 255, 0.05);
559
- }
560
- .smart-param-mode-opt input[type="radio"] {
561
- position: absolute;
562
- opacity: 0;
563
- width: 0;
564
- height: 0;
565
- margin: 0;
566
- }
567
- .smart-param-mode-opt:has(input:checked) {
568
- background: var(--accent);
569
- box-shadow: none;
570
- }
571
- .smart-param-mode-opt:has(input:checked) .smart-param-mode-title {
572
- color: #fff;
573
- }
574
- .smart-param-mode-title {
575
- font-size: 11px;
576
- font-weight: 600;
577
- color: var(--text-sub);
578
- text-align: center;
579
- line-height: 1.25;
580
- flex: none;
581
- min-width: 0;
582
- }
583
- /* 单次多关键帧:时间轴面板 */
584
- .batch-kf-panel {
585
- background: var(--item);
586
- border-radius: 10px;
587
- padding: 12px 14px;
588
- margin-bottom: 10px;
589
- border: 1px solid var(--border);
590
- }
591
- .batch-kf-panel-hd {
592
- display: flex;
593
- flex-wrap: wrap;
594
- align-items: center;
595
- justify-content: space-between;
596
- gap: 10px;
597
- margin-bottom: 8px;
598
- }
599
- .batch-kf-panel-title {
600
- font-size: 12px;
601
- font-weight: 700;
602
- color: var(--text);
603
- }
604
- .batch-kf-total-pill {
605
- font-size: 11px;
606
- color: var(--text-sub);
607
- background: var(--panel-2);
608
- border: 1px solid var(--border);
609
- border-radius: 999px;
610
- padding: 6px 12px;
611
- white-space: nowrap;
612
- }
613
- .batch-kf-total-pill strong {
614
- color: var(--accent);
615
- font-weight: 800;
616
- font-variant-numeric: tabular-nums;
617
- margin: 0 2px;
618
- }
619
- .batch-kf-total-unit {
620
- font-size: 10px;
621
- color: var(--text-dim);
622
- }
623
- .batch-kf-panel-hint {
624
- font-size: 10px;
625
- color: var(--text-dim);
626
- line-height: 1.5;
627
- margin: 0 0 12px;
628
- }
629
- .batch-kf-timeline-col {
630
- display: flex;
631
- flex-direction: column;
632
- gap: 0;
633
- }
634
- .batch-kf-kcard {
635
- border-radius: 10px;
636
- border: 1px solid var(--border);
637
- background: rgba(255, 255, 255, 0.03);
638
- padding: 10px 12px;
639
- }
640
- .batch-kf-kcard-head {
641
- display: flex;
642
- align-items: center;
643
- gap: 12px;
644
- margin-bottom: 10px;
645
- }
646
- .batch-kf-kthumb {
647
- width: 48px;
648
- height: 48px;
649
- border-radius: 8px;
650
- object-fit: cover;
651
- flex-shrink: 0;
652
- border: 1px solid var(--border);
653
- }
654
- .batch-kf-kcard-titles {
655
- display: flex;
656
- flex-direction: column;
657
- gap: 4px;
658
- min-width: 0;
659
- }
660
- .batch-kf-ktitle {
661
- font-size: 12px;
662
- font-weight: 700;
663
- color: var(--text);
664
- }
665
- .batch-kf-anchor {
666
- font-size: 11px;
667
- color: var(--accent);
668
- font-variant-numeric: tabular-nums;
669
- font-weight: 600;
670
- }
671
- .batch-kf-kcard-ctrl {
672
- display: flex;
673
- flex-wrap: wrap;
674
- align-items: center;
675
- gap: 12px;
676
- }
677
- .batch-kf-klabel {
678
- font-size: 10px;
679
- color: var(--text-dim);
680
- display: flex;
681
- align-items: center;
682
- gap: 8px;
683
- }
684
- .batch-kf-klabel input[type="number"] {
685
- width: 72px;
686
- padding: 6px 8px;
687
- font-size: 12px;
688
- border-radius: 6px;
689
- border: 1px solid var(--border);
690
- background: var(--panel);
691
- color: var(--text);
692
- }
693
- /* 关键帧之间:细时间轴 + 单行紧凑间隔输入 */
694
- .batch-kf-gap {
695
- display: flex;
696
- align-items: stretch;
697
- gap: 8px;
698
- padding: 0 0 6px;
699
- margin: 0 0 0 10px;
700
- }
701
- .batch-kf-gap-rail {
702
- width: 2px;
703
- flex-shrink: 0;
704
- border-radius: 2px;
705
- background: linear-gradient(
706
- 180deg,
707
- rgba(255, 255, 255, 0.06),
708
- var(--accent-dim),
709
- rgba(255, 255, 255, 0.04)
710
- );
711
- min-height: 22px;
712
- align-self: stretch;
713
- }
714
- .batch-kf-gap-inner {
715
- display: flex;
716
- align-items: center;
717
- gap: 8px;
718
- flex: 1;
719
- min-width: 0;
720
- padding: 2px 0 4px;
721
- }
722
- .batch-kf-gap-ix {
723
- font-size: 10px;
724
- font-weight: 600;
725
- color: var(--text-dim);
726
- font-variant-numeric: tabular-nums;
727
- letter-spacing: -0.02em;
728
- flex-shrink: 0;
729
- }
730
- .batch-kf-seg-field {
731
- display: inline-flex;
732
- align-items: center;
733
- gap: 3px;
734
- margin: 0;
735
- cursor: text;
736
- }
737
- .batch-kf-seg-input {
738
- width: 46px;
739
- min-width: 0;
740
- padding: 2px 5px;
741
- font-size: 11px;
742
- font-weight: 600;
743
- line-height: 1.3;
744
- border-radius: 4px;
745
- border: 1px solid var(--border);
746
- background: rgba(0, 0, 0, 0.2);
747
- color: var(--text);
748
- font-variant-numeric: tabular-nums;
749
- }
750
- .batch-kf-seg-input:hover {
751
- border-color: rgba(255, 255, 255, 0.12);
752
- }
753
- .batch-kf-seg-input:focus {
754
- outline: none;
755
- border-color: var(--accent);
756
- box-shadow: 0 0 0 1px var(--accent-ring);
757
- }
758
- .batch-kf-gap-unit {
759
- font-size: 10px;
760
- color: var(--text-dim);
761
- font-weight: 500;
762
- flex-shrink: 0;
763
- }
764
-
765
- .sub-mode-toggle { display: flex; background: var(--panel-2); border-radius: 7px; padding: 3px; border: 1px solid var(--border); }
766
- .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; }
767
- .sub-mode-btn.active { background: var(--accent); color: #fff; }
768
- .sub-mode-btn:hover:not(.active) { background: rgba(255,255,255,0.05); color: var(--text-sub); }
769
-
770
- .vid-section { display: none; margin-top: 12px; }
771
- .vid-section.active-section { display: block; animation: fadeIn 0.25s ease; }
772
- @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
773
-
774
- /* Status indicator */
775
- @keyframes breathe-orange {
776
- 0%,100% { box-shadow: 0 0 4px #FF9F0A; opacity: 0.7; }
777
- 50% { box-shadow: 0 0 10px #FF9F0A; opacity: 1; }
778
- }
779
- .indicator-busy { background: #FF9F0A !important; animation: breathe-orange 1.6s infinite ease-in-out !important; box-shadow: none !important; transition: all 0.3s; }
780
- .indicator-ready { background: #30D158 !important; box-shadow: 0 0 8px rgba(48,209,88,0.6) !important; animation: none !important; transition: all 0.3s; }
781
- .indicator-offline { background: #636366 !important; box-shadow: none !important; animation: none !important; transition: all 0.3s; }
782
-
783
- .res-preview-tag { font-size: 11px; color: var(--accent); margin-bottom: 10px; font-family: ui-monospace, monospace; }
784
- .top-status { display: flex; justify-content: space-between; font-size: 12px; color: var(--text-dim); margin-bottom: 8px; align-items: center; }
785
- .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); }
786
- .checkbox-container input { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; margin: 0; }
787
- .checkbox-container label { margin-bottom: 0; cursor: pointer; text-transform: none; color: var(--text); }
788
- .flex-row { display: flex; gap: 10px; }
789
- .flex-1 { flex: 1; min-width: 0; }
790
-
791
- @media (max-width: 1024px) {
792
- body { flex-direction: column; overflow-y: auto; }
793
- .sidebar { width: 100%; min-width: 100%; border-right: none; border-bottom: 1px solid var(--border); height: auto; overflow: visible; }
794
- .workspace { height: auto; min-height: 100vh; overflow: visible; }
795
- }
796
- :root {
797
- --plyr-color-main: #3F51B5;
798
- --plyr-video-control-background-hover: rgba(255,255,255,0.1);
799
- --plyr-control-radius: 6px;
800
- --plyr-player-width: 100%;
801
- }
802
- .plyr {
803
- border-radius: 8px;
804
- overflow: hidden;
805
- width: 100%;
806
- height: 100%;
807
- }
808
- .plyr--video .plyr__controls {
809
- background: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.8));
810
- padding: 20px 15px 15px 15px;
811
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
LTX2.3-1.0.4-new/UI/index.html DELETED
@@ -1,521 +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=dev-fp8-lora-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" 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
-
58
- <label style="font-size: 11px; margin-bottom: 6px; margin-top: 12px;" data-i18n="loraFolderPath">LoRA 文件夹路径</label>
59
- <div style="display: flex; gap: 6px; margin-bottom: 9px; align-items: stretch;">
60
- <input type="text" id="lora-dir-input" placeholder="留空使用默认路径" data-i18n-placeholder="loraFolderPathPlaceholder" style="flex: 1; height: 28px; box-sizing: border-box; font-size: 12px; padding: 0 10px;">
61
- <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>
62
- </div>
63
- <div id="lora-dir-status" style="font-size: 10px; color: var(--text-dim);"></div>
64
- </div>
65
- </div>
66
-
67
- <div class="sidebar-section" id="main-tabs-section">
68
- <div class="tabs">
69
- <div id="tab-video" class="tab" onclick="switchMode('video')">
70
- <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>
71
- <span data-i18n="tabVideo">视频生成</span>
72
- </div>
73
- <div id="tab-batch" class="tab" onclick="switchMode('batch')">
74
- <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>
75
- <span data-i18n="tabBatch">智能多帧</span>
76
- </div>
77
- <div id="tab-motion" class="tab" onclick="switchMode('motion')">
78
- <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>
79
- <span>动作迁移</span>
80
- </div>
81
- <div id="tab-image" class="tab" onclick="switchMode('image')">
82
- <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>
83
- <span data-i18n="tabImage">图像生成</span>
84
- </div>
85
- <div id="tab-tts" class="tab" onclick="switchMode('tts')">
86
- <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>
87
- <span data-i18n="tabTts">TTS 语音</span>
88
- </div>
89
- </div>
90
-
91
- <div id="prompt-container">
92
- <label data-i18n="promptLabel">视觉描述词 (Prompt)</label>
93
- <textarea id="prompt" data-i18n-placeholder="promptPlaceholder" placeholder="在此输入视觉描述词 (Prompt)..." style="height: 90px; margin-bottom: 0;"></textarea>
94
- </div>
95
- </div>
96
-
97
- <!-- 视频模式选项 -->
98
- <div class="sidebar-section" id="video-opts" style="display:none">
99
- <div class="setting-group">
100
- <div class="group-title" data-i18n="basicEngine">基础画面 / Basic EngineSpecs</div>
101
- <div class="flex-row">
102
- <div class="flex-1">
103
- <label data-i18n="qualityLevel">清晰度级别</label>
104
- <select id="vid-quality" onchange="updateResPreview()">
105
- <option value="1080">1080P Full HD</option>
106
- <option value="720" selected>720P Standard</option>
107
- <option value="540">540P Preview</option>
108
- </select>
109
- </div>
110
- <div class="flex-1">
111
- <label data-i18n="aspectRatio">画幅比例</label>
112
- <select id="vid-ratio" onchange="updateResPreview()">
113
- <option value="16:9" data-i18n="ratio169">16:9 电影宽幅</option>
114
- <option value="9:16" data-i18n="ratio916">9:16 移动竖屏</option>
115
- </select>
116
- </div>
117
- </div>
118
- <div id="res-preview" class="res-preview-tag" style="margin-top: -5px; margin-bottom: 12px;">最终发送: 1280x704</div>
119
-
120
- <div class="flex-row">
121
- <div class="flex-1">
122
- <label data-i18n="fpsLabel">帧率 (FPS)</label>
123
- <select id="vid-fps">
124
- <option value="24" selected>24 FPS</option>
125
- <option value="25">25 FPS</option>
126
- <option value="30">30 FPS</option>
127
- <option value="48">48 FPS</option>
128
- <option value="60">60 FPS</option>
129
- </select>
130
- </div>
131
- <div class="flex-1">
132
- <label data-i18n="durationLabel">时长 (秒)</label>
133
- <input type="number" id="vid-duration" value="5" min="1" max="30" step="1">
134
- </div>
135
- </div>
136
-
137
- <label style="margin-top: 12px;" data-i18n="cameraMotion">镜头运动方式</label>
138
- <select id="vid-motion">
139
- <option value="static" selected data-i18n="motionStatic">Static (静止机位)</option>
140
- <option value="dolly_in" data-i18n="motionDollyIn">Dolly In (推近)</option>
141
- <option value="dolly_out" data-i18n="motionDollyOut">Dolly Out (拉远)</option>
142
- <option value="dolly_left" data-i18n="motionDollyLeft">Dolly Left (向左)</option>
143
- <option value="dolly_right" data-i18n="motionDollyRight">Dolly Right (向右)</option>
144
- <option value="jib_up" data-i18n="motionJibUp">Jib Up (升臂)</option>
145
- <option value="jib_down" data-i18n="motionJibDown">Jib Down (降臂)</option>
146
- <option value="focus_shift" data-i18n="motionFocus">Focus Shift (焦点)</option>
147
- </select>
148
- <div class="checkbox-container" style="margin-top: 8px;">
149
- <input type="checkbox" id="vid-audio" checked>
150
- <label for="vid-audio" data-i18n="audioGen">生成 AI 环境音 (Audio Gen)</label>
151
- </div>
152
-
153
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
154
- <label data-i18n="selectLora" style="margin: 0;">选择 LoRA</label>
155
- <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>
156
- </div>
157
- <div id="loras-container" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px;"></div>
158
- </div>
159
-
160
- <!-- 生成媒介组 -->
161
- <div class="setting-group" id="video-source-group">
162
- <div class="group-title" data-i18n="genSource">生成媒介 / Generation Source</div>
163
-
164
- <div class="flex-row" style="margin-bottom: 10px;">
165
- <div class="flex-1">
166
- <label data-i18n="startFrame">起始帧 (首帧)</label>
167
- <div class="upload-zone" id="start-frame-drop-zone" onclick="document.getElementById('start-frame-input').click()">
168
- <div class="clear-img-overlay" id="clear-start-frame-overlay" onclick="event.stopPropagation(); clearFrame('start')">×</div>
169
- <div id="start-frame-placeholder">
170
- <div class="upload-icon">🖼️</div>
171
- <div class="upload-text" data-i18n="uploadStart">上传首帧</div>
172
- </div>
173
- <img id="start-frame-preview" class="preview-thumb">
174
- <input type="file" id="start-frame-input" accept="image/*" style="display:none" onchange="handleFrameUpload(this.files[0], 'start')">
175
- </div>
176
- <input type="hidden" id="start-frame-path">
177
- </div>
178
- <div class="flex-1">
179
- <label data-i18n="endFrame">结束帧 (尾帧)</label>
180
- <div class="upload-zone" id="end-frame-drop-zone" onclick="document.getElementById('end-frame-input').click()">
181
- <div class="clear-img-overlay" id="clear-end-frame-overlay" onclick="event.stopPropagation(); clearFrame('end')">×</div>
182
- <div id="end-frame-placeholder">
183
- <div class="upload-icon">🏁</div>
184
- <div class="upload-text" data-i18n="uploadEnd">上传尾帧 (可选)</div>
185
- </div>
186
- <img id="end-frame-preview" class="preview-thumb">
187
- <input type="file" id="end-frame-input" accept="image/*" style="display:none" onchange="handleFrameUpload(this.files[0], 'end')">
188
- </div>
189
- <input type="hidden" id="end-frame-path">
190
- </div>
191
- </div>
192
-
193
- <div class="flex-row">
194
- <div class="flex-1">
195
- <label data-i18n="refAudio">参考音频 (A2V)</label>
196
- <div class="upload-zone" id="audio-drop-zone" onclick="document.getElementById('vid-audio-input').click()">
197
- <div class="clear-img-overlay" id="clear-audio-overlay" onclick="event.stopPropagation(); clearUploadedAudio()">×</div>
198
- <div id="audio-upload-placeholder">
199
- <div class="upload-icon">🎵</div>
200
- <div class="upload-text" data-i18n="uploadAudio">点击上传音频</div>
201
- </div>
202
- <div id="audio-upload-status" style="display:none;">
203
- <div class="upload-icon" style="color:var(--accent); opacity:1;">✔️</div>
204
- <div id="audio-filename-status" class="upload-text"></div>
205
- </div>
206
- <input type="file" id="vid-audio-input" accept="audio/*" style="display:none" onchange="handleAudioUpload(this.files[0])">
207
- </div>
208
- <input type="hidden" id="uploaded-audio-path">
209
- </div>
210
- </div>
211
- <div style="font-size: 10px; color: var(--text-dim); text-align: center; margin-top: 5px;" data-i18n="sourceHint">
212
- 💡 若仅上传首帧 = 图生视频/音视频;若同时上传首尾帧 = 首尾插帧。
213
- </div>
214
- </div>
215
- </div>
216
-
217
- <!-- 图像模式选项 -->
218
- <div class="sidebar-section" id="motion-opts" style="display:none">
219
- <div class="setting-group">
220
- <div class="group-title">动作迁移 / Motion Transfer</div>
221
- <label>参考动作视频</label>
222
- <div class="upload-zone" id="motion-video-drop-zone" onclick="document.getElementById('motion-video-input').click()" style="height: 96px;">
223
- <div class="clear-img-overlay" id="clear-motion-video-overlay" onclick="event.stopPropagation(); clearMotionVideo()">×</div>
224
- <div id="motion-video-placeholder">
225
- <div class="upload-icon">🎬</div>
226
- <div>点击或拖拽视频</div>
227
- <small>提取动作/轮廓作为控制</small>
228
- </div>
229
- <div id="motion-video-status" style="display:none; padding: 10px; text-align:center;">
230
- <div style="font-size: 22px;">🎞️</div>
231
- <div id="motion-video-name" style="font-size: 11px; color: var(--text-sub); word-break: break-all;"></div>
232
- </div>
233
- </div>
234
- <input type="file" id="motion-video-input" accept="video/*" hidden onchange="handleMotionVideoUpload(this.files[0])">
235
- <input type="hidden" id="motion-video-path">
236
-
237
- <label style="margin-top: 12px;">目标主体图</label>
238
- <div class="upload-zone" id="motion-image-drop-zone" onclick="document.getElementById('motion-image-input').click()" style="height: 130px;">
239
- <div class="clear-img-overlay" id="clear-motion-image-overlay" onclick="event.stopPropagation(); clearMotionImage()">×</div>
240
- <div id="motion-image-placeholder">
241
- <div class="upload-icon">🖼️</div>
242
- <div>点击或拖拽图片</div>
243
- <small>作为主体/首帧引导</small>
244
- </div>
245
- <img id="motion-image-preview" style="display:none; width:100%; height:100%; object-fit:cover; border-radius:10px;">
246
- </div>
247
- <input type="file" id="motion-image-input" accept="image/*" hidden onchange="handleMotionImageUpload(this.files[0])">
248
- <input type="hidden" id="motion-image-path">
249
-
250
- <div class="flex-row" style="margin-top: 12px;">
251
- <div class="flex-1">
252
- <label>控制类型</label>
253
- <select id="motion-conditioning-type">
254
- <option value="canny" selected>Canny 轮廓</option>
255
- <option value="depth">Depth 深度</option>
256
- </select>
257
- </div>
258
- <div class="flex-1">
259
- <label style="display:flex;justify-content:space-between;align-items:center;">
260
- <span>控制强度</span>
261
- <span id="motion-strength-val" style="color:var(--accent);font-size:11px;font-weight:800;">0.5</span>
262
- </label>
263
- <input type="range" id="motion-strength" value="0.5" min="0.1" max="2" step="0.1" oninput="document.getElementById('motion-strength-val').textContent = this.value">
264
- </div>
265
- </div>
266
- <div style="font-size:11px;color:var(--text-dim);line-height:1.5;margin-top:8px;">
267
- 第一版使用 LTX 官方 IC-LoRA:参考视频负责动作/构图,目标图负责主体一致性。
268
- </div>
269
- </div>
270
- </div>
271
-
272
- <div id="image-opts" class="sidebar-section" style="display:none">
273
- <label data-i18n="imgPreset">预设分辨率 (Presets)</label>
274
- <select id="img-res-preset" onchange="applyImgPreset(this.value)">
275
- <option value="1024x1024" data-i18n="imgOptSquare">1:1 Square (1024x1024)</option>
276
- <option value="1280x720" data-i18n="imgOptLand">16:9 Landscape (1280x720)</option>
277
- <option value="720x1280" data-i18n="imgOptPort">9:16 Portrait (720x1280)</option>
278
- <option value="custom" data-i18n="imgOptCustom">Custom 自定义...</option>
279
- </select>
280
-
281
- <div id="img-custom-res" class="flex-row" style="margin-top: 10px;">
282
- <div class="flex-1"><label data-i18n="width">宽度</label><input type="number" id="img-w" value="1024" onchange="updateImgResPreview()"></div>
283
- <div class="flex-1"><label data-i18n="height">高度</label><input type="number" id="img-h" value="1024" onchange="updateImgResPreview()"></div>
284
- </div>
285
- <div id="img-res-preview" class="res-preview-tag">最终发送: 1024x1024</div>
286
-
287
- <div class="label-group" style="margin-top: 15px;">
288
- <label data-i18n="samplingSteps">采样步数 (Steps)</label>
289
- <span class="val-badge" id="stepsVal">28</span>
290
- </div>
291
- <div class="slider-container">
292
- <input type="range" id="img-steps" min="1" max="50" value="28" oninput="document.getElementById('stepsVal').innerText=this.value">
293
- </div>
294
- </div>
295
-
296
- <!-- 智能多帧模式 -->
297
- <div class="sidebar-section" id="batch-opts" style="display:none">
298
- <div class="setting-group">
299
- <div class="group-title" data-i18n="smartMultiFrameGroup">智能多帧</div>
300
- <div class="smart-param-mode-label" data-i18n="workflowModeLabel">工作流模式(点击切换)</div>
301
- <div class="smart-param-modes" role="radiogroup" aria-label="工作流模式">
302
- <label class="smart-param-mode-opt">
303
- <input type="radio" name="batch-workflow" value="single" checked onchange="onBatchWorkflowChange()">
304
- <span class="smart-param-mode-title" data-i18n="wfSingle">单次多关键帧</span>
305
- </label>
306
- <label class="smart-param-mode-opt">
307
- <input type="radio" name="batch-workflow" value="segments" onchange="onBatchWorkflowChange()">
308
- <span class="smart-param-mode-title" data-i18n="wfSegments">分段拼接</span>
309
- </label>
310
- </div>
311
-
312
- <label data-i18n="uploadImages">上传图片</label>
313
- <div class="upload-zone" id="batch-images-drop-zone" onclick="document.getElementById('batch-images-input').click()" style="min-height: 72px; margin-bottom: 0;">
314
- <div id="batch-images-placeholder">
315
- <div class="upload-icon">📁</div>
316
- <div class="upload-text" data-i18n="uploadMulti1">点击或拖入多张图片</div>
317
- <div class="upload-hint" data-i18n="uploadMulti2">支持一次选多张,可多次添加</div>
318
- </div>
319
- <input type="file" id="batch-images-input" accept="image/*" multiple style="display:none" onchange="handleBatchImagesUpload(this.files, true)">
320
- </div>
321
- <input type="hidden" id="batch-images-path">
322
-
323
- <div class="batch-thumb-strip-wrap" id="batch-thumb-strip-wrap" style="display: none;">
324
- <div class="batch-thumb-strip-head">
325
- <span class="batch-thumb-strip-title" data-i18n="batchStripTitle">已选图片 · 顺序 = 播放先后</span>
326
- <span class="batch-thumb-strip-hint" data-i18n="batchStripHint">在缩略图上按住拖动排序;松手落入虚线框位置</span>
327
- </div>
328
- <div class="batch-images-container" id="batch-images-container"></div>
329
- </div>
330
-
331
- <div style="font-size: 10px; color: var(--text-dim); margin-bottom: 12px; margin-top: 10px; line-height: 1.45;" data-i18n-html="batchFfmpegHint">
332
- 💡 <strong>分段模式</strong>:2 张 = 1 段;3 张 = 2 段再拼接。<strong>单次模式</strong>:几张图就几个 latent 锚点,一条视频出片。<br>
333
- 多段需 <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 完整路径。
334
- </div>
335
-
336
- <label style="margin-top: 8px;" data-i18n="bgmLabel">成片配乐(可选,统一音轨)</label>
337
- <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;">
338
- <div class="clear-img-overlay" id="clear-batch-audio-overlay" onclick="event.stopPropagation(); clearBatchBackgroundAudio()" style="display: none;">×</div>
339
- <div id="batch-audio-placeholder">
340
- <div class="upload-text" style="font-size: 11px;" data-i18n="bgmUploadHint">上传一条完整 BGM(生成完成后会替换整段成片的音轨)</div>
341
- </div>
342
- <div id="batch-audio-status" style="display: none; font-size: 11px; color: var(--accent);"></div>
343
- <input type="file" id="batch-audio-input" accept="audio/*" style="display:none" onchange="handleBatchBackgroundAudioUpload(this.files[0])">
344
- </div>
345
- <input type="hidden" id="batch-background-audio-path">
346
-
347
- <div id="batch-segments-container" style="margin-top: 15px;"></div>
348
- </div>
349
-
350
- <div class="setting-group">
351
- <div class="group-title" data-i18n="basicEngine">基础画面 / Basic EngineSpecs</div>
352
- <div class="flex-row">
353
- <div class="flex-1">
354
- <label data-i18n="qualityLevel">清晰度级别</label>
355
- <select id="batch-quality" onchange="updateBatchResPreview()">
356
- <option value="1080">1080P Full HD</option>
357
- <option value="720" selected>720P Standard</option>
358
- <option value="540">540P Preview</option>
359
- </select>
360
- </div>
361
- <div class="flex-1">
362
- <label data-i18n="aspectRatio">画幅比例</label>
363
- <select id="batch-ratio" onchange="updateBatchResPreview()">
364
- <option value="16:9" data-i18n="ratio169">16:9 电影宽幅</option>
365
- <option value="9:16" data-i18n="ratio916">9:16 移动竖屏</option>
366
- </select>
367
- </div>
368
- </div>
369
- <div id="batch-res-preview" class="res-preview-tag" style="margin-top: -5px; margin-bottom: 12px;">最终发送: 1280x704</div>
370
-
371
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
372
- <label data-i18n="selectLora" style="margin: 0;">选择 LoRA</label>
373
- <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>
374
- </div>
375
- <div id="batch-loras-container" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px;"></div>
376
- </div>
377
- </div>
378
-
379
- <!-- TTS 语音合成面板 -->
380
- <div id="tts-opts" class="sidebar-section" style="display:none; padding-top: 0;">
381
- <!-- 状态栏 -->
382
- <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">
383
- 🔍 正在检测 TTS 模型...
384
- </div>
385
-
386
- <!-- 合成文本 -->
387
- <div class="setting-group">
388
- <div class="group-title" data-i18n="ttsTextTitle">合成文本 / Text</div>
389
- <label style="font-size:11px; color:var(--text-dim); margin-bottom:5px; display:block;" data-i18n-html="ttsTextHint">
390
- 支持在文本开头加英文括号描述声音,例如:<code style="font-size:10px;">(年轻女声,温柔甜美)</code>
391
- </label>
392
- <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>
393
- </div>
394
-
395
- <!-- 合成模式 -->
396
- <div class="setting-group">
397
- <div class="group-title" data-i18n="ttsModeTitle">合成模式 / Mode</div>
398
- <select id="tts-mode" onchange="onTtsModeChange()" style="margin-bottom:8px;">
399
- <option value="text_only" data-i18n="ttsModeTextOnly">🗣️ 文字转语音(含声音设计)</option>
400
- <option value="clone" data-i18n="ttsModeClone">🎙️ 声音克隆</option>
401
- <option value="ultimate_clone" data-i18n="ttsModeUltimate">⭐ 终极克隆(最高还原度)</option>
402
- </select>
403
-
404
- <!-- 声音克隆:参考音频 -->
405
- <div id="tts-ref-section" style="display:none;">
406
- <label style="font-size:11px; margin-bottom:4px; display:block;" data-i18n="ttsRefLabel">📎 参考音频(Reference)</label>
407
- <div class="upload-zone" id="tts-ref-drop" onclick="document.getElementById('tts-ref-input').click()" style="min-height:48px; margin-bottom:6px; position:relative;">
408
- <div class="clear-img-overlay" id="tts-ref-clear" onclick="event.stopPropagation(); clearTtsRef()" style="display:none;">×</div>
409
- <div id="tts-ref-placeholder">
410
- <div class="upload-icon">🎵</div>
411
- <div class="upload-text" style="font-size:11px;" data-i18n="ttsRefUploadHint">点击上传参考音频 (.wav / .mp3)</div>
412
- </div>
413
- <div id="tts-ref-status" style="display:none; font-size:11px; color:var(--accent); padding:8px; text-align:center;"></div>
414
- <input type="file" id="tts-ref-input" accept="audio/*" style="display:none" onchange="handleTtsRefUpload(this.files[0])">
415
- </div>
416
- </div>
417
-
418
- <!-- 终极克隆额外选项 -->
419
- <div id="tts-ultimate-section" style="display:none;">
420
- <label style="font-size:11px; margin-bottom:4px; display:block;" data-i18n="ttsUltimateLabel">📝 参考音频对应的文本转录(可选,能显著提升相似度)</label>
421
- <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>
422
- </div>
423
- </div>
424
-
425
- <!-- 参数调节 -->
426
- <div class="setting-group">
427
- <div class="group-title" data-i18n="ttsParamsTitle">高级参数 / Parameters</div>
428
- <div class="flex-row" style="gap:12px;">
429
- <div class="flex-1">
430
- <div class="label-group">
431
- <label style="font-size:11px;" data-i18n="ttsCfgLabel">CFG 强度</label>
432
- <span class="val-badge" id="ttsCfgVal">2.0</span>
433
- </div>
434
- <div class="slider-container">
435
- <input type="range" id="tts-cfg" min="0.5" max="5.0" step="0.5" value="2.0"
436
- oninput="document.getElementById('ttsCfgVal').textContent=this.value">
437
- </div>
438
- </div>
439
- <div class="flex-1">
440
- <div class="label-group">
441
- <label style="font-size:11px;" data-i18n="ttsStepsLabel">推理步数</label>
442
- <span class="val-badge" id="ttsStepsVal">10</span>
443
- </div>
444
- <div class="slider-container">
445
- <input type="range" id="tts-steps" min="5" max="50" step="5" value="10"
446
- oninput="document.getElementById('ttsStepsVal').textContent=this.value">
447
- </div>
448
- </div>
449
- </div>
450
- </div>
451
-
452
- <!-- 输出结果播放区 -->
453
- <div id="tts-result-section" style="display:none;" class="setting-group">
454
- <div class="group-title" data-i18n="ttsResultTitle">生成结果 / Output</div>
455
- <audio id="tts-audio-player" controls style="width:100%; border-radius:8px; margin-bottom:6px;"></audio>
456
- <a id="tts-download-link" href="#" download style="font-size:11px; color:var(--accent); text-decoration:none;" data-i18n="ttsDownload">⬇️ 下载音频</a>
457
- </div>
458
-
459
- <div style="padding: 0 0 10px 0;">
460
- <button class="btn-primary" id="tts-gen-btn" onclick="runTts()" data-i18n="ttsGenBtn">🎙️ 开始生成语音</button>
461
- </div>
462
- </div>
463
-
464
- <div style="padding: 0 30px 30px 30px;">
465
- <button class="btn-primary" id="mainBtn" onclick="run()" data-i18n="mainRender">开始渲染</button>
466
- </div>
467
- </aside>
468
-
469
- <main class="workspace">
470
- <section class="viewer" id="viewer-section">
471
- <div class="monitor" id="viewer">
472
- <div id="loading-txt" data-i18n="waitingTask">等待分配渲染任务...</div>
473
- <img id="res-img" src="">
474
- <div id="video-wrapper" style="width:100%; height:100%; display:none; max-height:100%; align-items:center; justify-content:center;">
475
- <video id="res-video" autoplay loop playsinline></video>
476
- </div>
477
- <div id="audio-wrapper">
478
- <div class="audio-preview-art" id="audio-preview-art" role="button" tabindex="0">
479
- <div class="audio-preview-icon">♪</div>
480
- <div id="audio-preview-title">TTS Audio</div>
481
- </div>
482
- <audio id="res-audio" controls></audio>
483
- </div>
484
- <div class="progress-container"><div id="progress-fill"></div></div>
485
- </div>
486
- </section>
487
-
488
- <!-- Drag Handle -->
489
- <div id="resize-handle" style="
490
- height: 5px; background: transparent; cursor: row-resize;
491
- flex-shrink: 0; position: relative; z-index: 50;
492
- display: flex; align-items: center; justify-content: center;
493
- " data-i18n-title="resizeHandleTitle" title="拖动调整面板高度">
494
- <div style="width: 40px; height: 3px; background: var(--border); border-radius: 999px; pointer-events: none;"></div>
495
- </div>
496
-
497
- <section class="library" id="library-section">
498
- <div style="display: flex; justify-content: space-between; margin-bottom: 15px; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 10px;">
499
- <div style="display: flex; gap: 20px;">
500
- <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>
501
- <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>
502
- </div>
503
- <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>
504
- </div>
505
-
506
- <div id="log-container" style="display: none; flex: 1; flex-direction: column;">
507
- <div id="log" data-i18n="logReady">> LTX-2 Studio Ready. Expecting commands...</div>
508
- </div>
509
-
510
- <div id="history-wrapper">
511
- <div id="history-container"></div>
512
- </div>
513
- <div id="pagination-bar" style="display:none;"></div>
514
- </section>
515
- </main>
516
- <script src="https://cdnjs.cloudflare.com/ajax/libs/plyr/3.7.8/plyr.min.js"></script>
517
- <script src="i18n.js?v=motion-transfer-1"></script>
518
- <script src="index.js?v=motion-strength-1"></script>
519
-
520
- </body>
521
- </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 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:abf2075055a2501f4c8aadaff11b811b59fbbe4d02a931f98b5f194cacbe4f48
3
- size 111478
 
 
 
 
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 (11.3 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__/tts_worker.cpython-313.pyc DELETED
Binary file (11.3 kB)
 
LTX2.3-1.0.4-new/patches/api_types.py DELETED
@@ -1,397 +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"]
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: Literal["16:9", "9:16"] = "16:9"
323
- modelPath: str | None = None
324
- loraPath: str | None = None
325
- loraStrength: float = 1.0
326
- loraPaths: list[str] | None = None
327
- loraStrengths: list[float] | None = None
328
-
329
-
330
- class GenerateImageRequest(BaseModel):
331
- prompt: NonEmptyPrompt
332
- width: int = 1024
333
- height: int = 1024
334
- numSteps: int = 4
335
- numImages: int = 1
336
-
337
-
338
- def _default_model_types() -> set[ModelFileType]:
339
- return set()
340
-
341
-
342
- class ModelDownloadRequest(BaseModel):
343
- modelTypes: set[ModelFileType] = Field(default_factory=_default_model_types)
344
-
345
-
346
- class RequiredModelsResponse(BaseModel):
347
- modelTypes: list[ModelFileType]
348
-
349
-
350
- class SuggestGapPromptRequest(BaseModel):
351
- beforePrompt: str = ""
352
- afterPrompt: str = ""
353
- beforeFrame: str | None = None
354
- afterFrame: str | None = None
355
- gapDuration: float = 5
356
- mode: str = "t2v"
357
- inputImage: str | None = None
358
-
359
-
360
- class RetakeRequest(BaseModel):
361
- video_path: str
362
- start_time: float = 0
363
- duration: float = 0
364
- prompt: str = ""
365
- mode: str = "replace_video_only"
366
- width: int | None = None
367
- height: int | None = None
368
-
369
-
370
- class IcLoraExtractRequest(BaseModel):
371
- video_path: str
372
- conditioning_type: Literal["canny", "depth"] = "canny"
373
- frame_time: float = 0
374
-
375
-
376
- class IcLoraImageInput(BaseModel):
377
- path: str
378
- frame: int = 0
379
- strength: float = 1.0
380
-
381
-
382
- def _default_ic_lora_images() -> list[IcLoraImageInput]:
383
- return []
384
-
385
-
386
- class IcLoraGenerateRequest(BaseModel):
387
- video_path: str
388
- conditioning_type: Literal["canny", "depth"]
389
- prompt: NonEmptyPrompt
390
- conditioning_strength: float = 1.0
391
- num_inference_steps: int = 30
392
- cfg_guidance_scale: float = 1.0
393
- negative_prompt: str = ""
394
- images: list[IcLoraImageInput] = Field(default_factory=_default_ic_lora_images)
395
-
396
-
397
- ConditioningType: TypeAlias = Literal["canny", "depth"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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\1.0.4备份\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,244 +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 install_low_vram_on_pipelines(handler: Any) -> None:
60
- """启动时读取偏好,挂到 pipelines 上供各补丁读取。"""
61
- pl = handler.pipelines
62
- low = read_low_vram_pref()
63
- setattr(pl, "low_vram_mode", bool(low))
64
- if low:
65
- apply_low_vram_config_tweaks(handler)
66
- logger.info(
67
- "low_vram_mode: 已开启(尝试关闭 fast 超分;若显存仍高,多为权重常驻 GPU,需降分辨率/时长或 FP8 权重)"
68
- )
69
-
70
-
71
- def install_low_vram_pipeline_hooks(pl: Any) -> None:
72
- """在 load_gpu_pipeline / load_a2v 返回后尝试 Diffusers 式 CPU offload(无则静默)。"""
73
- if getattr(pl, "_ltx_low_vram_hooks_installed", False):
74
- return
75
- pl._ltx_low_vram_hooks_installed = True
76
-
77
- if hasattr(pl, "load_gpu_pipeline"):
78
- _orig_gpu = pl.load_gpu_pipeline
79
- pl._ltx_orig_load_gpu_for_low_vram = _orig_gpu
80
-
81
- def _load_gpu_wrapped(self: Any, *a: Any, **kw: Any) -> Any:
82
- r = _orig_gpu(*a, **kw)
83
- if getattr(self, "low_vram_mode", False):
84
- try_sequential_offload_on_pipeline_state(r)
85
- return r
86
-
87
- pl.load_gpu_pipeline = types.MethodType(_load_gpu_wrapped, pl)
88
-
89
- if hasattr(pl, "load_a2v_pipeline"):
90
- _orig_a2v = pl.load_a2v_pipeline
91
- pl._ltx_orig_load_a2v_for_low_vram = _orig_a2v
92
-
93
- def _load_a2v_wrapped(self: Any, *a: Any, **kw: Any) -> Any:
94
- r = _orig_a2v(*a, **kw)
95
- if getattr(self, "low_vram_mode", False):
96
- try_sequential_offload_on_pipeline_state(r)
97
- return r
98
-
99
- pl.load_a2v_pipeline = types.MethodType(_load_a2v_wrapped, pl)
100
-
101
- # Monkey patch: 接管 1.0.3 新增的底层 layer streaming 来实现完美的线性显存控制
102
- if not getattr(pl, "_ltx_layer_streaming_patched", False):
103
- pl._ltx_layer_streaming_patched = True
104
- try:
105
- def _patch_pipeline_class(cls_name, mod_name):
106
- import importlib
107
- try:
108
- mod = importlib.import_module(mod_name)
109
- pipeline_cls = getattr(mod, cls_name)
110
- _orig_call = pipeline_cls.__call__
111
-
112
- def _patched_call(self, *args, **kwargs):
113
- lim = get_vram_limit()
114
- if lim is not None:
115
- count = get_streaming_prefetch_count()
116
- kwargs["streaming_prefetch_count"] = count
117
- if count is None:
118
- logger.info(
119
- "low_vram_mode: VRAM limit is unlimited/high. Disabled layer streaming."
120
- )
121
- else:
122
- logger.info(
123
- "low_vram_mode: Dynamically tuned layer streaming prefetch count to %s for %sGB limit.",
124
- count,
125
- lim,
126
- )
127
-
128
- return _orig_call(self, *args, **kwargs)
129
-
130
- pipeline_cls.__call__ = _patched_call
131
- logger.info(f"low_vram_mode: Successfully patched {cls_name} to override streaming_prefetch_count")
132
- except Exception as e:
133
- pass
134
-
135
- _patch_pipeline_class("DistilledPipeline", "ltx_pipelines.distilled")
136
- _patch_pipeline_class("TI2VidTwoStagesPipeline", "ltx_pipelines.ti2vid_two_stages")
137
- _patch_pipeline_class("LTXRetakePipeline", "services.retake_pipeline.ltx_retake_pipeline")
138
- _patch_pipeline_class("ICLoRAPipeline", "services.ic_lora_pipeline.ltx_ic_lora_pipeline")
139
- _patch_pipeline_class("A2VPipeline", "services.a2v_pipeline.distilled_a2v_pipeline")
140
- except Exception:
141
- pass
142
-
143
-
144
- def get_vram_limit() -> float | None:
145
- try:
146
- import json
147
- from pathlib import Path
148
- settings_file = _ltx_desktop_config_dir() / "settings.json"
149
- if settings_file.exists():
150
- with open(settings_file, "r", encoding="utf-8") as f:
151
- data = json.load(f)
152
- if "vram_limit" in data:
153
- lim = data["vram_limit"]
154
- if lim != "":
155
- return float(lim)
156
- except Exception:
157
- pass
158
- return None
159
-
160
-
161
- def get_streaming_prefetch_count() -> int | None:
162
- """把设置里的显存上限映射为 layer streaming 强度。
163
-
164
- ``0`` 或留空表示纯 GPU / 速度优先,不启用层流式加载。
165
- """
166
- lim = get_vram_limit()
167
- if lim is None or lim == 0:
168
- return None
169
- if lim <= 10.0:
170
- return 1
171
- if lim >= 25.0:
172
- return None
173
- extra_gb = float(lim) - 10.0
174
- return max(1, min(32, 1 + round(extra_gb / 0.67)))
175
-
176
-
177
- def should_use_cpu_offload() -> bool:
178
- """只有设置了大于 0 的显存上限时才启用 CPU/offload 慢速兼容路径。"""
179
- lim = get_vram_limit()
180
- return lim is not None and lim > 0
181
-
182
-
183
- def try_sequential_offload_on_pipeline_state(state: Any) -> None:
184
- """按设定最高显存分配,爆显存后写入系统内存"""
185
- if state is None:
186
- return
187
- if not should_use_cpu_offload():
188
- logger.info(
189
- "low_vram_mode: VRAM limit is 0/blank. Skip CPU offload for pure GPU speed."
190
- )
191
- return
192
- root = getattr(state, "pipeline", state)
193
- candidates: list[Any] = [root]
194
- inner = getattr(root, "pipeline", None)
195
- if inner is not None and inner is not root:
196
- candidates.append(inner)
197
-
198
- # Capped-VRAM mode applies macro offload so T5/VAE can leave GPU while DiT runs.
199
- # Pure GPU mode returns above and keeps the old fast path.
200
- for obj in candidates:
201
- for method_name in (
202
- "enable_model_cpu_offload",
203
- "enable_sequential_cpu_offload",
204
- ):
205
- fn = getattr(obj, method_name, None)
206
- if callable(fn):
207
- try:
208
- fn()
209
- logger.info(
210
- "low_vram_mode: 已对管线调用 %s()",
211
- method_name,
212
- )
213
- return
214
- except Exception as e:
215
- logger.debug(
216
- "low_vram_mode: %s() 失败(可忽略): %s",
217
- method_name,
218
- e,
219
- )
220
-
221
-
222
- def maybe_release_pipeline_after_task(handler: Any) -> None:
223
- """单次生成结束后:低显存模式下强制卸载管线并回收缓存。"""
224
- pl = getattr(handler, "pipelines", None) or getattr(handler, "_pipelines", None)
225
- if pl is None or not getattr(pl, "low_vram_mode", False):
226
- return
227
- try:
228
- from keep_models_runtime import force_unload_gpu_pipeline
229
-
230
- force_unload_gpu_pipeline(pl)
231
- except Exception as e:
232
- logger.debug("low_vram_mode: 任务后卸载失败: %s", e)
233
- try:
234
- pl._pipeline_signature = None
235
- except Exception:
236
- pass
237
- gc.collect()
238
- try:
239
- import torch
240
-
241
- if torch.cuda.is_available():
242
- torch.cuda.empty_cache()
243
- except Exception:
244
- 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/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