"""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()