Files
dj/model-bridge/patch_desktop_video.py
T

122 lines
7.3 KiB
Python

"""Reproducible, version-checked local-video extension for HuobaoDrama 4.0.10.
Prepare by default; --install replaces the archive/frontend after the app closes.
Original files stay in integration-work/backups/multivideo-4.0.10.
"""
import hashlib
import json
from pathlib import Path
import shutil
import struct
import sys
ROOT = Path(__file__).resolve().parent.parent
WORK = ROOT / "integration-work"
RES = ROOT / "HuobaoDrama/resources"
BACKUP = WORK / "backups/multivideo-4.0.10"
MODELS = ["MiniMax-H3", "MiniMax-H3-Hybrid", "MiniMax-H3-R2V-4Step", "LTX-2.3", "LTX-2.3-Text-Preserve", "Wan2.2-I2V"]
def replace_once(text, old, new):
if text.count(old) != 1:
raise RuntimeError("Desktop version mismatch: " + old[:90])
return text.replace(old, new, 1)
def prepare():
# Never rebuild a newer installation from an old backup archive.
installed = (RES / 'app.asar').read_bytes()
_, hs, _, js = struct.unpack('<4I', installed[:16])
header = json.loads(installed[16:16 + js])
entry = header['files']['package.json']
start = 8 + hs + int(entry['offset'])
version = json.loads(installed[start:start + entry['size']])['version']
if version != '4.0.10':
raise RuntimeError('This legacy patch supports only 4.0.10; installed version is ' + version)
BACKUP.mkdir(parents=True, exist_ok=True)
archive = RES / "app.asar"
frontend = RES / "frontend/_nuxt/D3oBzv6c.js"
locale = RES / "frontend/_nuxt/BU13Ari2.js"
for source in (archive, frontend, locale):
target = BACKUP / source.name
if not target.exists():
shutil.copy2(source, target)
raw = (BACKUP / "app.asar").read_bytes()
_, header_size, _, json_size = struct.unpack("<4I", raw[:16])
tree = json.loads(raw[16:16 + json_size])
data = raw[8 + header_size:]
entry = tree["files"]["build"]["files"]["backend.mjs"]
offset, size = int(entry["offset"]), entry["size"]
original = data[offset:offset + size]
if hashlib.sha256(original).hexdigest() != "0fc3b513625e17fc31822b2c5c5d7527a103d163c872e08535d2b4792df60ae2":
raise RuntimeError("Expected the inspected HuobaoDrama 4.0.10 backend")
backend = original.decode("utf-8")
start = backend.index("var MiniMaxVideoAdapter = class {")
end = backend.index("\n// ../backend/src/services/adapters/aliyun-wan-video.ts", start)
adapter = backend[start:end].replace("MiniMaxVideoAdapter", "LocalVideoAdapter").replace('provider = "minimax"', 'provider = "localvideo"')
guard_start = adapter.index(" if (!model.toLowerCase()")
guard_end = adapter.index(" const prompt =", guard_start)
adapter = adapter[:guard_start] + " if (!" + json.dumps(MODELS) + ".includes(model)) throw new Error(`Unknown local video model: ${model}`);\n" + adapter[guard_end:]
adapter = adapter.replace("MiniMax H3", "Local video")
resolution_start = adapter.index(" normalizeResolution(resolution) {")
adapter = adapter[:resolution_start] + ''' normalizeResolution(resolution) {
const value = (resolution || "720p").toUpperCase();
if (!["480P", "720P", "1080P", "768P", "2K"].includes(value)) throw new Error("Unsupported local video resolution");
return value;
}
};
'''
# Do not silently throw away file/link attachments unsupported by the local pool.
adapter = replace_once(adapter, ' const prompt =', ' if (record2.referenceFileUrl || record2.referenceLinkUrl) throw new Error("Local video requires image references");\n const prompt =')
backend = replace_once(backend, 'video: ["volcengine", "minimax", "aliyun"]', 'video: ["volcengine", "minimax", "aliyun", "localvideo"]')
backend = replace_once(backend, "var videoAdapters = {", adapter + "\nvar videoAdapters = {\n localvideo: new LocalVideoAdapter(),")
backend = replace_once(backend, ' if (p === "openai") {', ' if (p === "openai" || p === "localvideo") {')
encoded = backend.encode("utf-8")
entry["offset"], entry["size"] = str(len(data)), len(encoded)
block_size = entry["integrity"]["blockSize"]
entry["integrity"].update(hash=hashlib.sha256(encoded).hexdigest(), blocks=[hashlib.sha256(encoded[i:i + block_size]).hexdigest() for i in range(0, len(encoded), block_size)])
header = json.dumps(tree, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
pad = (-len(header)) % 4
header_size = 8 + len(header) + pad
# Keep production data beside the installation, outside the resettable
# Electron profile. An explicit storage-config.json still takes precedence.
main_entry = tree["files"]["dist"]["files"]["main.js"]
main_start = int(main_entry["offset"])
main_source = data[main_start:main_start + main_entry["size"]].decode("utf-8")
main_source = replace_once(main_source,
' const fallback = path5.join(import_electron3.app.getPath("userData"), "data");',
' const fallback = import_electron3.app.isPackaged ? path5.resolve(process.resourcesPath, "../../huobao-data") : path5.join(import_electron3.app.getPath("userData"), "data");')
main_bytes = main_source.encode("utf-8")
main_entry["offset"], main_entry["size"] = str(len(data) + len(encoded)), len(main_bytes)
main_block_size = main_entry["integrity"]["blockSize"]
main_entry["integrity"].update(hash=hashlib.sha256(main_bytes).hexdigest(), blocks=[hashlib.sha256(main_bytes[i:i + main_block_size]).hexdigest() for i in range(0, len(main_bytes), main_block_size)])
header = json.dumps(tree, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
pad = (-len(header)) % 4
header_size = 8 + len(header) + pad
packed = struct.pack("<4I", 4, header_size, header_size - 4, len(header)) + header + bytes(pad) + data + encoded + main_bytes
(WORK / "app.multivideo.asar").write_bytes(packed)
(WORK / "backend.multivideo.mjs").write_bytes(encoded)
(WORK / "main.persistent.js").write_bytes(main_bytes)
(WORK / "local-video-adapter.js").write_text(adapter, encoding="utf-8")
ui = (BACKUP / frontend.name).read_text("utf-8")
ui = replace_once(ui, 'Ls=["gemini","openai","volcengine","minimax","aliyun"]', 'Ls=["gemini","openai","volcengine","minimax","aliyun","localvideo"]')
preset = json.dumps({"label": "本地视频任务池", "baseUrl": "http://192.168.110.111:18792", "models": MODELS}, ensure_ascii=False, separators=(",", ":"))
ui = replace_once(ui, 'video:{aliyun:{', 'video:{localvideo:' + preset + ',aliyun:{')
(WORK / frontend.name).write_text(ui, encoding="utf-8")
translations = (BACKUP / locale.name).read_text("utf-8")
translations = replace_once(translations, "镜头视频直出生成,默认 Seedance 2.0", "镜头视频生成,可选择多个模型并设置默认")
translations = replace_once(translations, "Direct video generation, Seedance 2.0 by default", "Video generation with selectable models and defaults")
(WORK / locale.name).write_text(translations, encoding="utf-8")
if "--install" in sys.argv or "--frontend-only" in sys.argv:
if "--frontend-only" not in sys.argv:
shutil.copy2(WORK / "app.multivideo.asar", archive)
shutil.copy2(WORK / frontend.name, frontend)
shutil.copy2(WORK / locale.name, locale)
print("Installed local-video provider; originals:", BACKUP)
else:
print("Prepared local-video provider for", len(MODELS), "models")
if __name__ == "__main__":
prepare()