122 lines
7.3 KiB
Python
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()
|